fix(kanban): bind an explicit survivor claim to the card that names it - #848
Conversation
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $6.64 · duration: 17m 00s · rounds: 1 · files examined: 6 |
Round-2 response to Argus's changes-requested on #848. Argus measured a dispatched worker reaching --survivor-unbound through the CLI and completing a card with an unrelated live PR, in two environment shapes, and asked for an identity gate: refuse the flag unless the caller is not a worker, plus an interactive tty. Measured on this host, that gate is not implementable. A worker runs as the same uid as the operator (502) and forges every process-local signal: * is_dispatcher_owned_worker_context() fails OPEN with no HERMES_KANBAN_OWNER_PID, so `env -u HERMES_KANBAN_*` reads as an operator -- predicate False inherited, True stripped; * ancestry survives one double-fork: the daemonized grandchild reports ppid = 1; * sys.stdin.isatty() is False in the worker and True in a pty.fork child it spawns itself. So the flag is bounded by CAPABILITY instead: _reusable() -- preserve(cleanup=True) does not re-verify, it reuses whatever completion recorded. An unbound claim would otherwise become STANDING authority to discard the workspace on every later reclamation without ever being re-tested. It is now refused as reclamation authority; a bound claim is still reused. _override_hint() -- #848's first refusal text named the flag that converts the reader's own refusal into a completion, and that text is persisted as held_reason and forwarded to a worker's model. The hint is now withheld when a dispatcher grant is present. The refusal still says why it refused. Verified (python3.14, private basetemp): 85 passed across all four survivor test files. Mutations, each applied -> run -> reverted, tree green after: drop _reusable 2 failed _reusable returns previous unconditionally 2 failed hint always names the override 2 failed hint never names the override 3 failed drop the binding (mined_for=None) 6 failed Consequence probe, real preserve(), both branches: dir PRESENT -- every arm REMOVED and RECOVERABLE (in-tree capture runs first, so the escape buys the worker nothing) dir ABSENT -- unbound HOLD, bound REUSE, none HOLD (anti-vacuity) Also root-caused the "inherited flake" reported in round 1. It is not load and not this diff: PYTEST_ADDOPTS pins a FIXED shared --basetemp, so concurrent sibling pytest runs GC each other's tmp dirs. Reproduced on the untouched base a3e3c16 by running that file twice concurrently against one basetemp: 1 failed / 1 error. With a private basetemp, 8/8 green here and 6/6 green on base.
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $12.28 · duration: 32m 48s · rounds: 1 · files examined: 6 |
e49c3fb to
5321511
Compare
Round-2 response to Argus's changes-requested on #848. Argus measured a dispatched worker reaching --survivor-unbound through the CLI and completing a card with an unrelated live PR, in two environment shapes, and asked for an identity gate: refuse the flag unless the caller is not a worker, plus an interactive tty. Measured on this host, that gate is not implementable. A worker runs as the same uid as the operator (502) and forges every process-local signal: * is_dispatcher_owned_worker_context() fails OPEN with no HERMES_KANBAN_OWNER_PID, so `env -u HERMES_KANBAN_*` reads as an operator -- predicate False inherited, True stripped; * ancestry survives one double-fork: the daemonized grandchild reports ppid = 1; * sys.stdin.isatty() is False in the worker and True in a pty.fork child it spawns itself. So the flag is bounded by CAPABILITY instead: _reusable() -- preserve(cleanup=True) does not re-verify, it reuses whatever completion recorded. An unbound claim would otherwise become STANDING authority to discard the workspace on every later reclamation without ever being re-tested. It is now refused as reclamation authority; a bound claim is still reused. _override_hint() -- #848's first refusal text named the flag that converts the reader's own refusal into a completion, and that text is persisted as held_reason and forwarded to a worker's model. The hint is now withheld when a dispatcher grant is present. The refusal still says why it refused. Verified (python3.14, private basetemp): 85 passed across all four survivor test files. Mutations, each applied -> run -> reverted, tree green after: drop _reusable 2 failed _reusable returns previous unconditionally 2 failed hint always names the override 2 failed hint never names the override 3 failed drop the binding (mined_for=None) 6 failed Consequence probe, real preserve(), both branches: dir PRESENT -- every arm REMOVED and RECOVERABLE (in-tree capture runs first, so the escape buys the worker nothing) dir ABSENT -- unbound HOLD, bound REUSE, none HOLD (anti-vacuity) Also root-caused the "inherited flake" reported in round 1. It is not load and not this diff: PYTEST_ADDOPTS pins a FIXED shared --basetemp, so concurrent sibling pytest runs GC each other's tmp dirs. Reproduced on the untouched base a3e3c16 by running that file twice concurrently against one basetemp: 1 failed / 1 error. With a private basetemp, 8/8 green here and 6/6 green on base.
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $15.55 · duration: 24m 43s · rounds: 1 · files examined: 7 |
…binding Four unresolved P1s on #848 @5321511f, each fixed with a test whose expectation is computed independently of the function under test. P1-1 attribution was forgeable. _claimant() used getpass.getuser(), which consults $LOGNAME/$USER/$LNAME/$USERNAME before the passwd database -- so the population the audit trail is meant to attribute (a dispatched worker at the same uid) could record claimed_by "operator". Resolve the real uid via pwd.getpwuid(os.getuid()) and record the number alongside the name. The test poisons all four vars and computes the expected string from pwd/os.getuid directly. P1-2/P1-4 binding was weaker than the path it replaces. A substring in the PR title or body counted as "names the task" and was recorded as a BOUND survivor, which _reusable turns into standing delete authority -- an umbrella changelog or "does not address t_..." qualifies. verify_pr/verify_ref now report corroborated_by, and _verified_explicit records a title/body match as UNBOUND: it still closes the card, it never buys a later delete. Pinned on the consequence via the real preserve(cleanup=True) on the dir-absent branch, with a branch-bound anti-vacuity arm that IS reused. P1-3 the suppressed hint was still persisted. _override_hint() read the WRITER's environment, but the refusal it decorates is written to held_reason and to a workspace_held event that kanban_show replays to a worker's model -- so an operator's refusal handed the flag to a worker redispatched onto the same card. The hint is off the message entirely now; it rides the exception and renders at the CLI boundary, where the environment tested belongs to the caller actually reading. Pinned on the literal _handle_show payload, both writer shapes. Measured: 98 passed serially across the five survivor files (PYTEST_ADDOPTS cleared, private --basetemp); 92 before this commit.
Round-2 response to Argus's changes-requested on #848. Argus measured a dispatched worker reaching --survivor-unbound through the CLI and completing a card with an unrelated live PR, in two environment shapes, and asked for an identity gate: refuse the flag unless the caller is not a worker, plus an interactive tty. Measured on this host, that gate is not implementable. A worker runs as the same uid as the operator (502) and forges every process-local signal: * is_dispatcher_owned_worker_context() fails OPEN with no HERMES_KANBAN_OWNER_PID, so `env -u HERMES_KANBAN_*` reads as an operator -- predicate False inherited, True stripped; * ancestry survives one double-fork: the daemonized grandchild reports ppid = 1; * sys.stdin.isatty() is False in the worker and True in a pty.fork child it spawns itself. So the flag is bounded by CAPABILITY instead: _reusable() -- preserve(cleanup=True) does not re-verify, it reuses whatever completion recorded. An unbound claim would otherwise become STANDING authority to discard the workspace on every later reclamation without ever being re-tested. It is now refused as reclamation authority; a bound claim is still reused. _override_hint() -- #848's first refusal text named the flag that converts the reader's own refusal into a completion, and that text is persisted as held_reason and forwarded to a worker's model. The hint is now withheld when a dispatcher grant is present. The refusal still says why it refused. Verified (python3.14, private basetemp): 85 passed across all four survivor test files. Mutations, each applied -> run -> reverted, tree green after: drop _reusable 2 failed _reusable returns previous unconditionally 2 failed hint always names the override 2 failed hint never names the override 3 failed drop the binding (mined_for=None) 6 failed Consequence probe, real preserve(), both branches: dir PRESENT -- every arm REMOVED and RECOVERABLE (in-tree capture runs first, so the escape buys the worker nothing) dir ABSENT -- unbound HOLD, bound REUSE, none HOLD (anti-vacuity) Also root-caused the "inherited flake" reported in round 1. It is not load and not this diff: PYTEST_ADDOPTS pins a FIXED shared --basetemp, so concurrent sibling pytest runs GC each other's tmp dirs. Reproduced on the untouched base a3e3c16 by running that file twice concurrently against one basetemp: 1 failed / 1 error. With a private basetemp, 8/8 green here and 6/6 green on base.
…binding Four unresolved P1s on #848 @5321511f, each fixed with a test whose expectation is computed independently of the function under test. P1-1 attribution was forgeable. _claimant() used getpass.getuser(), which consults $LOGNAME/$USER/$LNAME/$USERNAME before the passwd database -- so the population the audit trail is meant to attribute (a dispatched worker at the same uid) could record claimed_by "operator". Resolve the real uid via pwd.getpwuid(os.getuid()) and record the number alongside the name. The test poisons all four vars and computes the expected string from pwd/os.getuid directly. P1-2/P1-4 binding was weaker than the path it replaces. A substring in the PR title or body counted as "names the task" and was recorded as a BOUND survivor, which _reusable turns into standing delete authority -- an umbrella changelog or "does not address t_..." qualifies. verify_pr/verify_ref now report corroborated_by, and _verified_explicit records a title/body match as UNBOUND: it still closes the card, it never buys a later delete. Pinned on the consequence via the real preserve(cleanup=True) on the dir-absent branch, with a branch-bound anti-vacuity arm that IS reused. P1-3 the suppressed hint was still persisted. _override_hint() read the WRITER's environment, but the refusal it decorates is written to held_reason and to a workspace_held event that kanban_show replays to a worker's model -- so an operator's refusal handed the flag to a worker redispatched onto the same card. The hint is off the message entirely now; it rides the exception and renders at the CLI boundary, where the environment tested belongs to the caller actually reading. Pinned on the literal _handle_show payload, both writer shapes. Measured: 98 passed serially across the five survivor files (PYTEST_ADDOPTS cleared, private --basetemp); 92 before this commit.
5b1064d to
4205218
Compare
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6 · cost: $16.95 · duration: 16m 11s · rounds: 1 · files examined: 7 |
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $17.11 · duration: 30m 36s · rounds: 3 · files examined: 7 |
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $23.25 · duration: 25m 21s · rounds: 1 · files examined: 8 |
An operator- or worker-named --survivor-pr/--survivor-ref was verified only as "this PR exists on GitHub and is OPEN or MERGED". _verified_explicit() called verify_pr()/verify_ref() with mined_for UNSET, so the one check that ties a PR to a card never ran on the explicit path. preserve() then treats a verified explicit survivor as authority for the stale-bases branch -- the branch whose whole job is protecting UNPUSHED implementation work -- so any live PR authorised deleting a workspace whose bytes may exist nowhere else. Measured on fork/main with real gh, two PRs unrelated to the probe card: NousResearch#1 explicit -> ACCEPTED MERGED; mined -> REFUSED #837 explicit -> ACCEPTED OPEN; mined -> REFUSED After this change both are REFUSED on the explicit path and ACCEPTED only under the new override. Remedy (a) with an explicit override. The claim now carries the same task-id binding the mined path carries. The legitimate operator case -- a human who knows the work landed on a differently-named branch -- keeps a reachable path via --survivor-unbound, which is recorded on the survivor (unbound=True, claimed_by=<OS user>) and replayed into the task event log, so the authorisation is auditable rather than invisible. The override is a CLI flag only: no kanban_complete tool argument can express it, so a worker on the #839 tool surface cannot self-certify an unrelated survivor. The explicit path corroborates on headRefName/title/body -- the PR's own claim about which card it implements, from a caller who already vouched for the PR's identity. The mined path stays branch-only via the `corroborate` default: widening it would let a PR body that merely mentions a card id verify itself out of handoff text. Verified: 79 passed / 0 failed across all four survivor files (fork/main baseline measured 65 in a clean-room worktree; +14 new). Mutations, each run and reverted: drop the binding -> 3 red; widen the mined path -> 1 red; kill the override -> 2 red. Disjoint from #837/#839/#842/#796 -- none of them touches _verified_explicit.
Round-2 response to Argus's changes-requested on #848. Argus measured a dispatched worker reaching --survivor-unbound through the CLI and completing a card with an unrelated live PR, in two environment shapes, and asked for an identity gate: refuse the flag unless the caller is not a worker, plus an interactive tty. Measured on this host, that gate is not implementable. A worker runs as the same uid as the operator (502) and forges every process-local signal: * is_dispatcher_owned_worker_context() fails OPEN with no HERMES_KANBAN_OWNER_PID, so `env -u HERMES_KANBAN_*` reads as an operator -- predicate False inherited, True stripped; * ancestry survives one double-fork: the daemonized grandchild reports ppid = 1; * sys.stdin.isatty() is False in the worker and True in a pty.fork child it spawns itself. So the flag is bounded by CAPABILITY instead: _reusable() -- preserve(cleanup=True) does not re-verify, it reuses whatever completion recorded. An unbound claim would otherwise become STANDING authority to discard the workspace on every later reclamation without ever being re-tested. It is now refused as reclamation authority; a bound claim is still reused. _override_hint() -- #848's first refusal text named the flag that converts the reader's own refusal into a completion, and that text is persisted as held_reason and forwarded to a worker's model. The hint is now withheld when a dispatcher grant is present. The refusal still says why it refused. Verified (python3.14, private basetemp): 85 passed across all four survivor test files. Mutations, each applied -> run -> reverted, tree green after: drop _reusable 2 failed _reusable returns previous unconditionally 2 failed hint always names the override 2 failed hint never names the override 3 failed drop the binding (mined_for=None) 6 failed Consequence probe, real preserve(), both branches: dir PRESENT -- every arm REMOVED and RECOVERABLE (in-tree capture runs first, so the escape buys the worker nothing) dir ABSENT -- unbound HOLD, bound REUSE, none HOLD (anti-vacuity) Also root-caused the "inherited flake" reported in round 1. It is not load and not this diff: PYTEST_ADDOPTS pins a FIXED shared --basetemp, so concurrent sibling pytest runs GC each other's tmp dirs. Reproduced on the untouched base a3e3c16 by running that file twice concurrently against one basetemp: 1 failed / 1 error. With a private basetemp, 8/8 green here and 6/6 green on base.
… card The binding in 484c3b3 made an explicit --survivor-pr corroborate the card that names it. Three pre-existing tests in test_kanban_survivor_stale_bases.py drive the HAPPY path through that flag against a `remote` fixture whose headRefName was absent, so under the new binding they refused and went red: test_stale_bases_with_a_verified_survivor_pr_completes test_cli_successful_completion_exits_zero_and_says_so test_partial_loss_keeps_both_the_surviving_repo_and_the_operator_ref The fixture now returns a headRefName that is UNRELATED by default -- so the refusal tests in that file keep their teeth without being rewritten -- and a names_card() helper makes the claim corroborate the card for the three tests that want the happy path. No production code changed. Verified (python3.14, private basetemp, PYTEST_ADDOPTS cleared): test_kanban_survivor_stale_bases.py 7 passed all five survivor test files, serial 92 passed Mutations re-run over the four gated files (55 tests), each applied -> run -> reverted, tree green after: drop _reusable 2 failed _reusable returns previous unconditionally 2 failed hint always names the override 2 failed hint never names the override 3 failed drop the binding (mined_for=None) 6 failed
…binding Four unresolved P1s on #848 @5321511f, each fixed with a test whose expectation is computed independently of the function under test. P1-1 attribution was forgeable. _claimant() used getpass.getuser(), which consults $LOGNAME/$USER/$LNAME/$USERNAME before the passwd database -- so the population the audit trail is meant to attribute (a dispatched worker at the same uid) could record claimed_by "operator". Resolve the real uid via pwd.getpwuid(os.getuid()) and record the number alongside the name. The test poisons all four vars and computes the expected string from pwd/os.getuid directly. P1-2/P1-4 binding was weaker than the path it replaces. A substring in the PR title or body counted as "names the task" and was recorded as a BOUND survivor, which _reusable turns into standing delete authority -- an umbrella changelog or "does not address t_..." qualifies. verify_pr/verify_ref now report corroborated_by, and _verified_explicit records a title/body match as UNBOUND: it still closes the card, it never buys a later delete. Pinned on the consequence via the real preserve(cleanup=True) on the dir-absent branch, with a branch-bound anti-vacuity arm that IS reused. P1-3 the suppressed hint was still persisted. _override_hint() read the WRITER's environment, but the refusal it decorates is written to held_reason and to a workspace_held event that kanban_show replays to a worker's model -- so an operator's refusal handed the flag to a worker redispatched onto the same card. The hint is off the message entirely now; it rides the exception and renders at the CLI boundary, where the environment tested belongs to the caller actually reading. Pinned on the literal _handle_show payload, both writer shapes. Measured: 98 passed serially across the five survivor files (PYTEST_ADDOPTS cleared, private --basetemp); 92 before this commit.
Rebase onto fork/main 3154920 collided in test_kanban_survivor_stale_bases.py with #852 (44408ee), which added `missing`/`tips` to the `remote` fixture and narrowed the gh stub to forward exactly {state, headRefOid, mergeCommit}. That narrowing DECLAWS names_card(): it writes headRefName, the field the explicit binding corroborates on, and the stub dropped it -- so the three happy-path tests silently became refusal tests. Forward every PR field instead of an allowlist, and name the card on the --survivor-ref arm too (verify_ref binds on the advertised ref, so refs/heads/work is an unrelated branch now, not an unverified SHA). Verified the helper is still load-bearing after the merge: names_card made a no-op -> 3 red. 104 passed serially across the five survivor files.
…ding #839 (tool-reachable survivor_pr) landed on main mid-run. Its fixture PR is live and MERGED but its head branch names no card, so the binding this branch adds correctly REFUSES it -- caught as the only head-vs-base delta in the -k kanban sweep (33 head / 32 base, one name). The gate is right, the fixture was not: name the card for the happy path, exactly as tests/hermes_cli/test_kanban_survivor_stale_bases.py does. Also lands Argus's r2 carry-forward. test_the_tool_surface_cannot_express_ the_override is a SOURCE GREP and structurally cannot catch a runtime kwarg, so add the RUNTIME arms now that survivor_pr is really on the tool: - a live PR that does not name the card -> refused, workspace bytes intact - 4 smuggling spellings of the CLI-only override (survivor_unbound, unbound, string "1", metadata-nested) -> all refused, bytes intact Measured: 11 passed. Mutation, applied then reverted: drop the binding (mined_for unset on the explicit path) -> 5 red, all five of the new arms.
_verified_explicit chose its refusal TEXT by comparing two independent gh/git round-trips, and _query collapsed every failure mode into None. A blip on the bound call therefore made the second (weaker) call succeed, and the kernel stated as fact that a claim it never checked "is live but does not name <card>". That sentence is persisted to held_reason and to the workspace_held event kanban_show replays, and the CLI then offers --survivor-unbound as the remedy -- laundering a network blip into a permanent UNBOUND downgrade that _reusable refuses as reclamation authority forever. Split the two outcomes at the seam: _query raises RemoteUnavailable when the subprocess did not answer (non-zero exit, OSError, timeout, undecodable output); None still means the remote answered and the claim does not hold up. _verified_explicit maps RemoteUnavailable to "could not verify ... against the remote" with no override hint. discover() still treats no-answer as "not this candidate" -- it states no reason, so the distinction is not load-bearing there. Also closes the stale --survivor-ref help (verify_ref enforces the task-id binding since d80e20e; the help still described the old contract). Verified: 123 passed (survivor + external-survivor + binding + stale-bases + authority + tools/test_kanban_tool_survivor) Mutation battery, each applied to a clean tree and reverted (tree md5 byte-identical after): MT1 _query swallows the failure again -> 2 failed MT2 blip reported as irrelevance again -> 5 failed MT3 discover stops tolerating no-answer -> 1 failed M5 drop the binding -> 20 failed M1 drop _reusable -> 4 failed M8 title/body counts as BOUND -> 4 failed M10 corroborated_by never reported -> 7 failed HELP --survivor-ref help forgets the binding -> 1 failed Refs: kanban t_de2e348e, Argus round 3.
52fdc3c to
ecd1d5f
Compare
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 4/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $30.85 · duration: 26m 09s · rounds: 1 · files examined: 8 |
fork/main landed multi-repository operator survivors (#865 family): both claim flags became `action="append"` and each value may carry a `<workspace-relative-repo>=` qualifier, and `_verified_explicit` changed shape from "return one ref" to "return {repository-key-or-None: ref}". This branch had changed the SAME function to bind each claim to the card. Neither side is droppable, so both are kept: * `_verified_explicit(task_id, ...)` keeps the per-claim loop and the qualifier split from fork/main, and applies the binding, the RemoteUnavailable split and the unbound/weak-corroboration recording to EACH claim before it is filed under its repository key. * `_external` keeps fork/main's multi-ref explicit branch and this branch's `_reusable(previous)` gate on the cleanup path -- an unbound claim still never becomes standing delete authority. * `complete_task` takes `Sequence[str]` claims AND `survivor_unbound`. * The `--survivor-ref`/`--survivor-pr` help carries both contracts: the task-naming requirement and the repeatable `<repo>=` qualifier. Test-side: fork/main added 17 happy-path tests driving `survivor_pr=PR` against the `remote` fixture, whose `headRefName` is UNRELATED by design so this file's refusal tests keep their teeth. Under the binding those claims are correctly refused, so each now calls the existing `names_card(remote, tid)` helper -- the same declawing r2 applied to the three pre-existing happy paths. No production behaviour is relaxed for them; the refusal tests are untouched. `test_the_help_documents_the_task_naming_requirement` tracks the new `[REPO=]` metavars. Verified (python3.14, PYTEST_ADDOPTS cleared, private --basetemp, serial): 226 passed -- survivor + external-survivor + binding + stale-bases + authority + tools/test_kanban_tool_survivor + pr-gate Refs: kanban t_de2e348e.
FleetReviewReviewed with 2 of 3 model families — openai unavailable. Confidence: 3/5 Findings
FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $31.97 · duration: 48m 52s · rounds: 2 · files examined: 8 |
fork/main advanced 83 commits since 0dc715c, including the survivor stack (#837 #842 #848 #856 #872 #879 #886 #888) which rewrote kanban_survivor.py from 475 to 1516 lines. Four conflicts, all in kanban_survivor.py, resolved toward main's shapes: _git union: main's input= (needed by _present_commits/_rev_list) plus this PR's timeout= (needed by _content_advisory's 120s fetch). _capture kept main's extraction; folded this PR's canonical-tree fallback + mirror_hint advisory INTO it, so the _explain_broken_object_store classifier still wraps every object-reading step. ref arm kept main's 'not bundles and len(refs) == len(repos) + len(carried)' (the #842/#848 carried-survivor accounting) and this PR's canonical sidecar. The pre-#848 unbound-claim path is NOT reintroduced: _verified_explicit, _unbound_keys, _reusable and _bound are main's, untouched. The landed arm in preserve() and _verify_landed/_landed_contains_history merged without conflict. Verified: 11 survivor files (4 from this PR + 7 landed since): 255 passed, 0 failed the PR's own 4 files: 98 passed, 0 failed mutation, dirty-tree guard neutered: 16 passed, 2 FAILED mutation, history binding neutered: 15 passed, 3 FAILED ruff on kanban_survivor.py + kanban_db.py: clean git diff --check: clean
…requires (t_c70dac5c) #875 was a 6-commit stack whose first five were older copies of what #842/#848/#886 landed; only c0340b1 (the complete TOOL accepting a qualified --survivor-pr claim) was new. Re-ported that one commit linearly onto main (clean cherry-pick, 2 files, +192/-13). Its remote_multi fixture predates #848's rule that a live PR must name the card it vouches for, so 3 of its 5 new tests failed on main with "is live but does not name t_..."; the stub now answers headRefName=operator/<card>-landed-elsewhere like the single-repo fixture in the same file. tests/tools/test_kanban_tool_survivor.py 16/16. Apollo merge pass 2026-09-23.
Closes kanban card
t_de2e348e(found by Argus reviewing #839).The gap
_verified_explicit()calledverify_pr()/verify_ref()withmined_forUNSET, so the one check that ties a PR to a card never ran on the
operator/worker-named path. The claim was verified only as "this PR exists on
GitHub and is OPEN or MERGED".
preserve()then treats a verified explicitsurvivor as authority for the stale-bases branch — the branch whose whole job
is protecting UNPUSHED implementation work — so any live PR authorised
deleting a workspace whose bytes may exist nowhere else.
Pre-existing, not introduced by #837 (Argus measured it bit-identical on
a3e3c169b9, before #837). #837/#839 widen who can name a survivor; this PRfixes the lock rather than the door.
Decision: (a), with an explicit override
The explicit claim now carries the same task-id binding the mined path carries.
The legitimate operator case — a human who knows the work landed on a
differently-named branch — keeps a reachable path via
--survivor-unbound,recorded on the survivor (
unbound=True,claimed_by=<OS user>) and replayedinto the task event log, so the authorisation is auditable rather than
invisible.
The override is a CLI flag only. No
kanban_completetool argument canexpress it, so a worker on the #839 tool surface cannot self-certify an
unrelated survivor.
test_the_tool_surface_cannot_express_the_overridepinsthat by construction (schema + handler source), not by convention.
The explicit path corroborates on
headRefName/title/body— the PR's ownclaim about which card it implements, from a caller who already vouched for the
PR's identity. The mined path stays branch-only via the
corroboratedefault:widening it would let a PR body that merely mentions a card id verify itself
out of handoff text.
Measured, real
gh, the exact two PRs Argus measured as ACCEPTEDBoth were ACCEPTED on the explicit path before this change.
Tests
79 passedacross all four survivor files (fork/main baseline 65; +14 new).Mutations, each applied, run, and reverted:
mined_for=Noneon the explicit path)corroboratedefault gains title/body)test_the_wider_corroboration_is_explicit_onlyNegative control held: a named OPEN/MERGED PR with no correlation to the card
HOLDS the workspace;
test_unrelated_live_claim_does_not_delete_the_workspaceasserts the bytes on disk, not just the return value.
Three tests in
test_kanban_survivor.pyflake under parallel host load; proveninherited by reproducing on the untouched base
a3e3c169b9(run 4 of 4 redthere with an identical signature).
Fork-only files — no upstream PR (verified 404 upstream).
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.