diff --git a/bin/fm-nm-run-lib.sh b/bin/fm-nm-run-lib.sh index 8241d093947..d6d552cb71c 100644 --- a/bin/fm-nm-run-lib.sh +++ b/bin/fm-nm-run-lib.sh @@ -97,6 +97,30 @@ fm_nm_head_descends_from() { # && git -C "$wt" merge-base --is-ancestor "$ancestor_full" "$descendant_full" 2>/dev/null } +# 0 when $3 is a faithful restamp of the validated chain from $2 in worktree $1. +# The base must be an ancestor of both heads, their commit counts must match, and +# each pair of commits in base-to-head order must carry the same tree object. +fm_nm_head_is_faithful_restamp() { # + local wt=$1 base=$2 validated=$3 candidate=$4 base_full validated_full candidate_full + local validated_list candidate_list validated_trees candidate_trees commit + base_full=$(fm_nm_resolve_head "$wt" "$base") || return 1 + validated_full=$(fm_nm_resolve_head "$wt" "$validated") || return 1 + candidate_full=$(fm_nm_resolve_head "$wt" "$candidate") || return 1 + git -C "$wt" merge-base --is-ancestor "$base_full" "$validated_full" 2>/dev/null || return 1 + git -C "$wt" merge-base --is-ancestor "$base_full" "$candidate_full" 2>/dev/null || return 1 + validated_list=$(git -C "$wt" rev-list --reverse "$base_full..$validated_full") || return 1 + candidate_list=$(git -C "$wt" rev-list --reverse "$base_full..$candidate_full") || return 1 + validated_trees=$(printf '%s\n' "$validated_list" | while IFS= read -r commit; do + [ -n "$commit" ] || continue + git -C "$wt" rev-parse --verify "${commit}^{tree}" || exit 1 + done) || return 1 + candidate_trees=$(printf '%s\n' "$candidate_list" | while IFS= read -r commit; do + [ -n "$commit" ] || continue + git -C "$wt" rev-parse --verify "${commit}^{tree}" || exit 1 + done) || return 1 + [ "$validated_trees" = "$candidate_trees" ] +} + # 0 when a run's branch presentation identifies the checked-out branch. The # no-mistakes CLI renders Firstmate's slash branch names with a hyphen, so both # authoritative spellings are accepted and no other branch is normalized. diff --git a/bin/fm-receipt-check.sh b/bin/fm-receipt-check.sh index 5998e0a423b..d6b2f8894ce 100755 --- a/bin/fm-receipt-check.sh +++ b/bin/fm-receipt-check.sh @@ -44,11 +44,17 @@ # The resolved validation_tier, validation_path, reason code, base, head, size, # and start time are appended to state/.meta for durable inspection. # Every completion records validation_completed_head and refuses current head -# drift unless the bound No-Mistakes run proves a descendant of the latest -# validation_head: an active run must currently own the branch, while a terminal -# PASSED run proves the advance through its own reported head. A terminal run -# therefore needs no replan and no fresh run to seal its own pipeline commits, -# and foreign commits landed after the run still refuse completion. +# drift unless the bound No-Mistakes run accounts for the current content, in +# one of two shapes. A descendant of the latest validation_head is proved by +# pipeline ownership while the run is active and by the run's own reported head +# once it is terminal and PASSED, so a terminal run needs no replan and no fresh +# run to seal its own pipeline commits. A chain the pipeline's rebase step +# restamped is proved as a faithful restamp of the validation-base-to-head +# chain, because that step re-commits every branch commit with a fresh committer +# stamp and so reports a head that is neither validation_head nor a descendant +# of it; --bind-run accepts and records that same restamped head. Foreign +# commits landed after the run still refuse completion because they break the +# chain's ancestry, count, or pairwise tree identity. # When --plan returns path=receipts-mechanical, append fresh successful mechanical # evidence for every changed file with: # @@ -633,6 +639,7 @@ mechanical_evidence_covers_file() { if [ "$ACTION" = bind-run ]; then BIND_WORKTREE=$(grep '^worktree=' "$META" | tail -1 | cut -d= -f2- || true) BIND_PATH=$(grep '^validation_path=' "$META" | tail -1 | cut -d= -f2- || true) + BIND_BASE=$(grep '^validation_base=' "$META" | tail -1 | cut -d= -f2- || true) BIND_HEAD=$(grep '^validation_head=' "$META" | tail -1 | cut -d= -f2- || true) BIND_GENERATION=$(grep '^validation_generation=' "$META" | tail -1 | cut -d= -f2- || true) BIND_PREPLAN_RUN=$(grep '^validation_preplan_run_id=' "$META" | tail -1 | cut -d= -f2- || true) @@ -640,6 +647,8 @@ if [ "$ACTION" = bind-run ]; then [ -n "$BIND_WORKTREE" ] && [ -d "$BIND_WORKTREE" ] || { echo "error: validation worktree is missing" >&2; exit 2; } BIND_HEAD=$(git -C "$BIND_WORKTREE" rev-parse --verify "$BIND_HEAD^{commit}" 2>/dev/null) \ || { echo "error: validated head is missing" >&2; exit 2; } + BIND_BASE=$(git -C "$BIND_WORKTREE" rev-parse --verify "$BIND_BASE^{commit}" 2>/dev/null) \ + || { echo "error: validation base is missing" >&2; exit 2; } fm_worktree_is_clean "$BIND_WORKTREE" \ || { echo "error: validation worktree is dirty" >&2; exit 2; } BIND_OUT=$(fm_nm_run_checked "$BIND_WORKTREE" "$NM_TIMEOUT" axi status --run "$RUN_ID_INPUT") \ @@ -665,15 +674,22 @@ if [ "$ACTION" = bind-run ]; then passed:*|checks-passed:*|*:passed|*:checks-passed) BIND_STATE_OK=1 ;; running:*|fixing:*|ci:*|awaiting_approval:*) BIND_STATE_OK=1 ;; esac + # The run's head is the planned commit itself or a faithful restamp of its + # validated chain, proven from the recorded validation base. + BIND_RUN_HEAD=$(fm_nm_resolve_head "$BIND_WORKTREE" "$BIND_OBSERVED_HEAD" || true) + if [ -n "$BIND_RUN_HEAD" ] && [ "$BIND_RUN_HEAD" != "$BIND_HEAD" ] \ + && ! fm_nm_head_is_faithful_restamp "$BIND_WORKTREE" "$BIND_BASE" "$BIND_HEAD" "$BIND_RUN_HEAD"; then + BIND_RUN_HEAD= + fi [ "$BIND_OBSERVED_ID" = "$RUN_ID_INPUT" ] \ - && [ "$(fm_nm_resolve_head "$BIND_WORKTREE" "$BIND_OBSERVED_HEAD" || true)" = "$BIND_HEAD" ] \ + && [ -n "$BIND_RUN_HEAD" ] \ && [ "$BIND_STATE_OK" -eq 1 ] \ || { echo "error: No-Mistakes run does not match the latest plan" >&2; exit 2; } [ -n "$BIND_GENERATION" ] || { echo "error: validation generation is missing" >&2; exit 2; } printf 'validation_run_id=%s\nvalidation_run_path=%s\nvalidation_run_head=%s\nvalidation_run_generation=%s\n' \ - "$RUN_ID_INPUT" "$BIND_PATH" "$BIND_HEAD" "$BIND_GENERATION" | append_meta_records \ + "$RUN_ID_INPUT" "$BIND_PATH" "$BIND_RUN_HEAD" "$BIND_GENERATION" | append_meta_records \ || { echo "error: could not bind the No-Mistakes run" >&2; exit 2; } - jq -cn --arg task "$ID" --arg run "$RUN_ID_INPUT" --arg path "$BIND_PATH" --arg head "$BIND_HEAD" \ + jq -cn --arg task "$ID" --arg run "$RUN_ID_INPUT" --arg path "$BIND_PATH" --arg head "$BIND_RUN_HEAD" \ '{schema:"fm-validation-run-binding.v1",task:$task,status:"bound",run:$run,path:$path,head:$head}' exit 0 fi @@ -713,7 +729,7 @@ if [ "$ACTION" = mechanical-ready ]; then fi record_validation_completed() { - local started path generation published_generation completed completed_head completed_path completed_evidence completed_generation now worktree validated_head current_head completion_head expected_evidence observed pr pr_head branch boundary new_receipts run_id run_path run_generation run_out observed_id observed_head observed_head_full outcome run_status default_ref default_branch ci_state run_ready changed_file completion_files validation_base run_branch current_branch branch_sync_state + local started path generation published_generation completed completed_head completed_path completed_evidence completed_generation now worktree validation_base validated_head current_head completion_head expected_evidence observed pr pr_head branch boundary new_receipts run_id run_path run_generation run_out observed_id observed_head observed_head_full outcome run_status default_ref default_branch ci_state run_ready changed_file completion_files run_branch current_branch branch_sync_state run_head_matches_current restamp_accounted VALIDATION_LOCK="$STATE/.$ID.validation-plan.lock" if ! mkdir "$VALIDATION_LOCK" 2>/dev/null; then VALIDATION_LOCK= @@ -724,6 +740,7 @@ record_validation_completed() { path=$(grep '^validation_path=' "$META" | tail -1 | cut -d= -f2- || true) generation=$(grep '^validation_generation=' "$META" | tail -1 | cut -d= -f2- || true) worktree=$(grep '^worktree=' "$META" | tail -1 | cut -d= -f2- || true) + validation_base=$(grep '^validation_base=' "$META" | tail -1 | cut -d= -f2- || true) validated_head=$(grep '^validation_head=' "$META" | tail -1 | cut -d= -f2- || true) case "$started" in ''|*[!0-9]*) release_validation_lock; echo "error: validation start timestamp is missing or invalid" >&2; return 1 ;; @@ -741,6 +758,10 @@ record_validation_completed() { || { release_validation_lock; echo "error: validation worktree is missing" >&2; return 1; } validated_head=$(git -C "$worktree" rev-parse --verify "$validated_head^{commit}" 2>/dev/null) \ || { release_validation_lock; echo "error: validated head is missing or invalid" >&2; return 1; } + if [ "$path" = full-no-mistakes ]; then + validation_base=$(git -C "$worktree" rev-parse --verify "$validation_base^{commit}" 2>/dev/null) \ + || { release_validation_lock; echo "error: validation base is missing or invalid" >&2; return 1; } + fi current_head=$(git -C "$worktree" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) \ || { release_validation_lock; echo "error: current worktree head is unavailable" >&2; return 1; } fm_worktree_is_clean "$worktree" \ @@ -799,36 +820,65 @@ record_validation_completed() { echo "error: bound No-Mistakes run did not pass checks at the exact validated head" >&2 return 1 fi - [ "$observed_head_full" = "$current_head" ] \ - || { release_validation_lock; echo "error: bound No-Mistakes run head is not the current worktree head" >&2; return 1; } + # The run must account for the current head itself or both heads must be + # faithful restamps of the validated chain from the recorded base. + restamp_accounted=0 + if [ "$observed_head_full" = "$current_head" ]; then + run_head_matches_current=1 + elif fm_nm_head_is_faithful_restamp "$worktree" "$validation_base" "$validated_head" "$observed_head_full" \ + && fm_nm_head_is_faithful_restamp "$worktree" "$validation_base" "$validated_head" "$current_head"; then + run_head_matches_current=1 + restamp_accounted=1 + else + run_head_matches_current=0 + fi + [ "$run_head_matches_current" -eq 1 ] \ + || { release_validation_lock; echo "error: bound No-Mistakes run head does not account for the current worktree content" >&2; return 1; } if [ "$current_head" != "$validated_head" ]; then + fm_nm_head_descends_from "$worktree" "$validated_head" "$current_head" \ + || fm_nm_head_is_faithful_restamp "$worktree" "$validation_base" "$validated_head" "$current_head" \ + || { release_validation_lock; echo "error: current head neither descends from nor reproduces the implementation head" >&2; return 1; } + completion_head=$current_head + fi + if [ "$current_head" != "$validated_head" ] || [ "$restamp_accounted" -eq 1 ]; then run_branch=$(fm_nm_field "$run_out" branch) current_branch=$(git -C "$worktree" symbolic-ref --quiet --short HEAD 2>/dev/null || true) - fm_nm_head_descends_from "$worktree" "$validated_head" "$current_head" \ - || { release_validation_lock; echo "error: current head is not a descendant of the implementation head" >&2; return 1; } if [ -z "$current_branch" ] || ! fm_nm_branch_matches_worktree "$worktree" "$run_branch"; then release_validation_lock echo "error: pipeline run branch is not the current worktree branch" >&2 return 1 fi # The advance is authoritative in exactly two shapes, and the head - # equality checked above is what keeps both honest. While the run is + # accounting checked above is what keeps both honest. While the run is # ACTIVE the pipeline must currently own the branch. Once the run is # TERMINAL it has released the branch, so pipeline ownership is gone by # construction and requiring it would refuse a genuinely passed run # whose own review and doc commits advanced the head; there the run's # own reported head is the authority, and a foreign commit landed after - # the run finished still fails the head-equality check because the run - # never reports it. + # the run finished still fails that accounting because the run reports + # neither that commit nor its content. if fm_nm_run_is_active "$run_out"; then branch_sync_state=$(fm_nm_branch_sync_state "$run_out") - [ "$branch_sync_state" = pipeline_owned ] \ - || { release_validation_lock; echo "error: current head advance lacks authoritative pipeline ownership" >&2; return 1; } + if [ "$branch_sync_state" != pipeline_owned ]; then + release_validation_lock + if [ "$restamp_accounted" -eq 1 ]; then + echo "error: accepted restamp lacks authoritative pipeline ownership" >&2 + else + echo "error: current head advance lacks authoritative pipeline ownership" >&2 + fi + return 1 + fi else - fm_nm_run_is_terminal_passed "$run_out" \ - || { release_validation_lock; echo "error: current head advanced without a passing bound pipeline run" >&2; return 1; } + if ! fm_nm_run_is_terminal_passed "$run_out"; then + release_validation_lock + if [ "$restamp_accounted" -eq 1 ]; then + echo "error: accepted restamp lacks a passing bound pipeline run" >&2 + else + echo "error: current head advanced without a passing bound pipeline run" >&2 + fi + return 1 + fi fi - completion_head=$current_head fi observed=bound-matching-no-mistakes-run ;; diff --git a/docs/verification/evidence-receipts.md b/docs/verification/evidence-receipts.md index 462eef5e54e..006ca9fc885 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -36,10 +36,11 @@ The exact receipt key and type schema is owned by the header and `--help` output - Findings that invalidate a receipt or acceptance claim atomically bind one generation-scoped idempotent finding-to-criterion marker to the invalidation-time head and receipt boundary, then require a strict non-empty descendant delta and a later successful receipt bound to the new head before replanning or completion. - One pinned state-directory owner snapshots single-link no-follow metadata and performs compare-bound atomic replacements for every validation metadata update. - PR registration publishes canonical PR identity and its validation publication generation through one compare-bound pinned metadata replacement after the watcher artifacts publish, and revokes those artifacts if that replacement fails. -- Successful exact-head runs can bind after reaching checks-passed or passed, while failed and cancelled runs remain ineligible. +- Successful planned-head or faithful-restamp runs can bind after reaching checks-passed or passed, while failed and cancelled runs remain ineligible. - No-Mistakes status, intent, and CI-log observations use the shared bounded call boundary. - Every completion requires path-specific terminal evidence and records its plan path and authoritative completed head. -- A changed worktree head invalidates completion unless the bound No-Mistakes run proves a descendant of the planned head: an active run must currently own the branch, while a terminal passed run proves the advance through its own reported head. +- A changed worktree head invalidates completion unless the bound No-Mistakes run proves either a descendant of the planned head or a faithful restamp of its validation-base-to-planned chain: an active run must currently own the branch, while a terminal passed run proves the advance through its own reported head. +- A chain the pipeline's rebase step restamped binds and completes only when it is a faithful restamp of the planned chain from the recorded validation base and passes the same branch and pipeline-ownership checks in either custody shape. - Unrelated, missing, or ambiguous drift remains refused, and a terminal run that did not pass never seals an advance. - Local-only readiness and guarded landing consume one fail-closed executable default-branch resolver. - Planning and completion refuse tracked, staged, or untracked worktree changes. @@ -47,6 +48,23 @@ The exact receipt key and type schema is owned by the header and `--help` output - Ordinary No-Mistakes findings return to the original worker through guarded custody return and then full revalidation. - Direct-PR registration publishes its watcher before recording completion, while other paths preserve their earlier path-specific completion boundary. +## Reconciliation with the descendant-advance guarantee + +The descendant-advance guarantee above admits one shape of head change: new commits landed on top of the planned head, so the planned head stays an ancestor of the current head. +The no-mistakes rebase step produces a second shape it does not admit. +That step re-commits every commit on the branch with a fresh committer stamp, which mints a new object id for the whole chain while every tree stays byte-identical, so the planned head stops being an ancestor of anything the run reports. +Observed on 2026-09-05 in run `01M1RW6JNH5C5VN15PPRYDW3J0`: planned head `874ce334` and run head `bd8aaff5` both carry tree `f7d8fa3a`, and `git merge-base --is-ancestor 874ce334 bd8aaff5` exits 1. + +Three checks were relaxed to admit that shape, and no others. +`--bind-run` accepted only a run head that resolved to the planned head, and now also accepts a head that faithfully restamps the validation-base-to-planned chain, which it then records as `validation_run_head`. +`--complete` required the run's reported head to be the current worktree head, and now also accepts run and current heads that both faithfully restamp the validation-base-to-planned chain, which is what a run reports after its custody return puts the branch back on the pre-rebase chain. +`--complete` required the current head to descend from the planned head, and now also accepts a current head that faithfully restamps the validation-base-to-planned chain. + +Chain provenance is the content-identity mechanism, stated once in `fm_nm_head_is_faithful_restamp` in `bin/fm-nm-run-lib.sh`. +It resolves the recorded validation base, requires it to be an ancestor of both heads, requires equal commit counts, and compares each corresponding commit tree in base-to-head order. +Foreign drift, unrelated same-tree tips, reverted foreign commits, and rebases onto changed bases stay refused because they break ancestry, count, or a pairwise tree comparison. +Every other completion requirement is unchanged: the run must still be the bound run at the current generation, still be genuinely passed or checks-green, and still report the current worktree branch while active with pipeline ownership or be terminal PASSED otherwise. + ## Known limitations - Claim invalidation reads the worktree head immediately before acquiring the validation metadata lock, so an unsupported concurrent ref mutation can bind the marker to the earlier head; the single-operator workflow excludes that mutation during validation. @@ -94,6 +112,8 @@ ok - finding-to-criterion invalidations remain inspectable in task metadata ok - run binding resolves abbreviated heads and rejects non-planned commits ok - binding and completion work against the real agent-supplied intent-log shape while wrong runs fail closed ok - terminal passed runs seal their own pipeline advance and refuse foreign drift +ok - pipeline rebase restamps bind and seal their validated content +ok - restamped chains enforce provenance and ownership ok - low-risk mechanical changes can skip a full No-Mistakes run ok - low risk requires safe changelog prose and file-bound mechanical evidence ok - implementation completion refreshes per head and remains idempotent diff --git a/tests/fm-receipt-check.test.sh b/tests/fm-receipt-check.test.sh index ee9e02b47f6..f3c86ef26ca 100755 --- a/tests/fm-receipt-check.test.sh +++ b/tests/fm-receipt-check.test.sh @@ -1011,6 +1011,264 @@ test_terminal_passed_run_seals_its_own_pipeline_head_advance() { pass "terminal passed runs seal their own pipeline advance and refuse foreign drift" } +# Re-commit every commit in ..HEAD with the same tree under a fresh +# committer stamp, exactly as the no-mistakes rebase step does, leave the branch +# on the rewritten chain, and print the rewritten head. Every rewritten commit +# gets a new object id, so the pre-rewrite head is not an ancestor of the result. +restamp_chain() { # + local project=$1 parent=$2 commit tree subject + while IFS= read -r commit; do + tree=$(git -C "$project" rev-parse "$commit^{tree}") || fail "could not read $commit tree" + subject=$(git -C "$project" log -1 --format=%s "$commit") || fail "could not read $commit subject" + parent=$(GIT_AUTHOR_DATE='@1000000000 +0000' GIT_COMMITTER_DATE='@1000000000 +0000' \ + git -C "$project" commit-tree "$tree" -p "$parent" -m "$subject") \ + || fail "could not restamp $commit" + done < <(git -C "$project" rev-list --reverse "$2..HEAD") + git -C "$project" reset -q --hard "$parent" || fail "could not check out the restamped chain" + printf '%s\n' "$parent" +} + +# Set up one planned full-No-Mistakes task whose branch carries a multi-commit +# chain, and print " ". +plan_restamp_fixture() { # + local id=$1 base project + base=$(make_project "$id" no-mistakes localized) + project="$TMP_ROOT/project-$id" + printf 'second change\n' >> "$project/tests/app.test.sh" + git -C "$project" add tests/app.test.sh + git -C "$project" commit -q -m 'second implementation commit' + add_receipt "$id" AC1 test "2 passed" + add_receipt "$id" AC2 lint passed + FM_FAKE_NM_STATUS='' FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --plan --base "$base" >/dev/null || fail "$id restamp fixture plan failed" + printf '%s %s %s %s\n' "$base" "$project" \ + "$(git -C "$project" rev-parse HEAD)" \ + "$(grep '^validation_generation=' "$HOME_DIR/state/$id.meta" | tail -1 | cut -d= -f2-)" +} + +test_pipeline_rebase_restamp_binds_and_seals_identical_content() { + local id base project validated_head restamped generation status out rc + + # The deadlock: the pipeline's rebase step re-commits the whole branch with a + # fresh committer stamp, so the run reports a head that is neither the planned + # commit nor a descendant of it, while every tree it validated is unchanged. + id=receipt-restamp-seal + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + [ "$restamped" != "$validated_head" ] || fail "restamp fixture did not rewrite the chain" + git -C "$project" merge-base --is-ancestor "$validated_head" "$restamped" 2>/dev/null \ + && fail "restamp fixture left the validated head an ancestor" + [ "$(git -C "$project" rev-parse "$restamped^{tree}")" = "$(git -C "$project" rev-parse "$validated_head^{tree}")" ] \ + || fail "restamp fixture did not preserve the validated content" + status=$(nm_status RUN-restamp "$restamped" pending) + out=$(FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp --generation "$generation") \ + || fail "bind refused the pipeline's restamped head" + printf '%s' "$out" | jq -e --arg head "$restamped" '.status == "bound" and .head == $head' >/dev/null \ + || fail "bind did not record the restamped head under validation" + [ "$(grep '^validation_run_head=' "$HOME_DIR/state/$id.meta" | tail -1 | cut -d= -f2-)" = "$restamped" ] \ + || fail "bound run head metadata did not follow the restamped chain" + status=$(nm_pipeline_status RUN-restamp "fm/$id" "$restamped" completed passed agent_owned) + out=$(FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed) \ + || fail "completion refused a passed run whose rebase restamped the chain" + printf '%s' "$out" | jq -e --arg head "$restamped" '.status == "completed" and .completed_head == $head' >/dev/null \ + || fail "restamped completion did not bind the rewritten head" + + # Custody returned: the branch is back on the validated chain while the passed + # run still reports the restamped head it validated. + id=receipt-restamp-custody-returned + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + git -C "$project" reset -q --hard "$validated_head" + status=$(nm_status RUN-custody "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-custody --generation "$generation" >/dev/null \ + || fail "bind refused a restamped head after custody return" + status=$(nm_pipeline_status RUN-custody "fm/$id" "$restamped" completed passed agent_owned) + out=$(FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed) \ + || fail "completion refused a restamped run after custody return" + printf '%s' "$out" | jq -e --arg head "$validated_head" '.completed_head == $head' >/dev/null \ + || fail "custody-returned completion did not seal the validated head" + + # A run that did not pass never seals a restamped chain. + id=receipt-restamp-not-passed + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + status=$(nm_status RUN-restamp-cancelled "$restamped" cancelled) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-cancelled --generation "$generation" >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "bind accepted a cancelled run on a restamped chain" + status=$(nm_status RUN-restamp-pending "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-pending --generation "$generation" >/dev/null \ + || fail "restamp not-passed fixture binding failed" + status=$(nm_pipeline_status RUN-restamp-pending "fm/$id" "$restamped" failed failed agent_owned) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "completion sealed a restamped chain from a run that did not pass" + pass "pipeline rebase restamps bind and seal their validated content" +} + +test_restamped_chains_refuse_foreign_content_and_unowned_rewrites() { + local id base project validated_head restamped generation status rc + + # Foreign content: the chain is rewritten AND carries an unvalidated edit, so + # its tree differs from the validated tree and neither bind nor complete may + # accept it. + id=receipt-restamp-foreign + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + printf 'unvalidated edit\n' >> "$project/src/app.sh" + git -C "$project" add src/app.sh + git -C "$project" commit -q --amend --no-edit + restamped=$(git -C "$project" rev-parse HEAD) + git -C "$project" merge-base --is-ancestor "$validated_head" "$restamped" 2>/dev/null \ + && fail "foreign restamp fixture left the validated head an ancestor" + [ "$(git -C "$project" rev-parse "$restamped^{tree}")" != "$(git -C "$project" rev-parse "$validated_head^{tree}")" ] \ + || fail "foreign restamp fixture did not change the validated content" + status=$(nm_status RUN-restamp-foreign "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-foreign --generation "$generation" >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "bind accepted a rewritten chain carrying foreign content" + git -C "$project" reset -q --hard "$validated_head" + status=$(nm_status RUN-restamp-clean "$(restamp_chain "$project" "$base")" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-clean --generation "$generation" >/dev/null \ + || fail "foreign fixture control binding failed" + printf 'unvalidated edit\n' >> "$project/src/app.sh" + git -C "$project" add src/app.sh + git -C "$project" commit -q --amend --no-edit + restamped=$(git -C "$project" rev-parse HEAD) + status=$(nm_pipeline_status RUN-restamp-clean "fm/$id" "$restamped" completed passed agent_owned) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "completion sealed a rewritten chain carrying foreign content" + + # Unrelated same-tree tip: matching only the final tree must not bypass the + # different-parent and chain-count checks. + id=receipt-restamp-unrelated-same-tree + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + validated_tree=$(git -C "$project" rev-parse "$validated_head^{tree}") + foreign_parent=$(git -C "$project" commit-tree "$validated_tree" -p "$base" -m unrelated-parent) + unrelated=$(git -C "$project" commit-tree "$validated_tree" -p "$foreign_parent" -m unrelated-tip) + git -C "$project" reset -q --hard "$unrelated" + status=$(nm_status RUN-restamp-unrelated "$unrelated" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-unrelated --generation "$generation" >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "bind accepted an unrelated same-tree tip" + + # Reverted foreign commit: the final tree matches, but the extra commits and + # their intermediate tree make the candidate chain unfaithful. + id=receipt-restamp-reverted-foreign + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + printf 'foreign then reverted\n' > "$project/src/foreign.sh" + git -C "$project" add src/foreign.sh + git -C "$project" commit -q -m foreign-change + rm "$project/src/foreign.sh" + git -C "$project" add -u + git -C "$project" commit -q -m revert-foreign-change + reverted=$(git -C "$project" rev-parse HEAD) + [ "$(git -C "$project" rev-parse "$reverted^{tree}")" = "$(git -C "$project" rev-parse "$validated_head^{tree}")" ] \ + || fail "reverted foreign fixture did not restore the tip tree" + status=$(nm_status RUN-restamp-reverted "$reverted" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-reverted --generation "$generation" >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "bind accepted a reverted foreign chain" + + # Changed-base rebase: replay the task commits onto a newer base whose tree + # differs, which is new unvalidated content despite preserving commit count. + id=receipt-restamp-changed-base + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + git -C "$project" checkout -q main + printf 'new base content\n' >> "$project/README.md" + git -C "$project" add README.md + git -C "$project" commit -q -m newer-base + newer_base=$(git -C "$project" rev-parse HEAD) + git -C "$project" branch -f "fm/$id" "$newer_base" + git -C "$project" checkout -q "fm/$id" + while IFS= read -r commit; do + git -C "$project" cherry-pick "$commit" >/dev/null \ + || fail "changed-base fixture could not replay the task chain" + done < <(git -C "$project" rev-list --reverse "$base..$validated_head") + rebased=$(git -C "$project" rev-parse HEAD) + status=$(nm_status RUN-restamp-changed-base "$rebased" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-changed-base --generation "$generation" >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "bind accepted a chain rebased onto a changed base" + + # Unowned rewrite: the bound run reports another branch, so the rewritten + # worktree chain is not the chain that run owns. + id=receipt-restamp-unowned-branch + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + status=$(nm_status RUN-restamp-branch "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-branch --generation "$generation" >/dev/null \ + || fail "unowned-branch fixture binding failed" + status=$(nm_pipeline_status RUN-restamp-branch someone-elses-branch "$restamped" completed passed agent_owned) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "completion sealed a restamped chain owned by another branch" + + # Unowned rewrite: an active run that does not currently own the branch never + # proves the rewrite it is credited with. + id=receipt-restamp-unowned-active + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + status=$(nm_status RUN-restamp-active "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-active --generation "$generation" >/dev/null \ + || fail "unowned-active fixture binding failed" + status=$(nm_pipeline_status RUN-restamp-active "fm/$id" "$restamped" ci '' manual) + FM_FAKE_NM_STATUS="$status" FM_FAKE_NM_CI_LOG='all CI checks passed - still monitoring' \ + FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "completion sealed a restamped chain without pipeline ownership" + + # Custody-returned rewrite: equal current and validated heads still require + # branch ownership when the run reports a different faithful restamp. + id=receipt-restamp-custody-wrong-branch + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + git -C "$project" reset -q --hard "$validated_head" + status=$(nm_status RUN-restamp-wrong-branch "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-wrong-branch --generation "$generation" >/dev/null \ + || fail "custody wrong-branch fixture binding failed" + status=$(nm_pipeline_status RUN-restamp-wrong-branch someone-elses-branch "$restamped" completed passed agent_owned) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "custody-returned restamp with another branch" + + id=receipt-restamp-custody-unowned-active + read -r base project validated_head generation < <(plan_restamp_fixture "$id") + restamped=$(restamp_chain "$project" "$base") + git -C "$project" reset -q --hard "$validated_head" + status=$(nm_status RUN-restamp-custody-active "$restamped" pending) + FM_FAKE_NM_STATUS="$status" FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --bind-run RUN-restamp-custody-active --generation "$generation" >/dev/null \ + || fail "custody active fixture binding failed" + status=$(nm_pipeline_status RUN-restamp-custody-active "fm/$id" "$restamped" ci '' manual) + FM_FAKE_NM_STATUS="$status" FM_FAKE_NM_CI_LOG='all CI checks passed - still monitoring' \ + FM_NO_MISTAKES_BIN="$FAKE_NO_MISTAKES" FM_HOME="$HOME_DIR" \ + "$CHECK" "$id" --complete --terminal-evidence no-mistakes-passed >/dev/null 2>&1 + rc=$? + expect_code 2 "$rc" "custody-returned active restamp without ownership" + pass "restamped chains enforce provenance and ownership" +} + test_no_mistakes_observations_are_bounded() { local hang_nm id base project head generation running ci_status rc hang_nm="$TMP_ROOT/hang-no-mistakes" @@ -1563,6 +1821,8 @@ test_run_heads_resolve_authoritatively test_agent_supplied_intent_log_binds_and_completes test_completion_accepts_only_pipeline_owned_head_advance test_terminal_passed_run_seals_its_own_pipeline_head_advance +test_pipeline_rebase_restamp_binds_and_seals_identical_content +test_restamped_chains_refuse_foreign_content_and_unowned_rewrites test_low_risk_skips_no_mistakes_under_explicit_policy test_low_risk_requires_safe_prose_and_applicable_evidence test_implementation_completion_precedes_planning