feat(bin): detect duplicate work at spawn intake (CFVC-02) - #1979
Open
sbracewell64 wants to merge 5 commits into
Open
feat(bin): detect duplicate work at spawn intake (CFVC-02)#1979sbracewell64 wants to merge 5 commits into
sbracewell64 wants to merge 5 commits into
Conversation
sbracewell64
force-pushed
the
fm/cfvc-02-duplicate-work-intake
branch
from
August 9, 2026 00:12
dbd4cfe to
d622db9
Compare
fm-spawn already refused a duplicate LAUNCH - the same task id twice - and nothing compared a new task's SUBJECT against work already open. That gap was paid for once already: one fix, two tasks, two branches, two workers, and an open pull request that already carried the change, with nothing detecting it. Every ship and scout spawn now emits the candidate overlap set before any endpoint exists: open backlog tasks, tasks holding live runtime metadata, local and remote branches, and open pull request titles. Code emits candidates and never decides equivalence; whether two prose descriptions are the same work stays firstmate's judgment, made from a surfaced set instead of from memory. A candidate must clear a shared-token floor AND cover half of the smaller of the two vocabularies. Both halves are load-bearing: measured against a real 140-item backlog, 254 branches, and 95 open pull requests, the floor alone surfaced 61 candidates for one task, while denominating by the subject alone would put short branch names permanently out of reach. Candidates are ordered strongest evidence first, never bounded, so nothing is hidden from the reader or the gate. The empty-set law binds: a source that cannot be read yields overlap=unavailable, never overlap=none. The result is recorded in the task's own metadata, so "nothing was compared" and "compared, found nothing" stay distinguishable afterwards. Enforcement follows the additive-inert pattern. config/spawn-overlap absent or advisory prints and records without refusing; enforce additionally requires --overlap-ack to name every surfaced ref; off skips the scan and says so. Any other value refuses, because a safety knob that cannot be read must never be treated as an absent one. The pull request source goes through one new shared reader in fm-pr-lib.sh rather than a second forge reader with its own failure semantics. Tests replay the recorded incident and are red against the previous build by construction, with negative controls for the unrelated task, the unreachable forge, the absent forge CLI, the coincidental match, and the ordering.
…ce failure handling
…and ratio denominator
sbracewell64
force-pushed
the
fm/cfvc-02-duplicate-work-intake
branch
from
August 9, 2026 01:00
d622db9 to
5bc2bb2
Compare
4 tasks
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.
Intent
CFVC-02: detect duplicate work at intake, per data/cfvc-synthesis-and-plan/report.md section 4 (the CFVC-02 table there is the authoritative spec).
GOAL: a task cannot be dispatched onto work that an open task, a live branch, or an open PR already covers, without the overlap being surfaced first. Measured cost of the gap: two tasks, two branches, two workers, one fix, nothing detected - a recorded live incident.
OWNER, as the spec fixes it: bin/fm-spawn.sh, the existing last gate before dispatch - no new component. Also bin/fm-pr-lib.sh, which gains the shared open-pull-request title listing so the pull request source is not a second forge reader with its own failure semantics. CODE emits the candidate overlap set - open tasks sharing id/title/body tokens, local and remote branches, open PR titles - and dispatch requires the overlap to be acknowledged. Code NEVER decides equivalence; 'is this the same work' compares two prose descriptions and stays firstmate's judgment.
BINDING REQUIREMENTS FROM THE SPEC:
HOW REQUIREMENTS 1 AND 4c WERE RECONCILED - deliberate, not an oversight. The scan ALWAYS runs, always emits, is never silent, and config/spawn-overlap selects the posture. Absent or 'advisory' (default) prints and records but never refuses - the additive-inert first period, existing spawns behave as before. 'enforce' additionally requires --overlap-ack to name every surfaced ref, which is criterion (c). 'off' skips the scan and says so. Any other value, and a present-but-unreadable file, REFUSE, because a broken safety knob must never read as an absent one.
DELIBERATE DESIGN DECISION THE DIFF ALONE WILL NOT EXPLAIN - the matching rule. A bare shared-token floor was implemented first and then MEASURED against the real fleet: a 140-item backlog, 254 branches, 534 open pull requests. It surfaced 61 candidates for one task, because in a fleet whose work shares one house vocabulary two words in common is met by almost everything, and a set nobody can read trains the acknowledgement into a reflex - worse than no scan. So a candidate must clear BOTH a shared-token floor (default 2, FM_SPAWN_OVERLAP_MIN_TOKENS) AND cover at least half (default 50, FM_SPAWN_OVERLAP_MIN_PERCENT) of the SMALLER of the two token vocabularies. That cut the same case to 12 with the genuine duplicate ranked first. Denominating by the smaller vocabulary rather than by the subject is also deliberate: a branch name carries four or five words against a subject and body carrying dozens, so subject-only denomination would put branches - the earliest evidence somebody already started - permanently out of reach. Both halves have their own test with a verified negative control. Candidates are ORDERED strongest evidence first and never bounded; the sort column is dropped immediately after sorting.
ACCEPTED IN THE PRIOR REVIEW ROUND, all six findings, five auto-fix and one the user decided:
OTHER ACCEPTED DECISIONS: the subject is the task id plus the first line of the brief's task statement. Sources are the backlog's open items, tasks holding live runtime metadata, local and remote branches, and open PR titles; a persistent secondmate is a home rather than a work item and is excluded. Results are recorded in state/.meta as overlap=, overlap_refs= and overlap_ack=, so 'nothing was compared' and 'compared, found nothing' stay distinguishable. The pull request listing goes to a temporary file rather than a command substitution because FM_PR_LIST_ERROR - the whole difference between unavailable and none - would not survive the subshell. config/spawn-overlap is inherited by secondmate homes. --overlap-ack is refused on --secondmate spawns.
WHY THIS RUN IS A RESTART: the previous run 01KZF6PK6BYFXK2CN2X7DTN4E9 died mid-fix-round when its review agent exited during an environment freeze, not on any finding. Its fix commit b183ab8 was preserved and recovered through the supported guarded custody recovery, so this branch carries both commits and no work was discarded or re-done.
EVIDENCE: the replay test was confirmed red twice by stashing the change and green with it. All 16 cases in tests/fm-spawn-overlap.test.sh pass, bin/fm-lint.sh is clean, and bin/fm-doc-audience-check.sh is ok. fm-spawn-batch, fm-spawn-dispatch-profile, fm-spawn-worktree-settle, fm-trace-context-spawn, fm-pr-merge, fm-pr-check-security, fm-teardown, fm-gotmp, fm-crew-state, fm-fleet-snapshot-view, fm-bearings-snapshot, fm-brief, fm-backend and fm-documentation-audiences all pass. One PRE-EXISTING failure in tests/fm-secondmate-harness.test.sh is unrelated and was confirmed to fail identically on the base commit: it is the worktree-tangle guard firing because the task worktree sits on a feature branch.
The rebase step was already decided for this branch: SKIP. The fork trunk is the deliberate base and the 46 commits ahead of upstream are the fork landing queue, never bundled into contribution history.
What Changed
bin/fm-spawn.shnow runs a duplicate-work overlap scan as the last gate before dispatch: candidate overlaps drawn from open backlog items, live task metadata, local and remote branches, and open PR titles are surfaced ordered strongest-evidence-first and recorded instate/<id>.meta(overlap=,overlap_refs=,overlap_ack=). A newconfig/spawn-overlapknob selects the posture — advisory (default, prints and records but never refuses), enforce (dispatch requires--overlap-acknaming every surfaced ref), or off — while an unknown value or a present-but-unreadable config file refuses.bin/fm-pr-lib.shgains the sharedfm_pr_open_request_titleshelper backing the scan's PR source, hardened across the review rounds to fail loud rather than open: it parses gh-axi'scount: N of M totalheader, self-sizes the PR window, and fails truncated or short listings, so an unreachable forge or a guarded source-read failure yieldsoverlap=unavailable— neveroverlap=none.FM_SPAWN_OVERLAP_MIN_TOKENS, default 2) and coverage of at least half of the smaller token vocabulary (FM_SPAWN_OVERLAP_MIN_PERCENT, default 50), with new coverage intests/fm-spawn-overlap.test.sh(18 cases, all passing) replaying the recorded duplicate-dispatch incident red-before/green-after. Note the branch deliberately sits on the fork trunk, so its delta against origin/main also carries the 46-commit fork landing queue — the rebase step was skipped by prior decision, not oversight.Risk Assessment
✅ Low: The fix commit implements all four accepted findings exactly as instructed - the account-for-the-whole-total invariant with the old comparison removed, a one-shot self-sizing window bounded by a documented and validated ceiling knob, brief-read gaps surfaced as unavailable instead of silently degrading, and the corrected reader comment - each behind tests that assert the guard through the executable interface (including call-count proofs of exactly-one re-list), with docs fully consistent and a read-only live-forge check confirming the complete-listing header shape the pass path depends on; the only residual note is a latent set -e hazard that cannot fire at any current call site.
Testing
Ran the full targeted suite tests/fm-spawn-overlap.test.sh (18/18 green at the target commit), proved the incident-replay test is red-capable by reverting the three implementation files to the feature's parent commit (the spawn dispatched the duplicate silently, exactly the recorded incident), and captured end-user CLI transcripts demonstrating the advisory candidate table, the enforce refusal-then-acknowledge flow with overlap_ack recorded in the spawn record, and the forge-outage overlap=unavailable path; also verified --overlap-ack is refused on secondmate spawns and spawn-overlap is inherited config. No UI surface is involved — the CLI transcript is the real end-user surface. All checks passed and the worktree was restored clean.
Evidence: Overlap suite green at target (18/18)
Evidence: Replay test red on pre-feature code (spawn dispatches the duplicate silently)
not ok - the replay did not report a candidate overlap (missing: 'overlap=candidates') --- output --- warning: .../brief.md records no delivery contract line ... spawned merge-path-verifies-no-ci-green harness=pi kind=ship mode=no-mistakes yolo=off window=firstmate:fm-merge-path-verifies-no-ci-green worktree=.../replay/wtEvidence: CLI transcript: advisory mode surfaces the recorded incident's task, branch, and PR
$ fm-spawn.sh merge-path-verifies-no-ci-green <project> --mode no-mistakes --yolo off overlap=candidates overlap[3]{kind,ref,title,shared_tokens}: task,merge-refuses-unverified-green,"refuse merges without verified green checks (repo: alpha) (kind: ship) (since 2026-08-05)",green|merge|verifie branch,fm/merge-refuses-unverified-green,"",green|merge pr,1614,"fix(bin): refuse merges without verified green checks",green|merge These are CANDIDATES, not a verdict: read each one and decide whether it is the same work. ... spawned merge-path-verifies-no-ci-green ... [exit status: 0] $ grep overlap state/merge-path-verifies-no-ci-green.meta overlap=candidates overlap_refs=task:merge-refuses-unverified-green,branch:fm/merge-refuses-unverified-green,pr:1614Evidence: CLI transcript: enforce mode refuses, then dispatches with --overlap-ack recorded
error: merge-path-verifies-no-ci-green would dispatch against work already open, and --overlap-ack does not name: task:merge-refuses-unverified-green branch:fm/merge-refuses-unverified-green pr:1614 Read each candidate above ... re-run with --overlap-ack 'task:merge-refuses-unverified-green,branch:fm/merge-refuses-unverified-green,pr:1614'. Code lists candidates; it never decides equivalence. [exit status: 1] $ fm-spawn.sh ... --overlap-ack 'task:...,branch:...,pr:1614' spawned merge-path-verifies-no-ci-green ... [exit status: 0] meta: overlap_ack=task:merge-refuses-unverified-green,branch:fm/merge-refuses-unverified-green,pr:1614Evidence: CLI transcript: forge outage yields overlap=unavailable, never none
overlap=unavailable overlap[3]{kind,ref,title,shared_tokens}: unavailable,pr,"error: could not reach github.com: dial tcp: lookup github.com: no such host", task,merge-refuses-unverified-green,... meta: overlap=unavailable / overlap_refs=unavailable:pr,task:...,branch:...Evidence: CLI transcript: secondmate --overlap-ack refusal and spawn-overlap inheritance
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
.agents/skills/afk/SKILL.md- branch carries 46 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (212 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
bin/fm-spawn.sh:1459- The default FM_SPAWN_OVERLAP_PR_LIMIT=600 window is about to be exceeded by the live repo: a read-only check during this review shows the forge now reports 551 open pull requests, versus the 534 the intent measured when the default was chosen (~17/day growth). Within days, every ship/scout spawn's PR source will report overlap=unavailable (and enforce mode will require --overlap-ack unavailable:pr on every spawn) until the knob is raised. The intent raised the default to 600 specifically so the strict truncation rule is 'not permanently unavailable on this repo' - that property is expiring. Consider a larger default or documenting the expected operational response.bin/fm-pr-lib.sh:518- fm_pr_open_request_titles fails on truncation only when the forge-reported total exceeds the window (total > limit); the header's own listed-count N in 'count: N of M total' is never compared against M, so a listing where gh-axi returns fewer rows than the reported total for any reason other than the requested limit (M <= limit) passes as the complete open set. The function's contract comment at line 479 promises 'a listing truncated below the forge's reported open total' returns non-zero, and the test fixture itself encodes the gap (count: 2 of 523 with limit 600 asserts rc=0). Under currently verified gh-axi semantics (rows = min(limit, total)) the two rules coincide, and the implemented rule matches the accepted user decision verbatim - so tightening it (fail when header N < M) is the author's call and would require updating the fixture.bin/fm-spawn.sh:1500- The overlap subject extraction suppresses awk read errors (2>/dev/null) and masks the exit status behind the cut pipeline, so a brief that passes the -f existence check at line 1219 but cannot actually be read - or that lost its '# Task' heading - silently shrinks the subject to the bare task id, and the scan can then report overlap=none computed from id tokens alone. This is the only read-failure path in the scan that degrades silently rather than surfacing unavailable or refusing, which cuts against the change's own 'a present-but-unreadable file refuses' doctrine. Practical exposure is low (existence is checked, and neighboring brief reads share the same masking pattern), so this is a consistency question for the author rather than a defect.bin/fm-pr-lib.sh:471- The header comment on fm_pr_open_request_titles says fm-research-scan.sh's --landing prover and fm-spawn's overlap scan 'both ask it through one place', but fm-research-scan.sh:558 still runs its own inline 'gh-axi pr list --state all' reader (a genuinely different query that this open-only helper cannot serve). The comment overstates the helper's blast radius and could mislead a maintainer into believing changes here cover research-scan. Reword the comment to say research-scan asks a different (all-state, delivery) question with its own reader.🔧 Fix: self-size PR window, fail short listings, surface brief gaps
1 info still open:
bin/fm-pr-lib.sh:521- The newlisted=$(printf '%s\n' "$rows" | grep -c .)assignment exits 1 whenever the listing is empty (grep -c reports 0 with status 1), which is the pass case for a repo with no open PRs. Every current call site is safe - fm-spawn's overlap_candidates invokes fm_pr_open_request_titles inside an elif condition where set -e is suspended for the whole function body, and the test suite runs under set -u only - but a future caller invoking the function under plain set -e outside a condition context would abort on the empty-open-set pass path. This fails loud rather than open, so the empty-set law is not violated; noting it because the prior fix round hardened exactly this hazard class elsewhere in the scan. A trailing|| :guard on the assignment would remove the latency if ever desired.✅ **Test** - passed
✅ No issues found.
bash tests/fm-spawn-overlap.test.shat target commit a1babfb — all 18 cases passgit checkout ed376cf -- bin/fm-spawn.sh bin/fm-pr-lib.sh bin/fm-config-inherit-lib.sh && bash tests/fm-spawn-overlap.test.sh— replay test red on pre-feature code (spawn dispatches the duplicate silently), then restored and confirmed cleanManual CLI run offm-spawn.shon the recorded-incident fixture in advisory mode — candidate table surfaces the duplicate task, branch, and PR 1614; spawn proceeds; meta records overlap=candidates and overlap_refsManual CLI run in enforce mode — refusal names all three refs and the exact --overlap-ack value; re-run with it dispatches and records overlap_ack= in state/<id>.metaManual CLI run with unreachable forge — overlap=unavailable with the outage named, never overlap=none, persisted in the spawn recordfm-spawn.sh some-task --secondmate --overlap-ack task:x— refused with the documented reasonSourcedbin/fm-config-inherit-lib.sh— FM_INHERITABLE_CONFIG includes spawn-overlap🔧 **Document** - 1 issue found → auto-fixed ✅
bin/fm-spawn.sh:1296- The measurement comment in bin/fm-spawn.sh's overlap_match block says the matching rule was tuned against '95 open pull requests', while the author's intent narrative for the same measurement says the fleet reported 534 open pull requests (and FM_SPAWN_OVERLAP_PR_LIMIT was raised to 600 for that reason). Both numbers may be true in different frames - the measurement may have run under the pre-fix 100-row listing window - but I could not verify which describes the actual measured pool, so the empirical claim was left as written. Worth a one-word correction by the author if 95 is drift.🔧 Fix: fix overlap comment drift: measurement window and ratio denominator
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.