fix(engineer): stop pre-mutation guard tripping on .simard-engineer-claim (#2621)#2623
Merged
rysweet merged 5 commits intoJul 6, 2026
Conversation
…laim (#2621) The engineer-loop pre-mutation guard aborted every mutating engineer whose target repo did not gitignore Simard's private `.simard-engineer-claim` sentinel. Simard drops that sentinel into each engineer worktree but relied on the *target* repo's `.gitignore` to hide it, so external governed repos (e.g. agent-kgpacks-rs) reported `worktree_dirty == true` on the untracked sentinel and the guard failed Intake before the coding agent was ever spawned — an infinite engineer-dispatch loop with zero progress. Repo-agnostic, belt-and-suspenders fix: 1. Worktree creation (`engineer_worktree::allocate`): append `.simard-engineer-claim` to the new worktree's git exclude file, resolved via `git rev-parse --git-path info/exclude`. Idempotent, creates the file if absent, best-effort non-fatal with a WARN log. 2. `inspect_workspace`: filter `ENGINEER_CLAIM_FILE` out of both `changed_files` and the `worktree_dirty` determination so a worktree whose only change is the sentinel is treated as clean. Regression tests: - `allocate_excludes_claim_sentinel_from_git_status`: a freshly allocated worktree (parent repo does NOT gitignore the sentinel) reports a clean `git status` and excludes the sentinel exactly once (idempotent). - `inspect_workspace_treats_claim_only_worktree_as_clean`: the exact #2621 repro reports `worktree_dirty == false` and empty `changed_files`. - `inspect_workspace_still_flags_real_changes_alongside_claim`: a genuine change alongside the sentinel still marks the tree dirty (no over-broad filter). Closes #2621 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wraps the three cargo regression tests behind a single run-step whose exit code gates on all three passing, matching the pinned @gadugi/agentic-test runner schema (params.command + integer-ms timeout). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mers + serialize exclude write quality-audit findings (multi-agent validated, >=2/3 agreement): 1. verify_agent_spawn_artifacts used an UNFILTERED parse_status_paths, so the .simard-engineer-claim sentinel could leak into post_status and be reported as a spurious "new changed file" (falsely flipping a no-op session to "verified") whenever the .git/info/exclude append failed. Centralized the filter into a shared strip_claim_sentinel() helper now used by BOTH inspect_workspace and verify_agent_spawn_artifacts. 2. exclude_engineer_claim did a non-atomic read-modify-write of the SHARED common-dir info/exclude outside worktree_mutation_lock; concurrent allocations against the same parent repo could clobber pre-existing exclude entries. Serialized the exclude write under the same lock guarding `git worktree add`. Tests: strip_claim_sentinel unit tests (removes only root sentinel; keeps subdir same-basename), and exclude_engineer_claim idempotency + existing-entry-preservation test. Full engineer_worktree:: + engineer_loop:: suites: 257 passed; 0 failed. clippy -D warnings clean; fmt clean; gadugi qa-scenario green. Refs #2621 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
quality-audit cycle 2 (rubber-duck, High confidence; verified via git check-ignore): the bare `.simard-engineer-claim` exclude pattern uses gitignore basename semantics and matches at ANY depth, so a genuine agent-created `subdir/.simard-engineer-claim` would be silently hidden from `git status` and dropped by both inspect_workspace and verify_agent_spawn_artifacts. Write a root-anchored `/.simard-engineer-claim` pattern instead — the sentinel is only ever at the worktree root, so anchoring loses nothing and makes the exclude semantics identical to the exact-root strip_claim_sentinel filter. Empirically verified: an anchored entry in the shared common-dir info/exclude, evaluated from a linked worktree, hides only the root sentinel. Idempotency now also recognizes a legacy bare line so it never stacks a duplicate. Test: allocate_exclude_does_not_hide_subdir_same_basename asserts a nested same-basename file still surfaces in `git status`. engineer_worktree:: + engineer_loop:: suites: 258 passed; 0 failed. clippy -D warnings + fmt clean. Refs #2621 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…2621) The #2621 fix filters the .simard-engineer-claim sentinel from BOTH git-status consumers, but only inspect_workspace had behavioral coverage. Add two tests for verify_agent_spawn_artifacts on the degraded path (sentinel present, no .git/info/exclude entry): - verify_agent_spawn_artifacts_ignores_claim_only_no_op_session: a session whose only side-effect is the sentinel must report "unverified", not a false "verified". Verified to fail if the post-status filter is removed. - verify_agent_spawn_artifacts_still_verifies_real_change_alongside_claim: a genuine new file still verifies (guards against an over-broad filter). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3c78c4e to
aeaa7e6
Compare
📊 Coverage Summary
Coverage data from CI run. Test files matching |
rysweet
added a commit
that referenced
this pull request
Jul 6, 2026
…on (#2621) (#2646) Add a reference page describing the two-layer defense merged in #2623 that keeps the private per-worktree liveness sentinel (.simard-engineer-claim) from ever registering as an uncommitted change, so the engineer-loop pre-mutation guard cannot trip on it in a target repo that does not gitignore it: - Layer 1 (creation-time): exclude_engineer_claim() appends a root-anchored /.simard-engineer-claim to the worktree info/exclude. - Layer 2 (read-time): strip_claim_sentinel() drops the exact-root sentinel from every git-status consumer before worktree_dirty is computed. Documents the constant, both layers, security properties, and the pinning regression tests. Adds the page to the mkdocs nav. Docs-only; no code change. Refs #2621. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2621. The engineer-loop pre-mutation guard (
if analyzed.is_mutating() && inspection.worktree_dirty) aborted every mutating engineer whose target repo did not gitignore Simard's private.simard-engineer-claimsentinel. Simard drops that sentinel into every engineer worktree but relied on the target repo's.gitignoreto hide it, so external governed repos (e.g.agent-kgpacks-rs) reportedworktree_dirty == trueon the untracked sentinel and the guard failed Intake before the coding agent was ever spawned — an infinite engineer-dispatch loop with zero progress (blocked agent-kgpacks-rs WS #12/#16/#17/#18/#19/#20/#21).Fix (repo-agnostic, belt-and-suspenders)
engineer_worktree::allocatenow callsexclude_engineer_claim, which appends a root-anchored/.simard-engineer-claimline to the new worktree's git exclude file (resolved viagit rev-parse --git-path info/exclude). Idempotent (exact-line match short-circuits, also matches a legacy bare entry), creates the file/parent dir if absent, serialized underworktree_mutation_lock(the shared commoninfo/excludeis a read-modify-write race across concurrent allocations), best-effort non-fatal with a WARN log.inspect_workspace— filtersENGINEER_CLAIM_FILEout of bothchanged_filesand theworktree_dirtydetermination via the sharedstrip_claim_sentinelhelper, so a worktree whose only change is the sentinel is treated as clean while genuine changes still count. The same filter is applied toverify_agent_spawn_artifacts(the post-session evidence report), so the sentinel can't be reported as a spurious "new changed file" on the degraded path where the exclude append failed.Root-anchoring keeps the exclude/filter semantics identical (exact root path only): a genuine agent-created
subdir/.simard-engineer-claimis NOT hidden.Merge-ready evidence
1. qa-team scenarios (validated + run)
tests/qa-scenarios/engineer-claim-sentinel-guard.yaml:gadugi-test validate -f tests/qa-scenarios/engineer-claim-sentinel-guard.yaml→✓ Scenario "engineer-claim-sentinel-guard" is valid(Valid files: 1, Invalid: 0).gadugi-test run -d tests/qa-scenarios -s engineer-claim-sentinel-guard→✓ Passed: 1 ✗ Failed: 0(All tests passed!). The scenario runs the three core regression tests under a singlebash -lcwrapper that exits zero only whencargo testreports3 passed.Regression tests (9 total; all pass):
engineer_loop::tests_claim_sentinel::inspect_workspace_treats_claim_only_worktree_as_clean— the exact pre-mutation guard trips on untracked .simard-engineer-claim in non-Simard repos — infinite engineer dispatch loop (blocks all agent-kgpacks-rs WS goals) #2621 repro: a worktree whose only change is.simard-engineer-claimreportsworktree_dirty == falseand emptychanged_files. Since the pre-mutation guard reduces tois_mutating() && worktree_dirty, this proves the guard passes.inspect_workspace_still_flags_real_changes_alongside_claim— a genuine change alongside the sentinel still marks the tree dirty (no over-broad filter).verify_agent_spawn_artifacts_ignores_claim_only_no_op_session— degraded path (sentinel present, no exclude entry): a no-op session is"unverified", not a false"verified". Verified to fail if the post-status filter is removed (negative control: without the filter the test fails withleft: "verified").verify_agent_spawn_artifacts_still_verifies_real_change_alongside_claim— a real new file still verifies and only the real change appears in the evidence.strip_claim_sentinel_removes_only_the_root_sentinel,strip_claim_sentinel_keeps_subdir_same_basename— shared-filter unit coverage.engineer_worktree::tests_extra::allocate_excludes_claim_sentinel_from_git_status— a freshly allocated worktree (parent repo does not gitignore the sentinel) reports a cleangit status; exclude entry present exactly once (idempotent).allocate_exclude_does_not_hide_subdir_same_basename— root-anchored exclude does not hide a same-basename file in a subdirectory.exclude_engineer_claim_is_idempotent_and_preserves_existing_entries— repeated appends don't duplicate and preserve pre-existing exclude entries.Local suite:
engineer_loop::234 passed / 0 failed,engineer_worktree::42 passed / 0 failed.2. Docs / changed surfaces
No user-facing surface change (no CLI/API/output change). Internal-only, changed surfaces:
engineer_worktree::allocatenow writes a repo-local.git/info/excludeentry (uncommitted, never pushed).inspect_workspaceandverify_agent_spawn_artifactsignore Simard's private sentinel.Justification: these are private Simard infrastructure behaviors with no external contract; the sentinel (
.simard-engineer-claim) is Simard-internal. Note (non-blocking): the exclude entry also benignly widens the daemon sweep / GC "recoverable work" probes to no longer treat the ever-present sentinel as work in non-gitignoring repos; genuine uncommitted work is still detected (root anchor), so no data-loss hazard.3. quality-audit (≥3 SEEK→VALIDATE→FIX cycles, ended clean)
todo!/unimplemented!/bail!("TODO")/panic!/stubOk(());exclude_engineer_claimusesmap_err/?throughout; allunwrap/expectare test-only).cargo fmt --all -- --checkclean;cargo clippy --all-targets --all-features --locked -- -D warningsclean. Guard-coverage analysis: the guard is exactlyis_mutating() && worktree_dirty, fully covered by the repro test. Deadlock analysis: step-4git worktree addlock is dropped at its block end before step-4b re-acquires it (std::sync::Mutexnon-reentrant, no nesting). Validated by two independentcode-reviewagents.git rev-parse --git-path info/excludereturns an absolute path into the shared common dir for linked worktrees and a cwd-relative path in normal repos (both handled by theis_absolute()branch +joinfallback); a root-anchored entry in the shared exclude hides only each worktree-root sentinel (asubdir/same-basename file still shows??, main repo unaffected); idempotency handles missing file / no-trailing-newline / legacy bare line;strip_claim_sentinelis applied identically to bothgit statusconsumers socount_new_status_paths(after \ before) cannot reintroduce the sentinel. Zero critical/high/medium correctness or security findings.verify_agent_spawn_artifacts's sentinel filter had no behavioral test (a regression deleting it would compile and pass every other test) — was fixed by adding the twoverify_agent_spawn_artifacts_*tests above, proven to catch the regression via a negative control (removed the filter → test failed with the exact false-verifiedbehavior → restored). Final re-scan: stub scan clean, fmt clean, clippy-D warningsclean,engineer_loop::234 passed.Final cycle clean: zero critical/high; zero medium correctness/security findings. The single medium coverage finding was fixed within the audit.
4. CI
CI is 100% green on the current PR head
aeaa7e62764c638ca0187be5e28050cbe17cc80e— all 16 required checks concludedSUCCESS, 0 failing, 0 pending. Confirmed green workflow runs:verify— SUCCESS: https://github.com/rysweet/Simard/actions/runs/28761097315 and https://github.com/rysweet/Simard/actions/runs/28761096366 (jobs:pre-commit,cargo-audit,cargo-deny,cargo-vet,npm-audit,install-real,e2e-dashboard).coverage— SUCCESS: https://github.com/rysweet/Simard/actions/runs/28761097304gh pr checks 2623rollup: 15 pass + 1 security pass, 0 fail/pending.mergeStateStatus: CLEAN,mergeable: MERGEABLE. Local pre-push gates (mirror CIverify.yml) were also all green on this head:cargo fmt --all -- --check,cargo test --release --lib (cognitive_memory|bootstrap|memory_ipc|memory_consolidation),cargo clippy --all-targets --all-features --locked -- -D warnings. Branch is rebased onto latestorigin/main.5. Verdict — READY TO MERGE
This PR is ready to merge. Rationale: all six merge-ready criteria have substantive, concrete evidence tied to the decision — (1) qa-team scenario
engineer-claim-sentinel-guard.yamlvalidated (✓ Scenario … is valid) and run (✓ Passed: 1 ✗ Failed: 0), backed by 9 named regression tests all passing including the exact #2621 reproinspect_workspace_treats_claim_only_worktree_as_clean; (2) changed surfaces are enumerated with internal-only justification (no user-facing contract change — the sentinel is Simard-private infra); (3) three explicit SEEK→VALIDATE→FIX quality-audit cycles ending clean, with a negative-control proof that theverify_agent_spawn_artifactsfilter test catches its own regression; (4) CI 100% green on headaeaa7e62with the run links above; (6) the diff is focused on exactly 5 files scoped to the fix with no unrelated edits. The change is a targeted, root-anchored filename filter over git's stable porcelaingit status --shortoutput plus a best-effort, non-fatal.git/info/excludeappend — low blast radius, no cognition/memory-consolidation logic touched, no brittle model-output parsing. It resolves the P1 infinite engineer-dispatch loop blocking the agent-kgpacks-rs WS cascade. No blocking findings remain.6. Diff focused
Scoped to the fix:
engineer_loop/mod.rs(sharedstrip_claim_sentinelfilter + both consumers + test-module registration),engineer_loop/tests_claim_sentinel.rs,engineer_worktree/mod.rs(exclude_engineer_claimhelper + call site),engineer_worktree/tests_extra.rs, one qa-scenario. No unrelated edits.Closes #2621