Repository navigation
Move the Session Handoff to a Chain of Labeled Issues - #1501
Conversation
A handoff written to a scratch file fails three ways a chain of issues closes. The file is not found where the next session looks, more than one candidate is found and nothing says which is current, and it holds no history, so a later round re-runs a path an earlier round already tried and already wrote down. Four surfaces carry the replacement. `repo-config/labels.json` declares a `handoff` label, a surface marker rather than a triage label, and `repo-config/README.md` records why a handoff issue carries no triage label at all. `scripts/handoff.py` is the mechanics: seven subcommands, a three-value exit contract separating a refusal from the command not having run, and a `--dry-run` on each of the three that write. `.agents/skills/session-handoff/` is the judgment, carrying `GOVERNANCE.md` "Communicating with the User" whole as a generated include. And `AGENTS.md` "Session Scope" replaces the file rule with the issue rule, keeping a size rule for `GOVERNANCE.md` to point at. `backlog-burndown` now filters the label out of its ranking and its backlog counts, since a track in use always has an open link. Closes #1470 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review rounds over the fourteen canonical units this change moves found sixty-one defects, and this settles the ones that survived triage. Six were in the script. `new` on a track whose lane was closed out filed round 1 with `previous=none` beside the existing chain, orphaning every link already written, and `newest_closed` had no full-window refusal, so the same orphan was reachable one page further out. Exit 2 collided with argparse's usage code while an unmodeled exception fell through to CPython's exit 1, so a refusal and a failure to reach one shared codes both ways. A walk that stopped at a link carrying no metadata block returned no notice, and under `--grep` the terminating link was filtered out of the listing, so a search that stopped three links in read as exhaustive. `adopt --previous` stamped a caller-typed issue number into the block with nothing having read it. And the cap message named `--limit` on `resume`, which rejects that flag. The rest were prose. The load-bearing ones: the carried rule left an unattended session required to file, forbidden the fallback, and unable to be authorized, so filing a link is now authorized standing and bounded to the chain, with a report as the one alternative where no link can be filed at all. The size rule now resolves against the `GOVERNANCE.md` sentence that points at it. And the skill no longer reads its own trigger as a grant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test double returned every field of every issue whatever `--json` asked for, so a caller reading a field it never requested passed here and raised against the real tool. That is not hypothetical: `open_handoffs` did not request `state`, `cmd_new` compared it, and the primary path therefore raised `KeyError` after the create and the comment had landed and before the close, leaving two open handoffs on the track and every later command refusing. Sixty-five green cases said nothing. The stub now honors `--json`, `--label`, `--state`, and `--limit`, and reverting the field makes the suite fail on exactly the two cases that name the three-step order. Five more defects the same pass found. The marker's line anchor rejected a CRLF body, so one press of GitHub's Edit button unchained a handoff, which is the one property the block exists for. A body written on Windows would have been CRLF for the same reason `build_dist.py` already guards. `link` validated one field of one of its two issues, so it would close another lane's open handoff, or make an issue its own predecessor, and exit 0. `adopt` wrote a track without reading that track's one-open invariant, manufacturing the ambiguity every other subcommand refuses over. And the label read was the one window with no full-page guard, failing toward a refusal that sends the caller to re-apply a set that is already applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`link` guarded self-linking and a track mismatch but not direction, so transposing its two numbers passed every check: the older link was rewritten to name the newer, the newer was closed, and the run exited 0 over a two-link cycle. A chain runs one way, so the round numbers settle which of the two is the successor, and the direct back-edge is refused beside it. Transposing them is the ordinary slip under the one condition `link` exists for. `adopt` wrote the label before it read `--previous`, so a number that did not resolve left a labeled issue carrying no metadata block, and `require_adopted` then refused every read on every track in the repository until someone re-ran it. Every read a write depends on now happens before the first write. The closed-side read window was a wall a working repository reaches by construction, since closed links accumulate one per round while open ones are bounded by the one-per-track invariant. It is now its own much larger window, and it refuses only where the page filled and the track was not found in it, rather than on a full page regardless. `tracks` is documented as the survey that reports what the others refuse over, and a double-marked issue took it down along with `adopt`, which is the way out. The refusal now rides on the row. Also: `gh` stderr ran on into this tool's own message with no newline and counted characters while saying bytes, and the below-1 refusal for `--new` and `--previous` named them without their dashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A review pass reverted six of the previous round's seven fixes one at a time and the suite stayed green through all six, so nothing in it held that work in place. Two of its cases could not have failed at all: both built their target with the helper's default labels, so `adopt` took the already-labeled branch, never issued a label write, and the assertion that no write preceded the failing read held whichever order the two steps ran in. Every guard in the tool now has a case that fails when the guard is removed, checked by mechanically reverting all twenty-one of them one at a time. That is the check that was missing, rather than more cases. Three defects the same pass found. `parse_marker`'s refusal was caught at one of its three call sites, so one hand-edited issue anywhere in the repository refused `new` and `chain` on every unrelated lane, and aborted `chain` and `resume` mid-output. `adopt --previous` got none of the three guards `link` applies to the same field, so it would write a self-cycle or a backwards cross-track predecessor and exit 0. And the marker grammar admitted `previous=0`, which reached `gh` as an issue number and exited 2 for a block the caller can fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every case replaced `run_gh` wholesale, so its own body ran in no test at all, and neither did the confirmations the write helpers make on top of it. Four guards could be deleted outright with the suite green: the return-code check, the stderr cut notice, the JSON decode guard whose failure mode is the degraded empty answer the label refusal exists to prevent, and the close read-back that `GOVERNANCE.md` "Repository Boundaries and Write Safety" requires. The matrix now runs to thirty-one and covers all of them. Three more defects the same pass reproduced. `newest_closed` skipped an unreadable closed link and returned an older match, forking the chain silently inside the one function written to prevent chain forks, so a link met before a match is now refused over rather than passed: the scan runs newest first, which makes anything skipped newer than the match. `adopt` still wrote the label before the body, so a failed body edit left a labeled block-less issue that refused every read on every track, where the other order leaves an unlabeled issue carrying a block that blocks nothing. And its occupied-track guard read only rows whose block parsed, so a malformed standing link was invisible to the one check whose whole job is keeping two off a lane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tter The previous round moved `adopt`'s body write ahead of its label write on the grounds that an unlabeled issue carrying a block blocks nothing. It does block something: every read here is label-filtered, so that issue is invisible to `current`, `resume`, `tracks` and `chain`, the re-run was refused as already carrying a block, and the next `new` then filed round 1 with `previous=none` beside a chain that existed. That is a silent fork where the old order gave a loud, self-healing refusal. The order stays, and a block matching what this run would write now falls through to the label step, which is what makes body-first recoverable. `link` tested the predecessor's state before commenting where `new` comments before testing it, so on an already-closed predecessor the one write `link` exists to finish was the one write it could never make, and it reported the chain finished. `walk` gained a phrase for its fifth early end, an ancestor `gh` cannot resolve, which previously discarded every link already read. Two fixture defects mattered more than any of those. `FakeGh` ignored `--repo`, so it could be dropped from any of nine call sites with every case green, and `gh` would then resolve the repository from the working directory, which is the failure this whole script is built against. `_create` hardcoded the label rather than reading argv, so `create` could stop labeling and nothing would notice, while every handoff filed would be invisible to every read. The suite's green was not evidence for either property. The revert matrix now runs to forty-five. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…anches `newest_closed` refused over a closed link whose block could not be read, because either could be the track's real newest and chaining past it forks the chain. A closed link carrying no block at all is the same hazard for the same reason, and it fell through the track test with nothing recorded: a lane whose newest link had its block edited out filed its round number twice and orphaned the newer one, exit 0, silent. The search side lied to match, reporting no result over a walk that never reached the link holding the text. `adopt`'s half-applied recovery returned before the guards its first-order branch runs, so a lane that acquired an open link between the half-applied run and the re-run got a second one, turning a silent fork into a hard ambiguity that every later read refuses over. Both branches now run the same check, which is its own function rather than a block one path happens to reach. Three fixtures proved less than their names. The newer-than-the-match case only ever used a doubled block, which is why the bare-block half went unseen. The half-applied-adopt case held only the issue being adopted, so the competing link its own docstring describes was never there. And the label check looped two subcommands rather than the seven the parser declares. The revert matrix reaches fifty-four, including the seven guards it could not previously see: the below-one check whose absence would stamp `round=0` and make an issue permanently unreadable, the three shape guards, the OWNER/NAME slash count, and the two Windows-only behaviors, which are asserted on the argument rather than the bytes since on Linux the two are identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An issue carrying a metadata block and no label is invisible to every read here, since all of them filter on the label. Two rounds closed that hazard on the `newest_closed` and `adopt` sides and left it open on the third. `link` guarded its successor for a block and never for the label, so handing the lane to a half-applied `adopt` closed the predecessor and left the track with no findable head, and the next `new` then filed a second live head naming the same predecessor at the same round. Every step reported success. `link` now refuses and prints the `adopt` command that finishes the labeling. `adopt` shared one refusal between two states, so an issue with a block and no label was told there was nothing to adopt when the label was the whole remaining step. The two now say what they mean, and the second names the values the block actually holds so a re-run can match them. The suite had one systematic hole behind three of those findings: no case asserted that a write carries the prose it moves. `new` could file a body holding nothing but the marker and `link` could erase the handoff it was pointing at, both with every test green. `require_adopted` was also covered at one of its three call sites, and two early returns whose absence duplicates a write were invisible. The revert matrix reaches sixty-two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous round taught `link` to refuse a successor carrying a block and no label, and to print the `adopt` command that finishes labeling it. That command always refused in the one state that printed it: the predecessor is still open and labeled on the track, so the occupied-track guard counted it as a competitor. The predecessor a successor's own block names is the link it replaces rather than a second one competing with it, so the guard now excludes it. While stuck, every read reported the lane healthy and `new` filed a second head naming the same predecessor at the same round, which is the fork the round set out to close, re-entered through the round's own new refusal. `new` consults the closed side so a closed-out lane is chained onto rather than restarted, and `adopt` is the other write that can start a chain. With no `--previous` it wrote `previous=none` beside an existing chain and exited 0, and because that walk ends cleanly at the block, nothing told a later `--grep` its search had never reached the chain holding the text. It now refuses and names the newest closed link. An issue no longer counts as an unreadable link blocking its own adoption. The case that stood over the first defect asserted the recovery string rather than that the command it names can run, so it proved the refusal and not the recovery. It now runs the printed command through to a labeled issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous round taught `adopt` to refuse starting a second chain beside a closed one, and read only the closed side to decide it. A lane's whole chain can be one open head with nothing closed behind it, which is the ordinary state after the first `new`, so adopting onto exactly that lane printed "this track has no chain" and filed a second root. Where the adopted issue's number is higher than the head's, the next `new` then chains onto the stray and orphans the real round one, every step exiting 0. `track_links` reads open and closed together, and the refusal names the newest link of either. The `--previous` path had the sibling gap: it verified the predecessor's track and round direction and never that the predecessor already had a successor, so two links could name one predecessor and the second was unreachable from `chain`. `--track` was validated with `match` rather than `fullmatch`, so a trailing newline passed while the block parsed it back without one, and the two then read as different tracks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten adversarial passes ran over this branch. The first six each found a new defect in `adopt`, always the same shape: a multi-step write leaving the chain forked, orphaned, or unrecoverable while every step reports success. Each round closed the member it found and the next round found another. The tenth pass measured it rather than arguing it. A randomised search over reachable stores, built only from command sequences that exited 0, found sixty distinct all-green sequences that fork or orphan a track in twenty thousand trials, and every one of them requires `adopt`. Two independent controls restricted to `new` and `link` found none in four hundred trials and none in a further hundred and fifty. The shortest fork is three commands with no malformed data and no failed write, after which `chain --grep` answers "no match" over a chain that contains the text. So `adopt` goes, along with `require_track_free` and `track_links`, which existed only for it. `new` and `link` write the chain forward from state the tool itself produced. `adopt` wrote it from state a human typed, and every guard it needed was discovered one pass at a time rather than designed. An issue that predates the chain joins it by hand now, adding the block to its body and the label to the issue, which the refusals already name. `require_adopted` becomes `require_marked` so no vocabulary points at a command that no longer exists. Five review passes over the skill's own section caught the residue: a `link` bullet that omitted which of its writes the interrupted-run case can actually reach, an authorization paragraph that counted a label riding inside `gh issue create` as a write of its own and so read the standing grant wider than the rule it cites, a `tracks` bullet justifying a hand edit with a claim `link` falsifies, a usage line using one placeholder for two values the tool refuses as equal, and a closed-set claim about the read side that `chain` and `tracks` contradict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pass that first called the remaining risk ordinary still found one write that destroys data. `link`'s body edit replaced the whole body from a copy read earlier in the run, so an edit landing between the two was discarded with no warning and exit 0, and `link` runs in the recovery window, which is exactly when a maintainer is most likely editing the handoff by hand. It now re-reads immediately before writing and refuses where the body moved. Three pieces of residue from dropping the seventh subcommand: the walk's final return was unreachable, since its counter starts as an int and is only reassigned from a digit capture, so every exit is one of the returns inside the loop; the test double still handled a label flag no call site passes; and the track argument still carried a required-mode parameter whose only caller was removed. Three paths had no case. The guard that refuses to hand a lane to an issue carrying a block and no label could be deleted with the suite green, which is the one guard on the anti-orphan path. `link --dry-run` was the preview a caller is told to read before trusting this to close an issue, and no case ran it. And the re-read above needed one. The revert matrix reaches fifty-four, and one of its entries was mislabelled as `link`'s where it anchors on a variable in `new`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping the seventh subcommand deleted the guard that refuses a predecessor which already has a successor, and `link` is now the only path that adopts an issue into a lane. Pointing an orphan at a mid-chain link therefore gave one predecessor two successors, made the skipped branch unreachable from every walk, and exited 0 at each step, after which `chain --grep` answered no match over a chain holding the text with no stop notice printed. That is a regression this branch's own history names: the round before last identified the same gap on the same field, fixed it in the subcommand that has since been removed, and left the sibling untouched. `with_marker` stripped with a bare `rstrip()`, which removes all twenty-five characters Python calls whitespace where the marker grammar tolerates three. A marker line ending in anything else, a non-breaking space being the ordinary artifact of pasting back the line `resume` prints, parses as text to every guard and is then revived by the strip, so the body ends with two blocks and no command can read the track again. Reachable through `new` and through `link`, and invisible to `--dry-run`, which never prints a body. The refusal listing unreadable closed links was unbounded while this module caps gh's own stderr at two thousand characters. A measured case reached twenty-five thousand on one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guard added last round answered None both for "no successor" and for "could not tell", so it passed wherever it was blind. Every other reader in this module separates those two states. Three ways through it, each exiting 0 and each forking the lane: a closed side that filled its read window, a successor whose block could not be parsed, and a successor carrying no label. The first two are now refusals naming what could not be read. The third is answered at the walk instead, which stops at a link carrying no label, since the chain is indexed by that label and a walk reaching what the list reads cannot see makes the two disagree about what the chain contains. `new` had no such guard at all, while resolving a lane's head by issue number where `link` orders by round. Two commands, both exiting 0, then forked a fully readable three-issue lane and regressed its round. It runs the same guard now, before the create rather than after, since a refusal that leaves an issue behind is a refusal that changed something. Last round's strip fix narrowed its class without closing it. A marker's tail is an ordered suffix rather than a set, so removing any character that suffix may itself hold can revive a line: `-->\r ` is not a block and `-->\r` is. Only newlines are stripped now, which is the one thing the tail can never hold, since the line anchor sits before it. And the bound put on one refusal last round was left off its sibling one function away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything about a chain is ordered by round, and `newest_closed` picked a closed lane's head by issue number instead. The two agree only where the links were filed in numeric order, and they disagree wherever a lower-numbered issue joined the lane later. The consequence took two commands, each exiting 0 and each doing the job it exists for. `link` recovers an interrupted `new`, correctly. `new` then resolves a different link as the head, files onto it, and the lane ends with two links claiming one round, the real chain unreachable, and `chain --grep` answering no match over text that is in the lane with no stop notice printed. The guard added last round did not catch it, because it asks whether anything succeeds the head it was handed, which is the wrong question when the head itself is wrong. The head is now the highest round, with the issue number breaking a tie between two links claiming one round, which is itself a defect a reader should not have to resolve. Three cases catch the regression, checked by putting the old key back, and one of them is the two-command sequence end to end with the searchability half asserted rather than only the shape. An unreadable closed link is now refused outright rather than compared by number. Its round compares with nothing, so it could outrank every readable link, which makes naming a head unprovable rather than merely uncertain. The previous behavior used the number as a proxy for that comparison, and the number is exactly the key this commit removes. `scripts/README.md` said settling an unmarked labeled issue is a hand edit because no subcommand writes to an issue the chain did not create. `link` does precisely that, which the same section says two paragraphs later. The reason is now the one that holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eing True The refusal over an unreadable closed link was added breadth-first and named no way out, where its open-side twin ends by saying to add the block by hand or take the label off. One closed issue carrying the label and no block blocks every track in the repository until someone fixes it, so the message now says which edit does that. Its possessive was also built by interpolating a quoted slug, which rendered as `'lane''s`. The comment above `new`'s anti-fork guard still explained it by the head being resolved by issue number, which the commit before this one removed. A head can already have a successor whatever key resolved it, an interrupted run being the ordinary way, so the reason is now that rather than a mechanism the tree no longer has. The read path's own version of that check is filed as #1500 rather than fixed here. Every write path refuses on the states it covers, so no wrong write is reachable through it, and what is reachable is a wrong answer on a lane that is already irregular. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The remedy added last commit was lifted from `require_marked`, where it is attached to the bodies carrying no block and deliberately not to the bodies carrying two. `newest_closed` merged both classes into one list and appended it to both, so the owner of a doubled block was told to add a block, which gives it three, and the refusal that follows says so. The per-issue message for that class already carries its own remedy, and the `cut` bound makes the mismatch worse rather than better, since the wrong appended sentence always survives truncation while the correct per-issue ones do not. The two classes are now separate lists and the appended sentence covers only the class it is true for. The comment above `new`'s anti-fork guard named an interrupted run as a route to it. That is not a route: an interrupted run leaves two open links, and the ambiguity refusal fires before the guard is reached. A hand-edited lane does reach it, on either side, which is the case the existing tests build. The stated harm was also wrong on the closed-side shape, where the rounds do not collide and what is lost is the skipped branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🟡 Changes recommended
scripts/handoff.py has a real error-handling gap where unexpected gh label list --json name row shapes can raise unhelpful unmodeled exceptions instead of a clear execution failure.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR implements issue-based session handoffs by replacing scratch-file handoffs with a chain of GitHub issues labeled handoff, supported by a new hub script (scripts/handoff.py) plus extensive unit tests and documentation updates across the repo’s governance, repo-config, and skills surfaces.
Changes:
- Add
scripts/handoff.py(6 subcommands) to read/write the handoff chain with a strict 0/1/2 exit-code contract and--dry-runon writers. - Add a comprehensive stubbed-
ghtest suite (scripts/tests/test_handoff.py) validating ordering, refusals, and write confirmations. - Declare and document the new fleet
handofflabel and update the session-handoff/backlog-burndown guidance to exclude handoff issues from backlog ranking/counts.
File summaries
| File | Description |
|---|---|
| scripts/tests/test_handoff.py | New unit test suite exercising handoff.py behavior against a stubbed gh. |
| scripts/README.md | Documents handoff.py, adds local test invocation, and updates script taxonomy. |
| scripts/handoff.py | New handoff-chain CLI implementing read/write subcommands and safety/validation rules. |
| reports/canonical-review.json | Updates canonical review digests/stamps for modified/new canonical units. |
| repo-config/README.md | Documents handoff as a surface marker and adds label taxonomy entry. |
| repo-config/labels.json | Adds the handoff label to the fleet label set. |
| GOVERNANCE.md | Updates “Communicating with the User” section references to include session-handoff. |
| AGENTS.md | Replaces scratch-file handoff rule with chained-issue handoff rule. |
| .github/skills/session-handoff/SKILL.md | Generated skill copy for session-handoff guidance. |
| .github/skills/backlog-burndown/SKILL.md | Generated skill copy excluding handoff from ranking/counts. |
| .github/skills/agent-conduct/SKILL.md | Generated skill copy updated to reference session-handoff. |
| .claude-plugin/fleet-skills/skills/session-handoff/SKILL.md | Generated Claude plugin skill for session-handoff. |
| .claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md | Generated Claude plugin skill with handoff exclusion guidance. |
| .claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md | Generated Claude plugin skill updated to reference session-handoff. |
| .claude-plugin/fleet-skills/.source-digests/session-handoff | Records source digest for generated session-handoff skill. |
| .claude-plugin/fleet-skills/.source-digests/backlog-burndown | Updates source digest for generated backlog-burndown skill. |
| .claude-plugin/fleet-skills/.source-digests/agent-conduct | Updates source digest for generated agent-conduct skill. |
| .claude-plugin/fleet-skills/.claude-plugin/plugin.json | Registers the new session-handoff skill in the plugin. |
| .agents/skills/session-handoff/SKILL.md | Source skill defining the session-handoff contract and usage. |
| .agents/skills/backlog-burndown/SKILL.md | Source skill updated to exclude handoff from ranking/counts. |
| .agents/skills/agent-conduct/SKILL.md | Source skill updated to reference session-handoff at the handoff moment. |
Review details
- Files reviewed: 21/21 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot found that `require_label` checked the outer list and then read `row["name"]` off each row unguarded, so a row of another shape raises out of the comprehension and lands on the blind catch, which reports exit 2 without saying what `gh` returned. The same exposure sat in three sibling readers, each of which validated the array and none of which validated a row, so this closes the class rather than the member. `rows_of` takes the field its caller is about to read and refuses anything else, naming the list and the row. One case had to move: it produced its unmodeled exception with a row missing `number`, which is now a named failure. It uses a timestamp `gh` would never emit instead, which no reader validates because none could name every field's own grammar, so the blind catch is still exercised. Answers the review on #1501. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its docstring said each reader validates what it is about to subscript, which is wider than what it does. The commands consuming those rows read fields of their own, `updatedAt` and `title` and `url` and `state` among them, and an absent one still lands on the blind catch. The promise now stops at the field the reader asks `gh` for, and says where the rest go. The case named for the type half of that check did not pin it: a plain string fails the missing-field half on its own, so removing the type check left the case green. It now drives a row that carries the field name without being an object, which is what only the type check catches, and it fails on all three shapes when that clause is removed. The message spent its bound on its own prefix rather than on the row, so a long repository name left six characters of the row visible, and it truncated silently. It goes through `cut` now, like every other bounded message here, which says how much it left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docstring said a plain string proves nothing about the type check, while the case's own second row is a plain string, picked because `"name" not in "name"` is false and the membership half therefore lets it through. The third row is not a container at all, so the membership test raises rather than passing. Three routes to one clause, and the text described one of them as the reason to disregard another. A later editor following it would have dropped a row and un-pinned a mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new GitHub-writing operational workflow (creating/commenting/closing issues) and updates core governance/agent contracts, so it warrants final human review despite strong test and documentation coverage.
Review details
- Files reviewed: 21/21 changed files
- Comments generated: 0 new
- Review effort level: Lite
…g, and the Release-Trigger Retirement (#1489) Promotes thirteen merged pull requests from `develop` to `main`. ## What this promotes - **#1486** State the `host-tools.json` carry rule and stop the empty-overlay warning. `spec/files.json` now requires the declaration unconditionally, and `host_gate.py` no longer warns on an empty one. - **#1484** Fan the validator's pytest leg out over a `python-versions` input, so a caller with more than one interpreter no longer silently loses its other legs to a pinned 3.13, with an entry guard that refuses a value `fromJson` would otherwise admit. - **#1476** Retire the publish-on-merge release trigger, which `spec/files.json` applied to an interface contract whose task has no publish-on-merge path. - **#1479** Ship the release caller snippet and retire the not-yet-released claim. - **#1474** Exclude `node_modules` from markdownlint and Docker lint discovery at any depth. - **#1468** Move the carried-surface cluster off decision, recording the measurements and what stays unmeasured on the issues that hold them. - **#1498** Correct the release-caller snippet's publish-on-merge claim, raised by CodeRabbit against this promotion and fixed on its own branch, since a promotion PR's head is `develop`. - **#1494** Settle the markdown lint exclusion seam as a nested config. - **#1492** Check declared deployment environments and fix the carried-payload rule. - **#1501** Move the session handoff to a chain of labeled issues. Two of its units are verbatim fidelity, `AGENTS.md` "Session Scope" and `GOVERNANCE.md` "Communicating with the User", so every fleet repo picks them up on its next resync. - **#1505** Refuse a bare handoff link rather than reading it as no successor, answering a finding raised against this promotion. - **#1504** Reject a deployment environment name or branch carrying surrounding whitespace, answering two more. - **#1506** Encode an environment name before it becomes a path segment, delete a branch that cannot fire, and dedupe a registry name by case, answering two more and declining one on a reproduction. ## Closing references A `develop`-targeted pull request registers no closing reference, so the issues those pull requests settled close with this promotion. #1470 is the one that needs a word. Two of its seven acceptance items are not met as literally written, and the maintainer's decision to close it here anyway, with the basis for both, is recorded on the issue itself. Closes #1483 Closes #1473 Closes #1487 Closes #1482 Closes #1475 Closes #1470 ## Left open deliberately - **#1499**, **#1500** follow-ups #1501 filed rather than fixed. - **#1503** the validator-looser-than-its-consumer class, filed from findings on this promotion and deliberately not closed by it. Five instances recorded, and the fix is a design decision rather than a check. - **#1497** the Docker delta list never says to remove the `push:` block. - **#1472** the fleet-level pin call, deliberately not pre-empted by #1479. - **#1321**, **#1464** carry #1468's decision record and remain in their own scope. ## Verification `origin/main..origin/develop` merges clean, no end-of-line conflict. Each promoted pull request ran the fleet gate set on its own branch before merging; this promotion adds no content of its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01817rckKywjfoaEV5WF9TKs
Implements #1470. The session handoff moves from a scratch file to a chain of GitHub issues, one open link per track per repository.
Why
A handoff written to a scratch file fails three ways a chain closes. The file is not found where the next session looks. More than one candidate is found and nothing says which is current. And it holds no history, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is the record of what has been attempted, and one overwritten file on one machine is the shape that cannot hold it.
What ships
repo-config/labels.jsonhandoffdeclared beside the other sixteenrepo-config/README.md"Labels"scripts/handoff.py--dry-runon both writersscripts/tests/test_handoff.py.agents/skills/session-handoff/GOVERNANCE.md"Communicating with the User" as a generated includeAGENTS.md"Session Scope"GOVERNANCE.mdto name.agents/skills/backlog-burndown/"Ranking"handofffiltered out of the ranking and every backlog countThe chain is machine-readable from one HTML comment on the body's last line, so a retitled or hand-edited issue still chains and nothing parses the title. The invariant is one open link per track, read from that block rather than from the label. Two on a track is reported, never resolved by picking.
adoptis not in v1#1470 specified a seventh subcommand,
adopt, to put the label and the block on an issue predating both. It was built, and then removed. #1499 records the reasoning and what a returning design must answer.The short version is that it was measured rather than argued. A randomised search over reachable stores, built only from command sequences that exited 0, found 60 distinct all-green sequences that fork or orphan a track in 20000 trials, and every one required
adopt. Two controls restricted tonewandlinkfound none in 400 trials and none in a further 150. An issue predating the chain joins it by hand now, and the tool's refusals name the edit.Verification
Fifteen adversarial review passes ran against this branch, at the strongest tier available, and the branch answers all of them. Defects found per pass: 8, 8, 7, 4, 5, 2, 5, 2, 2, 4, 3, 6, 4, 4, 1. Every one was the same class, a multi-step write leaving the chain forked while each step exits 0.
Three things beyond the ordinary gates are worth naming:
ghrather than a superset of it. It projects--json, honors--label,--stateand--limit, and refuses any call carrying no--repo. Before that, a caller reading a field it never requested passed the suite and raised against the real tool, which is exactly what happened once.linkexists for lanes that are already irregular and a healthy-state search provably cannot reach its defects.Gates: ruff, ruff format, mypy,
build_dist.py --check, 1282 tests,repo_gate.py --check eol,spec/validate.py, whole-treeprose_lint.py, markdownlint, cspell, and 14 of 14 changed canonical units covered by a recorded review pass.Live
repo-config/configure.sh check ptr727/ProjectTemplate releasepasses, which also cleared two pre-existing drifts:has_projectsand thedecisionlabel's description. #1469 was marked by hand and now reads as this repository's round 1 on trackdefault.🤖 Generated with Claude Code
https://claude.ai/code/session_01V1MHrN5KP2ZV1VfEgbE4nr