Repository navigation
fix: refuse duplicate briefs before spawning workers - #283
Merged
Merged
Conversation
A brief filled with another task's stale Task or acceptance text launched a worker against the wrong work. A fresh ship or scout spawn now compares its normalized Task section and acceptance-criteria block with live and recently written briefs in this home and refuses an exact match, naming the other task. --allow-duplicate-brief <other-task-id> permits deliberate reuse; --relaunch reuses its own brief and skips the check.
dnth
added a commit
that referenced
this pull request
Oct 29, 2026
* fix(panes): close a finished task's pane on its delivery receipt Finished brief and scout panes accumulated because pane closure only ran at teardown, so work that ended in a delivery receipt kept its pane open forever. deliver now hands its newest covered status endpoint and status identity to the new bin/fm-pane-autoclose.sh, which owns one gate: the covered line is the live standing done: claim with the artifact its kind/mode requires, no keyed decision, blocker, linked external request, routed reply, custom check, public commitment, or held board row remains, and the recorded endpoint resolves from ordinary task metadata alone. A failed:, paused:, or unkeyed decision receipt keeps the pane, and so does a later status event, a rotated status identity, an already-closed endpoint, or a secondmate home. The close is teardown's guarded exact-pane path: the named session's presentation lock, then the focus-preserving pane close that refuses the captain's active tab and never calls workspace close. It closes the pane view only: the Herdr session, its other panes, transcripts, and every durable task record stay intact. Coverage: tests/fm-pane-autoclose.test.sh pins sixteen gate decisions against a stateful fake herdr, including a rotated status identity that must not close a replacement pane; tests/fm-pane-autoclose-herdr-e2e.test.sh drives a real guarded lab session for close-after-receipt with the board row still In flight, every keep exclusion, the active-tab refusal, transcript survival, and the ambiguous-endpoint refusal. * fix(panes): treat a removed status record as nothing to close A receipt recorded after teardown removed the task record has no live claim left to judge, so the walk keeps silently instead of reporting a refusal on a normal reconciliation path. An unsafe status path still refuses. * no-mistakes(review): Revalidate autoclose gates and strengthen pane lifecycle regressions * fix(panes): close a finished task's pane on its delivery receipt Finished brief and scout panes accumulated because pane closure only ran at teardown, so work that ended in a delivery receipt kept its pane open forever. deliver now hands its newest covered status endpoint and status identity to the new bin/fm-pane-autoclose.sh, which owns one gate: the covered line is the live standing done: claim with the artifact its kind/mode requires, no keyed decision, blocker, linked external request, routed reply, custom check, public commitment, or held board row remains, and the recorded endpoint resolves from ordinary task metadata alone. A failed:, paused:, or unkeyed decision receipt keeps the pane, and so does a later status event, a rotated status identity, an already-closed endpoint, or a secondmate home. The close is teardown's guarded exact-pane path: the named session's presentation lock, then the focus-preserving pane close that refuses the captain's active tab and never calls workspace close. It closes the pane view only: the Herdr session, its other panes, transcripts, and every durable task record stay intact. Coverage: tests/fm-pane-autoclose.test.sh pins sixteen gate decisions against a stateful fake herdr, including a rotated status identity that must not close a replacement pane; tests/fm-pane-autoclose-herdr-e2e.test.sh drives a real guarded lab session for close-after-receipt with the board row still In flight, every keep exclusion, the active-tab refusal, transcript survival, and the ambiguous-endpoint refusal. * fix(panes): treat a removed status record as nothing to close A receipt recorded after teardown removed the task record has no live claim left to judge, so the walk keeps silently instead of reporting a refusal on a normal reconciliation path. An unsafe status path still refuses. * no-mistakes(review): Revalidate autoclose gates and strengthen pane lifecycle regressions * no-mistakes(review): Fix stale receipt and renewed hold autoclose gates * no-mistakes(review): Guard delegated public promises before closing task panes * no-mistakes(document): Clarify delivery-triggered pane closure documentation * test(panes): lab-cover the delegated promise and annotate output globals The lint gate flagged the review-1 output globals as unused; annotate them the way the rest of the repo marks sourcing-caller outputs. Add the guarded-lab case the supervisor required: a promise registered in the primary home for secondmate:alice keeps the secondmate task's pane, proven against real Herdr and real tasks-axi. * test(panes): mark fixture-global SC2031 false positive The suite's setup_home assigns fixture globals in the parent shell while the backgrounded and substituted helpers only read them, so pin that intent once at the file head rather than fifteen per-line directives. * no-mistakes(document): Correct public followup library ownership documentation * fix: refuse duplicate briefs before spawning workers (#283) * fix(spawn): refuse a fresh brief that duplicates another task's content A brief filled with another task's stale Task or acceptance text launched a worker against the wrong work. A fresh ship or scout spawn now compares its normalized Task section and acceptance-criteria block with live and recently written briefs in this home and refuses an exact match, naming the other task. --allow-duplicate-brief <other-task-id> permits deliberate reuse; --relaunch reuses its own brief and skips the check. * no-mistakes(review): Forward duplicate brief overrides through batch spawn dispatch * no-mistakes(document): Clarify duplicate brief comparison help * no-mistakes(lint): Fix SC2181 status checks in task delivery tests * test(panes): assert the persisted-history contract Herdr actually provides The previous assertion required Herdr to retain a closed pane in session-history.json. Measured on 0.9.3 that is false: the file is written on a periodic flush, a close leaves the existing text intact, and the next flush rewrites it with live panes only, so a closed pane leaves the persisted history. The test now proves what closure does and does not touch: it waits for Herdr's own capture of both the target and a sibling pane before the close, asserts both survive the close immediately, then waits for one flush and asserts the surviving pane's history persists and the session is still running. It does not pin when a closed pane's text disappears. * fix(panes): close a finished task's pane on its delivery receipt Finished brief and scout panes accumulated because pane closure only ran at teardown, so work that ended in a delivery receipt kept its pane open forever. deliver now hands its newest covered status endpoint and status identity to the new bin/fm-pane-autoclose.sh, which owns one gate: the covered line is the live standing done: claim with the artifact its kind/mode requires, no keyed decision, blocker, linked external request, routed reply, custom check, public commitment, or held board row remains, and the recorded endpoint resolves from ordinary task metadata alone. A failed:, paused:, or unkeyed decision receipt keeps the pane, and so does a later status event, a rotated status identity, an already-closed endpoint, or a secondmate home. The close is teardown's guarded exact-pane path: the named session's presentation lock, then the focus-preserving pane close that refuses the captain's active tab and never calls workspace close. It closes the pane view only: the Herdr session, its other panes, transcripts, and every durable task record stay intact. Coverage: tests/fm-pane-autoclose.test.sh pins sixteen gate decisions against a stateful fake herdr, including a rotated status identity that must not close a replacement pane; tests/fm-pane-autoclose-herdr-e2e.test.sh drives a real guarded lab session for close-after-receipt with the board row still In flight, every keep exclusion, the active-tab refusal, transcript survival, and the ambiguous-endpoint refusal. * fix(panes): treat a removed status record as nothing to close A receipt recorded after teardown removed the task record has no live claim left to judge, so the walk keeps silently instead of reporting a refusal on a normal reconciliation path. An unsafe status path still refuses. * no-mistakes(review): Revalidate autoclose gates and strengthen pane lifecycle regressions * no-mistakes(review): Fix stale receipt and renewed hold autoclose gates * no-mistakes(review): Guard delegated public promises before closing task panes * no-mistakes(document): Clarify delivery-triggered pane closure documentation * test(panes): lab-cover the delegated promise and annotate output globals The lint gate flagged the review-1 output globals as unused; annotate them the way the rest of the repo marks sourcing-caller outputs. Add the guarded-lab case the supervisor required: a promise registered in the primary home for secondmate:alice keeps the secondmate task's pane, proven against real Herdr and real tasks-axi. * test(panes): mark fixture-global SC2031 false positive The suite's setup_home assigns fixture globals in the parent shell while the backgrounded and substituted helpers only read them, so pin that intent once at the file head rather than fifteen per-line directives. * fix(panes): close a finished task's pane on its delivery receipt Finished brief and scout panes accumulated because pane closure only ran at teardown, so work that ended in a delivery receipt kept its pane open forever. deliver now hands its newest covered status endpoint and status identity to the new bin/fm-pane-autoclose.sh, which owns one gate: the covered line is the live standing done: claim with the artifact its kind/mode requires, no keyed decision, blocker, linked external request, routed reply, custom check, public commitment, or held board row remains, and the recorded endpoint resolves from ordinary task metadata alone. A failed:, paused:, or unkeyed decision receipt keeps the pane, and so does a later status event, a rotated status identity, an already-closed endpoint, or a secondmate home. The close is teardown's guarded exact-pane path: the named session's presentation lock, then the focus-preserving pane close that refuses the captain's active tab and never calls workspace close. It closes the pane view only: the Herdr session, its other panes, transcripts, and every durable task record stay intact. Coverage: tests/fm-pane-autoclose.test.sh pins sixteen gate decisions against a stateful fake herdr, including a rotated status identity that must not close a replacement pane; tests/fm-pane-autoclose-herdr-e2e.test.sh drives a real guarded lab session for close-after-receipt with the board row still In flight, every keep exclusion, the active-tab refusal, transcript survival, and the ambiguous-endpoint refusal. * fix(panes): treat a removed status record as nothing to close A receipt recorded after teardown removed the task record has no live claim left to judge, so the walk keeps silently instead of reporting a refusal on a normal reconciliation path. An unsafe status path still refuses. * no-mistakes(review): Revalidate autoclose gates and strengthen pane lifecycle regressions * no-mistakes(document): Correct public followup library ownership documentation * test(panes): assert the persisted-history contract Herdr actually provides The previous assertion required Herdr to retain a closed pane in session-history.json. Measured on 0.9.3 that is false: the file is written on a periodic flush, a close leaves the existing text intact, and the next flush rewrites it with live panes only, so a closed pane leaves the persisted history. The test now proves what closure does and does not touch: it waits for Herdr's own capture of both the target and a sibling pane before the close, asserts both survive the close immediately, then waits for one flush and asserts the surviving pane's history persists and the session is still running. It does not pin when a closed pane's text disappears. * no-mistakes(review): Consult captain-call inventory only when a task records one * no-mistakes(review): Add lab ship PR close case, drop artifact knob * no-mistakes(document): Note delegated-promise case in Herdr lab header
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
A brief filled with another task's stale Task and acceptance text sent a worker to the wrong repo.
fm-spawnchecked only for leftover placeholders, so it launched anyway.--allow-duplicate-brief <other-task-id>covers only the task it names. It is forwarded through batch spawns and refused on--relaunchand--secondmate.--help(the script header) documents the refusal and the override.tests/fm-task-delivery.test.shfixture helper now gives each brief unique text, so sibling fixtures don't trip the guard.Evidence
fm-spawn Bwith task A's Task section launched B with no warning.After:
error: ship brief for dup-task-bravo has the same Task section as task dup-task-alpha (...); refill the brief, or pass --allow-duplicate-brief dup-task-alpha if the reuse is deliberate, ran intests/fm-task-delivery.test.sh(test_spawn_refuses_a_duplicate_brief,test_spawn_duplicate_brief_scope, plus batch cases).tests/fm-spawn-relaunch-dead-endpoint.test.shtest_relaunch_skips_duplicate_brief_guardrelaunches while a live sibling has an identical brief. Mutation check, ran: enabling the guard on relaunch makes that test fail.Stock macOS Bash snapshot compatibilityjob was cancelled without a verdict and approved past at the CI gate. Reason, cited from firstmate's decision: it is a pre-existing CI timeout that is not caused by this PR. The same job was cancelled at its 15-minute limit onmaintwice (run 40701512292 and its rerun), and a rerun would cancel again. It is tracked separately asfm-ci-macos-bash-timeout.fm-spawn(captain-hold-lifecycle, pr-merge, composer-lib, boat-*, and others). The checked ones fail identically on an unmodifiedmainexport.Merge Danger
Door: two-way
Reverting the commit removes the guard. No data or state format changes.
Blast Radius: spawn
A false match blocks a fresh spawn until the brief is refilled or the override names the other task. Relaunches and secondmates are unaffected.
Testing
Both targeted regression scripts and all live CLI scenarios passed using real tmux, Treehouse, an isolated home, and a local Git origin. Setup issues were corrected and re-driven; CLI transcripts and persisted state were captured. Full-suite and lint checks were left to their owning phases.
Evidence: Live CLI transcript
Evidence: Actual tmux endpoints and persisted task metadata
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/fm-spawn.sh:485- Batch dispatch silently drops the override. With B duplicating A,fm-spawn.sh B=repo --scout --allow-duplicate-brief Aconsumes the flag here, but shared_args (bin/fm-spawn.sh:1541-1553) never forwards it. Both scout (1564) and ship (1566) child invocations therefore refuse deliberate reuse at 3436. Forward the named override through shared_args so both paths honor it.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
TMPDIR="$PWD/.test-disposable/tmp" bash tests/fm-task-delivery.test.sh— passed.TMPDIR="$PWD/.test-disposable/tmp" bash tests/fm-spawn-relaunch-dead-endpoint.test.sh— passed.python3 .test-disposable/live.py— exercised real duplicate refusal and successful named override.python3 .test-disposable/more.pyfollowed by correctedpython3 .test-disposable/resume.py— exercised boundaries, relaunch, batches, flag validation, and help.python3 .test-disposable/extra.py— exercised multiline criteria overrides and empty sections.Captured real tmux endpoints and task metadata, stopped the isolated server, removed disposable resources, and confirmed cleangit status --short.✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.