From f2de6b1eb9ac06a0fc61b53f823edc0f7c891de7 Mon Sep 17 00:00:00 2001 From: dnth Date: Sat, 5 Sep 2026 22:44:22 +0800 Subject: [PATCH 1/7] fix(receipts): seal runs whose rebase restamped the branch The no-mistakes rebase step re-commits every branch commit with a fresh committer stamp. Every tree stays byte-identical, but the whole chain gets new object ids, so the planned implementation head stops being an ancestor of anything the run reports. `--bind-run` then refused the run because its head no longer resolved to the planned head, and `--complete` refused it because the current head was not a descendant of the planned head. A genuinely passed run could not seal without a replan and a fresh run. Accept that shape through content identity. `fm_nm_head_content_identical` compares the tree object Git already computes for each commit: it holds for a pure restamp and breaks on any change to any tracked file. `--bind-run` now accepts a run head recording the planned head's tree and records that restamped head; `--complete` accepts a run head recording the current head's tree, and a current head recording the planned head's tree. Every other requirement is unchanged. The run must still be the bound run at the current generation, still be genuinely passed or checks-green, still report the current worktree branch with pipeline ownership while active, and still be terminal PASSED otherwise. The descendant-advance path added for commits landed on top of the validated head keeps its exact behavior. Claude-Session: https://claude.ai/code/session_01WGckfiJn9GAx7n4jCJYntc --- bin/fm-nm-run-lib.sh | 19 +++ bin/fm-receipt-check.sh | 52 ++++++-- docs/verification/evidence-receipts.md | 20 +++ tests/fm-receipt-check.test.sh | 174 +++++++++++++++++++++++++ 4 files changed, 252 insertions(+), 13 deletions(-) diff --git a/bin/fm-nm-run-lib.sh b/bin/fm-nm-run-lib.sh index 8241d093947..fcdd875a3cc 100644 --- a/bin/fm-nm-run-lib.sh +++ b/bin/fm-nm-run-lib.sh @@ -97,6 +97,25 @@ fm_nm_head_descends_from() { # && git -C "$wt" merge-base --is-ancestor "$ancestor_full" "$descendant_full" 2>/dev/null } +# 0 when commits $2 and $3 both resolve in worktree $1 and record byte-identical +# content, which Git states exactly once as their tree object identity. +# The no-mistakes rebase step re-commits an entire branch with fresh committer +# stamps, so the head it reports is neither the pre-rebase commit nor a +# descendant of it while the content it validated is unchanged. Tree identity is +# the authoritative content proof for that rewrite: it holds for a pure restamp +# and breaks on any change to any tracked file, so a caller may accept a +# rewritten chain without ever accepting foreign content. Identical commits are +# trivially content identical and are accepted; a caller that also needs the two +# commits to differ compares them itself. +fm_nm_head_content_identical() { # + local wt=$1 a_full b_full a_tree b_tree + a_full=$(fm_nm_resolve_head "$wt" "$2") || return 1 + b_full=$(fm_nm_resolve_head "$wt" "$3") || return 1 + a_tree=$(git -C "$wt" rev-parse --verify "${a_full}^{tree}" 2>/dev/null) || return 1 + b_tree=$(git -C "$wt" rev-parse --verify "${b_full}^{tree}" 2>/dev/null) || return 1 + [ "$a_tree" = "$b_tree" ] +} + # 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..b5dcd1ae66a 100755 --- a/bin/fm-receipt-check.sh +++ b/bin/fm-receipt-check.sh @@ -44,11 +44,16 @@ # 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 by tree identity with validation_head, 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 change the tree the run reported. # When --plan returns path=receipts-mechanical, append fresh successful mechanical # evidence for every changed file with: # @@ -665,15 +670,27 @@ 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 the same content re-committed + # by the pipeline's own rebase step, which restamps every branch commit and so + # reports a head that is neither the planned commit nor a descendant of it. + # Tree identity is what keeps that rewrite honest: it proves the run is + # validating exactly the planned change, while any foreign edit changes the + # tree and is still refused here. The bound head is the head the run actually + # reports, so completion below compares against the chain under validation. + 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_content_identical "$BIND_WORKTREE" "$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 @@ -799,27 +816,36 @@ record_validation_completed() { echo "error: bound No-Mistakes run did not pass checks at the exact validated head" >&2 return 1 fi + # The run must account for the content the worktree currently holds. It + # does that by reporting the current head itself, or by reporting a head + # its own rebase step restamped: the pipeline re-commits the whole branch + # with fresh committer stamps, so the run head and the worktree head are + # then different commits recording the same trees. Tree identity is the + # content proof for that shape, so a foreign change still refuses here + # because it changes the tree the run reported. [ "$observed_head_full" = "$current_head" ] \ - || { release_validation_lock; echo "error: bound No-Mistakes run head is not the current worktree head" >&2; return 1; } + || fm_nm_head_content_identical "$worktree" "$observed_head_full" "$current_head" \ + || { 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 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; } + || fm_nm_head_content_identical "$worktree" "$validated_head" "$current_head" \ + || { release_validation_lock; echo "error: current head neither descends from nor reproduces 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 ] \ diff --git a/docs/verification/evidence-receipts.md b/docs/verification/evidence-receipts.md index 462eef5e54e..cceb56042a0 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -40,6 +40,7 @@ The exact receipt key and type schema is owned by the header and `--help` output - 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 chain the pipeline's rebase step restamped binds and completes on tree identity with the planned head, so a genuinely passed run seals without a replan even though its head is neither the planned commit nor a descendant of it. - 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 recording the planned head's tree, 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 a run head recording the current head's tree, 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 recording the planned head's tree. + +Tree identity is the content-identity mechanism, stated once in `fm_nm_head_content_identical` in `bin/fm-nm-run-lib.sh`. +It was chosen over a cumulative `validation_base..head` diff comparison because Git already computes and names the content of a commit exactly once as its tree object, so the comparison needs no diff options, no rename policy, and no second base to resolve. +Foreign drift stays refused because any change to any tracked file changes that tree, so the rewritten chain no longer matches the head the run reported. +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, still report the current worktree branch while active with pipeline ownership, and still 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 refuse foreign content and rewrites the bound run does not own 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..ec7dd51aa1e 100755 --- a/tests/fm-receipt-check.test.sh +++ b/tests/fm-receipt-check.test.sh @@ -1011,6 +1011,178 @@ 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" + + # 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" + pass "restamped chains refuse foreign content and rewrites the bound run does not own" +} + 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 +1735,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 From 3deb8175f49a319a7eb3c824188b10e95f989ba6 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 00:53:25 +0800 Subject: [PATCH 2/7] no-mistakes(review): Enforce faithful chain provenance for restamped heads --- bin/fm-nm-run-lib.sh | 39 +++++++++++++---------- bin/fm-receipt-check.sh | 44 ++++++++++++-------------- docs/verification/evidence-receipts.md | 14 ++++---- 3 files changed, 50 insertions(+), 47 deletions(-) diff --git a/bin/fm-nm-run-lib.sh b/bin/fm-nm-run-lib.sh index fcdd875a3cc..d5125e84763 100644 --- a/bin/fm-nm-run-lib.sh +++ b/bin/fm-nm-run-lib.sh @@ -97,23 +97,28 @@ fm_nm_head_descends_from() { # && git -C "$wt" merge-base --is-ancestor "$ancestor_full" "$descendant_full" 2>/dev/null } -# 0 when commits $2 and $3 both resolve in worktree $1 and record byte-identical -# content, which Git states exactly once as their tree object identity. -# The no-mistakes rebase step re-commits an entire branch with fresh committer -# stamps, so the head it reports is neither the pre-rebase commit nor a -# descendant of it while the content it validated is unchanged. Tree identity is -# the authoritative content proof for that rewrite: it holds for a pure restamp -# and breaks on any change to any tracked file, so a caller may accept a -# rewritten chain without ever accepting foreign content. Identical commits are -# trivially content identical and are accepted; a caller that also needs the two -# commits to differ compares them itself. -fm_nm_head_content_identical() { # - local wt=$1 a_full b_full a_tree b_tree - a_full=$(fm_nm_resolve_head "$wt" "$2") || return 1 - b_full=$(fm_nm_resolve_head "$wt" "$3") || return 1 - a_tree=$(git -C "$wt" rev-parse --verify "${a_full}^{tree}" 2>/dev/null) || return 1 - b_tree=$(git -C "$wt" rev-parse --verify "${b_full}^{tree}" 2>/dev/null) || return 1 - [ "$a_tree" = "$b_tree" ] +# 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_tree candidate_tree i + local -a validated_commits=() candidate_commits=() + 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 + if [ -n "$validated_list" ]; then mapfile -t validated_commits <<< "$validated_list"; fi + if [ -n "$candidate_list" ]; then mapfile -t candidate_commits <<< "$candidate_list"; fi + [ "${#validated_commits[@]}" -eq "${#candidate_commits[@]}" ] || return 1 + for i in "${!validated_commits[@]}"; do + validated_tree=$(git -C "$wt" rev-parse --verify "${validated_commits[$i]}^{tree}") || return 1 + candidate_tree=$(git -C "$wt" rev-parse --verify "${candidate_commits[$i]}^{tree}") || return 1 + [ "$validated_tree" = "$candidate_tree" ] || return 1 + done } # 0 when a run's branch presentation identifies the checked-out branch. The diff --git a/bin/fm-receipt-check.sh b/bin/fm-receipt-check.sh index b5dcd1ae66a..8b102ae1768 100755 --- a/bin/fm-receipt-check.sh +++ b/bin/fm-receipt-check.sh @@ -49,11 +49,12 @@ # 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 by tree identity with validation_head, 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 change the tree the run reported. +# 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: # @@ -638,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) @@ -645,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") \ @@ -670,16 +674,11 @@ 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 the same content re-committed - # by the pipeline's own rebase step, which restamps every branch commit and so - # reports a head that is neither the planned commit nor a descendant of it. - # Tree identity is what keeps that rewrite honest: it proves the run is - # validating exactly the planned change, while any foreign edit changes the - # tree and is still refused here. The bound head is the head the run actually - # reports, so completion below compares against the chain under validation. + # 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_content_identical "$BIND_WORKTREE" "$BIND_HEAD" "$BIND_RUN_HEAD"; then + && ! 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" ] \ @@ -730,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 VALIDATION_LOCK="$STATE/.$ID.validation-plan.lock" if ! mkdir "$VALIDATION_LOCK" 2>/dev/null; then VALIDATION_LOCK= @@ -741,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 ;; @@ -758,6 +758,8 @@ 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; } + 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; } 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" \ @@ -816,21 +818,17 @@ record_validation_completed() { echo "error: bound No-Mistakes run did not pass checks at the exact validated head" >&2 return 1 fi - # The run must account for the content the worktree currently holds. It - # does that by reporting the current head itself, or by reporting a head - # its own rebase step restamped: the pipeline re-commits the whole branch - # with fresh committer stamps, so the run head and the worktree head are - # then different commits recording the same trees. Tree identity is the - # content proof for that shape, so a foreign change still refuses here - # because it changes the tree the run reported. + # The run must account for the current head itself or both heads must be + # faithful restamps of the validated chain from the recorded base. [ "$observed_head_full" = "$current_head" ] \ - || fm_nm_head_content_identical "$worktree" "$observed_head_full" "$current_head" \ + || { 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"; } \ || { 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 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" \ - || fm_nm_head_content_identical "$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; } if [ -z "$current_branch" ] || ! fm_nm_branch_matches_worktree "$worktree" "$run_branch"; then release_validation_lock diff --git a/docs/verification/evidence-receipts.md b/docs/verification/evidence-receipts.md index cceb56042a0..e008df1e8f7 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -40,7 +40,7 @@ The exact receipt key and type schema is owned by the header and `--help` output - 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 chain the pipeline's rebase step restamped binds and completes on tree identity with the planned head, so a genuinely passed run seals without a replan even though its head is neither the planned commit nor a descendant of it. +- 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, so a genuinely passed run seals without a replan even though its head is neither the planned commit nor a descendant of it. - 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. @@ -56,13 +56,13 @@ That step re-commits every commit on the branch with a fresh committer stamp, wh 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 recording the planned head's tree, 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 a run head recording the current head's tree, 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 recording the planned head's tree. +`--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. -Tree identity is the content-identity mechanism, stated once in `fm_nm_head_content_identical` in `bin/fm-nm-run-lib.sh`. -It was chosen over a cumulative `validation_base..head` diff comparison because Git already computes and names the content of a commit exactly once as its tree object, so the comparison needs no diff options, no rename policy, and no second base to resolve. -Foreign drift stays refused because any change to any tracked file changes that tree, so the rewritten chain no longer matches the head the run reported. +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, still report the current worktree branch while active with pipeline ownership, and still be terminal PASSED otherwise. ## Known limitations From a2e7fce05d6b0ee6fe340f77c2a5abaabf9c0c39 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 01:03:28 +0800 Subject: [PATCH 3/7] no-mistakes(review): Enforce ownership checks for custody-returned restamps --- bin/fm-receipt-check.sh | 46 ++++++++++++++++++++------ docs/verification/evidence-receipts.md | 4 +-- tests/fm-receipt-check.test.sh | 31 +++++++++++++++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/bin/fm-receipt-check.sh b/bin/fm-receipt-check.sh index 8b102ae1768..1235f8bbb7a 100755 --- a/bin/fm-receipt-check.sh +++ b/bin/fm-receipt-check.sh @@ -729,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 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 + 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= @@ -820,16 +820,27 @@ record_validation_completed() { fi # The run must account for the current head itself or both heads must be # faithful restamps of the validated chain from the recorded base. - [ "$observed_head_full" = "$current_head" ] \ - || { 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"; } \ + 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 - 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" \ || 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) 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 @@ -846,13 +857,26 @@ record_validation_completed() { # 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 e008df1e8f7..c1ebada278d 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -40,7 +40,7 @@ The exact receipt key and type schema is owned by the header and `--help` output - 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 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, so a genuinely passed run seals without a replan even though its head is neither the planned commit nor a descendant of it. +- 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. @@ -63,7 +63,7 @@ Three checks were relaxed to admit that shape, and no others. 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, still report the current worktree branch while active with pipeline ownership, and still be terminal PASSED otherwise. +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 diff --git a/tests/fm-receipt-check.test.sh b/tests/fm-receipt-check.test.sh index ec7dd51aa1e..18cfa451e3e 100755 --- a/tests/fm-receipt-check.test.sh +++ b/tests/fm-receipt-check.test.sh @@ -1180,6 +1180,37 @@ test_restamped_chains_refuse_foreign_content_and_unowned_rewrites() { "$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 refuse foreign content and rewrites the bound run does not own" } From 677a30888dfeb54629ed29303469b177710da949 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 01:13:33 +0800 Subject: [PATCH 4/7] no-mistakes(review): Ensure Bash 3.2 compatibility and adversarial restamp coverage --- bin/fm-nm-run-lib.sh | 20 +++++----- docs/verification/evidence-receipts.md | 2 +- tests/fm-receipt-check.test.sh | 55 +++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/bin/fm-nm-run-lib.sh b/bin/fm-nm-run-lib.sh index d5125e84763..d6d552cb71c 100644 --- a/bin/fm-nm-run-lib.sh +++ b/bin/fm-nm-run-lib.sh @@ -102,8 +102,7 @@ fm_nm_head_descends_from() { # # 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_tree candidate_tree i - local -a validated_commits=() candidate_commits=() + 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 @@ -111,14 +110,15 @@ fm_nm_head_is_faithful_restamp() { # /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 - if [ -n "$validated_list" ]; then mapfile -t validated_commits <<< "$validated_list"; fi - if [ -n "$candidate_list" ]; then mapfile -t candidate_commits <<< "$candidate_list"; fi - [ "${#validated_commits[@]}" -eq "${#candidate_commits[@]}" ] || return 1 - for i in "${!validated_commits[@]}"; do - validated_tree=$(git -C "$wt" rev-parse --verify "${validated_commits[$i]}^{tree}") || return 1 - candidate_tree=$(git -C "$wt" rev-parse --verify "${candidate_commits[$i]}^{tree}") || return 1 - [ "$validated_tree" = "$candidate_tree" ] || return 1 - done + 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 diff --git a/docs/verification/evidence-receipts.md b/docs/verification/evidence-receipts.md index c1ebada278d..57361ba455f 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -113,7 +113,7 @@ 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 refuse foreign content and rewrites the bound run does not own +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 18cfa451e3e..1449e3e851f 100755 --- a/tests/fm-receipt-check.test.sh +++ b/tests/fm-receipt-check.test.sh @@ -1150,6 +1150,59 @@ test_restamped_chains_refuse_foreign_content_and_unowned_rewrites() { 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" + git -C "$project" cherry-pick $(git -C "$project" rev-list --reverse "$base..$validated_head") >/dev/null \ + || fail "changed-base fixture could not replay the task chain" + 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 @@ -1211,7 +1264,7 @@ test_restamped_chains_refuse_foreign_content_and_unowned_rewrites() { "$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 refuse foreign content and rewrites the bound run does not own" + pass "restamped chains enforce provenance and ownership" } test_no_mistakes_observations_are_bounded() { From 717cb74a64746f801bdb656d3b7943a9843fa746 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 01:23:41 +0800 Subject: [PATCH 5/7] no-mistakes(document): Updated receipt restamp guarantees --- docs/verification/evidence-receipts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/verification/evidence-receipts.md b/docs/verification/evidence-receipts.md index 57361ba455f..006ca9fc885 100644 --- a/docs/verification/evidence-receipts.md +++ b/docs/verification/evidence-receipts.md @@ -36,10 +36,10 @@ 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. From fdfe0af4ef961c2c4c711d65a33c3864d8a32006 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 01:36:38 +0800 Subject: [PATCH 6/7] no-mistakes(lint): Replaced unsafe cherry-pick command substitution with Bash-compatible loop --- tests/fm-receipt-check.test.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/fm-receipt-check.test.sh b/tests/fm-receipt-check.test.sh index 1449e3e851f..f3c86ef26ca 100755 --- a/tests/fm-receipt-check.test.sh +++ b/tests/fm-receipt-check.test.sh @@ -1194,8 +1194,10 @@ test_restamped_chains_refuse_foreign_content_and_unowned_rewrites() { newer_base=$(git -C "$project" rev-parse HEAD) git -C "$project" branch -f "fm/$id" "$newer_base" git -C "$project" checkout -q "fm/$id" - git -C "$project" cherry-pick $(git -C "$project" rev-list --reverse "$base..$validated_head") >/dev/null \ - || fail "changed-base fixture could not replay the task chain" + 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" \ From adf716041460e96c4adcc987f4b964abb6e23c89 Mon Sep 17 00:00:00 2001 From: dnth Date: Sun, 6 Sep 2026 02:18:21 +0800 Subject: [PATCH 7/7] no-mistakes(ci): Fixed bin/fm-receipt-check.sh to require validation_base only for full-no-mistakes completion, preserving direct-PR/local completion paths that legitimately lack it. Verified with fm-pr-check-security, fm-receipt-check, bin/fm-lint.sh, and git diff --check --- bin/fm-receipt-check.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/fm-receipt-check.sh b/bin/fm-receipt-check.sh index 1235f8bbb7a..d6b2f8894ce 100755 --- a/bin/fm-receipt-check.sh +++ b/bin/fm-receipt-check.sh @@ -758,8 +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; } - 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; } + 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" \