Skip to content

fix: refuse duplicate briefs before spawning workers - #283

Merged
dnth merged 4 commits into
mainfrom
fm/fm-spawn-refuse-duplicate-brief
Oct 29, 2026
Merged

dnth merged 4 commits into
mainfrom
fm/fm-spawn-refuse-duplicate-brief

Conversation

@dnth

@dnth dnth commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

A brief filled with another task's stale Task and acceptance text sent a worker to the wrong repo. fm-spawn checked only for leftover placeholders, so it launched anyway.

fm-spawn <id> (fresh ship/scout; skipped on --relaunch)
  placeholder + delivery-contract checks        (unchanged)
  duplicate-brief guard                          (new)
    own  = normalize(# Task), normalize(AC lines)   # collapse whitespace, drop words containing <id>
    for other in data/*/brief.md where state/<other>.meta exists or mtime < 7 days
      if own.task == other.task or own.criteria == other.criteria
        other == --allow-duplicate-brief ? notice : refuse "same <section> as task <other> ... --allow-duplicate-brief <other>"
  endpoint creation                              (unchanged)
  • --allow-duplicate-brief <other-task-id> covers only the task it names. It is forwarded through batch spawns and refused on --relaunch and --secondmate.
  • --help (the script header) documents the refusal and the override.
  • The tests/fm-task-delivery.test.sh fixture helper now gives each brief unique text, so sibling fixtures don't trip the guard.

Evidence

  • Before: fm-spawn B with 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 in tests/fm-task-delivery.test.sh (test_spawn_refuses_a_duplicate_brief, test_spawn_duplicate_brief_scope, plus batch cases).
  • Relaunch: tests/fm-spawn-relaunch-dead-endpoint.test.sh test_relaunch_skips_duplicate_brief_guard relaunches while a live sibling has an identical brief. Mutation check, ran: enabling the guard on relaunch makes that test fail.
  • CI: the Stock macOS Bash snapshot compatibility job 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 on main twice (run 40701512292 and its rerun), and a rerun would cancel again. It is tracked separately as fm-ci-macos-bash-timeout.
  • Local full suite, ran: failures in scripts that never invoke fm-spawn (captain-hold-lifecycle, pr-merge, composer-lib, boat-*, and others). The checked ones fail identically on an unmodified main export.

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.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Spawn a copied Task section with changed whitespace and task-specific paths; refusal names the donor and override before metadata exists. ✅ pass live Live CLI transcript: normalized task duplicate refused
Copy the complete acceptance block with different Task text; exact copies refuse while partial overlap launches. ✅ pass live Live CLI transcript: criteria-only duplicate refused; full multiline criteria duplicate refused; partial criteria overlap launches
Override a specifically named donor; Task and criteria reuse launch, while wrong names and additional matching donors still refuse. ✅ pass live Live CLI transcript: named override launches; criteria named override launches; wrong override refused; override cannot mask second donor
Spawn distinct briefs and briefs with empty normalized sections; all launch without false duplicate matches. ✅ pass live Live CLI transcript: distinct brief launches; empty normalized task checks; empty sections scout blankone and blanktwo
Compare recent briefs without metadata and old briefs with live records; ignore old inactive briefs. ✅ pass live Live CLI transcript: initial recent alpha donor; old inactive brief ignored; old live donor specifically refused
Recreate an actual task endpoint using its unchanged brief while another task has matching content; relaunch succeeds. ✅ pass live Live CLI transcript: relaunch unchanged brief with duplicate donor
Dispatch ship and scout batches; duplicates refuse and named overrides reach child spawns using separate and equals flag syntax. ✅ pass live Live CLI transcript: batch ship and scout duplicate refusals and named override launches
Supply the override with relaunch, secondmate, or no value; each invocation refuses with an explanatory error. ✅ pass live Live CLI transcript: override relaunch refused; override secondmate refused; override missing value refused
Run --help and observe documentation of duplicate refusal and the named override. ✅ pass live Live CLI transcript: public help
Evidence: Live CLI transcript
# Live spawn validation

Real fm-spawn CLI, real tmux and Treehouse, disposable local Git origin and home; worker command /bin/sleep 180.

## normalized task duplicate refused

`` `text
$ fm-spawn.sh bravo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=1
notice: bravo ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
error: ship brief for bravo has the same Task section as task alpha (~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/home/data/alpha/brief.md); refill the brief, or pass --allow-duplicate-brief alpha if the reuse is deliberate
`` `

## wrong override refused

`` `text
$ fm-spawn.sh bravo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off --allow-duplicate-brief charlie
exit=1
notice: bravo ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
error: ship brief for bravo has the same Task section as task alpha (~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/home/data/alpha/brief.md); refill the brief, or pass --allow-duplicate-brief alpha if the reuse is deliberate
`` `

## named override launches

`` `text
$ fm-spawn.sh bravo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off --allow-duplicate-brief alpha
exit=0
notice: bravo ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
notice: bravo reuses the Task section of task alpha, allowed by --allow-duplicate-brief alpha
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project' to release it.
spawned bravo harness=sleep kind=ship mode=local-only yolo=off window=firstmate:fm-bravo worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project
`` `

## distinct brief launches

`` `text
$ fm-spawn.sh distinct ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=0
notice: distinct ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/2/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/2/project' to release it.
spawned distinct harness=sleep kind=ship mode=local-only yolo=off window=firstmate:fm-distinct worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/2/project
`` `

## criteria-only duplicate refused

`` `text
$ fm-spawn.sh criteria ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=1
notice: criteria ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
error: ship brief for criteria has the same acceptance criteria as task alpha (~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/home/data/alpha/brief.md); refill the brief, or pass --allow-duplicate-brief alpha if the reuse is deliberate
`` `

## partial criteria overlap launches

`` `text
$ fm-spawn.sh partial ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=0
notice: partial ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/3/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/3/project' to release it.
spawned partial harness=sleep kind=ship mode=local-only yolo=off window=firstmate:fm-partial worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/3/project
`` `

## relaunch unchanged brief with duplicate donor

`` `text
$ fm-spawn.sh bravo --relaunch --harness /bin/sleep 180
exit=1
error: raw launch project directory cannot be resolved: 
`` `

## relaunch unchanged brief with duplicate donor

`` `text
$ fm-spawn.sh bravo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project --relaunch --harness /bin/sleep 180
exit=0
notice: bravo ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
spawned bravo harness=sleep kind=ship mode=local-only yolo=off window=firstmate:fm-bravo worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project
`` `

## override cannot mask second donor

`` `text
$ fm-spawn.sh delta ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off --allow-duplicate-brief alpha
exit=1
notice: delta ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
notice: delta reuses the Task section of task alpha, allowed by --allow-duplicate-brief alpha
error: ship brief for delta has the same Task section as task bravo (~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/home/data/bravo/brief.md); refill the brief, or pass --allow-duplicate-brief bravo if the reuse is deliberate
`` `

## old inactive brief ignored

`` `text
$ fm-spawn.sh new ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=0
notice: new ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22c

... [26735 bytes truncated] ...

h incarnation bound to the task's metadata
    __PIEXT__    absolute path to state/<task-id>.pi-ext.ts (pi turn-end extension,
                 written by this script; outside the worktree to avoid pi's trust gate)
    __OMPEXT__   absolute path to state/<task-id>.omp-ext.ts (OMP turn-start
                 acknowledgement, programmatic inbox doorbell, and turn-end
                 extension, also outside the worktree)
    __OMPSESSIONDIR__ task-local or secondmate-home OMP session directory for exact resume
    __OMPRESUMEFLAG__ empty for a fresh OMP launch or the exact retained secondmate session file
    __OMPPRIMARY__ absolute path to .omp/extensions/fm-primary-omp.ts in an OMP secondmate home
    __OMPMESSAGE__ OMP-only initial positional message: the encoded launch brief, or the `orchestrate` keyword plus the brief path when the brief opts in
    __PITURNEND__ absolute path to .pi/extensions/fm-primary-turnend-guard.ts in a pi secondmate home
    __PIWATCH__   absolute path to .pi/extensions/fm-primary-pi-watch.ts in a pi secondmate home
    __HERMESBIN__ absolute resolved Hermes executable (PATH first, then $HOME/.local/bin/hermes)
    __OPINPUT__   absolute path to the canonical operational-input encoder
Hermes TUI launches additionally inherit the task-unique lifecycle token as FM_HERMES_TASK_TOKEN.
Verified per-harness turn-end hooks are installed automatically where enabled; some live outside the worktree.
Kimi uses one surgically installed Firstmate region in $HOME/.kimi-code/config.toml,
a firstmate-owned global hook and registry, and a gitignored per-task pointer.
Hermes uses surgically installed entries in the active profile's config.yaml,
a firstmate-owned global hook and registry, and a gitignored per-task pointer.
grok uses a firstmate-owned global hook under ${GROK_HOME:-$HOME/.grok}/hooks
plus a gitignored .fm-grok-turnend worktree pointer and a state token.
When these registries are enabled, state/<id>.meta records grok_turnend_dir= and kimi_turnend_dir= so teardown removes tokens through their owning directories.
Publishing the record and moving this home's backlog item to In flight are one
step, not two: bin/fm-backlog-transition-lib.sh owns that invariant, and this
script performs the transition under the task's own meta lock before it reports
success. A ship or scout dispatch therefore REFUSES up front, before any
endpoint, worktree, or record exists, unless the home's backlog has an
unheld, unblocked Queued or In flight item for the id; a transition that fails
after publication removes the record it just wrote rather than leaving a
worker the backlog does not own. A relaunch re-reads the row instead of
re-running the transition, so an eligible In-flight item is left untouched.
The transition is skipped entirely for --secondmate spawns (persistent agents
are not work items), on a config/backlog-backend=manual home, and in a
markdown home that keeps no data/backlog.md. A configured non-markdown
adapter remains active without a markdown file; any active automatic backend
without compatible tasks-axi refuses before creating lifecycle state.
On success prints: spawned <id> harness=<name> kind=<ship|scout|secondmate> [mode=<mode> yolo=<on|off>] window=<backend-target> worktree=<path>
A ship task records the explicit mode/yolo it was passed; a secondmate spawn records
mode=secondmate, yolo=off, home=, and projects=; a scout records neither, and both the
success line and state/<id>.meta omit them.
When the home session's frozen trace-context decision is enabled (see
docs/configuration.md and bin/fm-trace-context-lib.sh), the meta also records
one W3C traceparent= carrier, the same value injected into the pane as
TRACEPARENT; the default-off path writes neither, leaving the generated meta
and launch environment unchanged.
  --traceparent <carrier> delivers a carrier that a REMOTE parent already
  resolved and will record, instead of resolving one from this home's frozen
  decision. It is accepted only for --secondmate spawns, only as a strictly
  validated W3C traceparent, and exists because a remote secondmate's task
  identity is owned by the parent home that holds its task metadata, while the
  pane export happens on the remote host (bin/fm-remote-secondmate-control.sh).
  Local spawns never pass it and resolve their own carrier exactly as before.
`` `

## full multiline criteria duplicate refused

`` `text
$ fm-spawn.sh criteria-copy ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off
exit=1
notice: criteria-copy ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
error: ship brief for criteria-copy has the same acceptance criteria as task criteria-donor (~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/home/data/criteria-donor/brief.md); refill the brief, or pass --allow-duplicate-brief criteria-donor if the reuse is deliberate
`` `

## criteria named override launches

`` `text
$ fm-spawn.sh criteria-copy ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --mode local-only --yolo off --allow-duplicate-brief criteria-donor
exit=0
notice: criteria-copy ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
notice: criteria-copy reuses the acceptance criteria of task criteria-donor, allowed by --allow-duplicate-brief criteria-donor
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/9/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/9/project' to release it.
spawned criteria-copy harness=sleep kind=ship mode=local-only yolo=off window=firstmate:fm-criteria-copy worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/9/project
`` `

## empty sections scout blankone

`` `text
$ fm-spawn.sh blankone ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --scout
exit=0
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/10/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/10/project' to release it.
spawned blankone harness=sleep kind=scout window=firstmate:fm-blankone worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/10/project
`` `

## empty sections scout blanktwo

`` `text
$ fm-spawn.sh blanktwo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project /bin/sleep 180 --scout
exit=0
A new version of treehouse is available: v3.1.1 → v3.1.7
Run "treehouse update" to update

🌳 Setting up worktree...
🌳 Leased worktree at ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/11/project. Run 'treehouse return ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/11/project' to release it.
spawned blanktwo harness=sleep kind=scout window=firstmate:fm-blanktwo worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/11/project
`` `
Evidence: Actual tmux endpoints and persisted task metadata
bash ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20 bash
fm-bravo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project sleep
fm-distinct ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/2/project sleep
fm-partial ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/3/project sleep
fm-new ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/4/project sleep
fm-emptyone ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/5/project sleep
fm-emptytwo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/6/project sleep
fm-child-ship ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/7/project sleep
fm-child-scout ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/8/project sleep
fm-criteria-copy ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/9/project sleep
fm-blankone ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/10/project sleep
fm-blanktwo ~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/11/project sleep


child-ship.meta:
window=firstmate:fm-child-ship
endpoint_task_id=child-ship
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/7/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-child-ship
model=default
effort=default
spawn_gen=s1793188583.4146618.6448

emptyone.meta:
window=firstmate:fm-emptyone
endpoint_task_id=emptyone
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/5/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-emptyone
model=default
effort=default
spawn_gen=s1793188562.4049211.11015

distinct.meta:
window=firstmate:fm-distinct
endpoint_task_id=distinct
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/2/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-distinct
model=default
effort=default
spawn_gen=s1793188496.3746165.28904

blankone.meta:
window=firstmate:fm-blankone
endpoint_task_id=blankone
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/10/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=scout
tasktmp=/tmp/fm-blankone
model=default
effort=default
spawn_gen=s1793188627.154937.7612

partial.meta:
window=firstmate:fm-partial
endpoint_task_id=partial
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/3/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-partial
model=default
effort=default
spawn_gen=s1793188508.3805642.438

bravo.meta:
window=firstmate:fm-bravo
endpoint_task_id=bravo
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/1/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-bravo
model=default
effort=default
spawn_gen=s1793188535.3925156.3965

emptytwo.meta:
window=firstmate:fm-emptytwo
endpoint_task_id=emptytwo
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/6/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-emptytwo
model=default
effort=default
spawn_gen=s1793188569.4080278.8421

blanktwo.meta:
window=firstmate:fm-blanktwo
endpoint_task_id=blanktwo
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/11/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=scout
tasktmp=/tmp/fm-blanktwo
model=default
effort=default
spawn_gen=s1793188635.188118.23972

old.meta:
kind=ship

criteria-copy.meta:
window=firstmate:fm-criteria-copy
endpoint_task_id=criteria-copy
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/9/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-criteria-copy
model=default
effort=default
spawn_gen=s1793188620.120506.1929

new.meta:
window=firstmate:fm-new
endpoint_task_id=new
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/4/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=ship
mode=local-only
yolo=off
tasktmp=/tmp/fm-new
model=default
effort=default
spawn_gen=s1793188546.3976917.1655

child-scout.meta:
window=firstmate:fm-child-scout
endpoint_task_id=child-scout
worktree=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/pool/.treehouse/project-87be89/8/project
project=~/.no-mistakes/worktrees/dd71c22cc6d7/01M6157V236DKNER1SJ2F5FT20/.test-disposable/project
harness=sleep
kind=scout
tasktmp=/tmp/fm-child-scout
model=default
effort=default
spawn_gen=s1793188598.21987.14640

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 A consumes 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.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Spawn a copied Task section with changed whitespace and task-specific paths; refusal names the donor and override before metadata exists. ✅ pass live Live CLI transcript: normalized task duplicate refused
Copy the complete acceptance block with different Task text; exact copies refuse while partial overlap launches. ✅ pass live Live CLI transcript: criteria-only duplicate refused; full multiline criteria duplicate refused; partial criteria overlap launches
Override a specifically named donor; Task and criteria reuse launch, while wrong names and additional matching donors still refuse. ✅ pass live Live CLI transcript: named override launches; criteria named override launches; wrong override refused; override cannot mask second donor
Spawn distinct briefs and briefs with empty normalized sections; all launch without false duplicate matches. ✅ pass live Live CLI transcript: distinct brief launches; empty normalized task checks; empty sections scout blankone and blanktwo
Compare recent briefs without metadata and old briefs with live records; ignore old inactive briefs. ✅ pass live Live CLI transcript: initial recent alpha donor; old inactive brief ignored; old live donor specifically refused
Recreate an actual task endpoint using its unchanged brief while another task has matching content; relaunch succeeds. ✅ pass live Live CLI transcript: relaunch unchanged brief with duplicate donor
Dispatch ship and scout batches; duplicates refuse and named overrides reach child spawns using separate and equals flag syntax. ✅ pass live Live CLI transcript: batch ship and scout duplicate refusals and named override launches
Supply the override with relaunch, secondmate, or no value; each invocation refuses with an explanatory error. ✅ pass live Live CLI transcript: override relaunch refused; override secondmate refused; override missing value refused
Run --help and observe documentation of duplicate refusal and the named override. ✅ pass live Live CLI transcript: public help
  • TMPDIR=&#34;$PWD/.test-disposable/tmp&#34; bash tests/fm-task-delivery.test.sh — passed.
  • TMPDIR=&#34;$PWD/.test-disposable/tmp&#34; 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.py followed by corrected python3 .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 clean git status --short.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

dnth added 4 commits October 28, 2026 19:01
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
dnth merged commit 722ac88 into main Oct 29, 2026
35 of 36 checks passed
@dnth
dnth deleted the fm/fm-spawn-refuse-duplicate-brief branch October 29, 2026 06:39
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant