Repository navigation
Description
Design and requirements only. Four surfaces: a fleet label, a hub script, a skill, and a change to a carried rule in AGENTS.md. Relates #1469, #1434, #1433, #1439.
Why
Handoff between sessions runs through a file today, HANDOFF.md and dated siblings beside it. Three failures recur. The file is not found where the next session looks for it. More than one candidate file is found and nothing says which one is current. And there is no history at all, so a later round re-runs a path an earlier round already tried and already wrote down. The value of a handoff is precisely the record of what has already been attempted, and a single overwritten file on one machine is the one shape that cannot hold it.
#1469 moved the handoff to an issue by hand, under direct instruction, and the shape works. It is discoverable from any machine, its history is permanent, and the chain is walkable. What it lacks is a mechanism. The title format, the forward link, the close, the section order, and the discovery query all live in that one issue body and in whichever session happens to have read it, which is the same fragility the file had, moved.
The purpose is to cut the context a session carries without losing what that session learned, and to let a large body of work break into rounds without every round being planned ahead.
What a Handoff Is, and Is Not
- It is session state and pointers, not the durable record. A defect becomes an issue, a rule becomes documented rule text, a lesson becomes governance prose. The handoff points at those and does not restate them, per
GOVERNANCE.md"Durable Knowledge and Self-Improvement". Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469's own account of what restating a filed decision cost is the worked example. - It records work to do next, and it is not itself backlog work. It carries no triage label, and every backlog enumeration excludes it.
- It is not a place to ask a question. A question waiting on the maintainer is an issue carrying
decision, andGOVERNANCE.md"Communicating with the User" requires the handoff to account for that queue and present it, not to hold it.
Scope
1. A handoff label
Declare one entry in repo-config/labels.json beside the existing sixteen, with a description at most 100 characters and no tab or newline, which is what labels_payload_ok in repo-config/configure.sh enforces. Apply it with repo-config/configure.sh apply, naming the target repository and its workflow model, run from a hub checkout.
Record in repo-config/README.md "Labels" that handoff is a surface marker like agents and skills, never a triage label, and that a handoff issue carries no triage label at all because it records no work of its own. Without that sentence the next reader of the taxonomy reads an unlabelled handoff issue as drift.
The label makes the chain findable by label instead of by title search, which is what lets the title carry a human subject without a parser depending on it.
This inherits #1434's rollout gap. The decision label exists on the hub and on none of the carriers, so a downstream session enumerates an empty queue and reports it healthy. A handoff label declared and not applied fails the same way, silently. The script must therefore refuse rather than degrade when the label is missing from the target repository, naming the configure.sh apply command that fixes it.
2. The chain model
-
One open handoff issue per track per repository. A track is a short kebab-case slug naming a lane of work. A session that names no track uses
default. Parallel lanes each name their own, so each closes its own predecessor and neither reads the other's state as current. -
The title is for humans:
Session Handoff [<track>]: <subject>. Nothing parses it. -
The chain is machine-readable from a metadata block in the body, so a retitled or edited issue still chains. One HTML comment, rendered invisible, as the body's last line:
<!-- handoff: v1 track=<slug> round=<n> previous=<issue number or none> --> -
Closing the loop, in this order: create the new issue, comment the forward link on the previous one, then close the previous one. Creating first means a failure at any later step leaves a discoverable new issue rather than a closed chain with no successor.
-
The invariant a reader checks: exactly one open issue carrying
handofffor a given track in a given repository. Zero means this is the first handoff on that track. Two or more is a defect to report, never one to resolve by picking. -
The handoff issue lives in the repository the work was in. A session that spanned repositories writes the handoff where its branch and its pull requests are, and names the other repositories in the state section.
3. scripts/handoff.py
Hub-hosted, per GOVERNANCE.md "Hub-Hosted Tooling", so it is reached from a hub checkout rather than carried. Python, standard library only, matching the other hub scripts.
Every subcommand takes a required --repo naming the target repository, with no default, for the same reason pr_review.py requires one: a number alone resolves in every repository, and a digest of the wrong one is well formed.
current [--track T]prints the open handoff issue for that track, number and URL.resume [--track T] [--history N]prints the current handoff body followed by a one-line index of the last N closed links in the chain, which is the read side of the repeated-work problem.chain [--track T] [--limit N] [--grep PATTERN]walks backwards throughprevious=and lists number, title, date, and state.--grepsearches the bodies, which is how a session answers "has this already been tried" without reading every round.new [--track T] --title <subject> --body-file <path>performs the three-step chain and prints each step's result.link --new <n> --previous <n>finishes a chain that half-applied, so a failed run is recoverable without a second issue.trackslists every open handoff with its track and age, so an abandoned lane is visible rather than silently current.adopt <issue> --track Tputs the label and the metadata block on an existing hand-written handoff. Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469 needs this: it predates both, and it is the chain's first link.
Behaviour that is not optional:
- Every identifier a write consumes is captured from a read in the same run. No constructed issue numbers, no reuse from a previous run.
- No output of a write is suppressed and no failure is converted into success. Each of the three steps reports its own result. A step that appears to fail is verified, never assumed harmless.
- Ambiguity is a refusal, never a pick. Two open handoffs on one track exits non-zero and names both.
- Exit codes separate the three outcomes: 0 success, 1 a refusal the caller can act on (no label in the repository, no open handoff, an ambiguous track, a body over the hard cap), 2 an execution failure. This is
build_dist.py's contract and the reason a caller can tell a finding from the check not having run. - Body size: warn above roughly 12 KB and refuse above 60 KB, under GitHub's own 65536-character limit. The cap is a backstop. What actually bounds a handoff is the per-section rules below.
- A dry-run flag that prints the calls it would make and writes nothing, because the first thing anyone will want is to see the chain step before trusting it to close an issue.
4. A session-handoff skill
Hand-authored at .agents/skills/session-handoff/SKILL.md, per the skill-lifecycle Skill, with the frontmatter's description carrying the trigger moments: about to end a session or a round of work, about to start one, asked for a handoff, asked what the previous round did, or about to re-attempt something a previous round may already have tried. It is that last case that earns the skill, since a session that does not know a chain exists will not go looking for one.
The skill owns the judgment and the script owns the mechanics. It states what belongs in each section and what does not, it routes the parked-decision-queue account to GOVERNANCE.md "Communicating with the User" as a generated include rather than a restatement, and it names the commands from a hub checkout as plain code spans rather than repo-relative links, which is what makes an installed copy usable.
The resume half states one rule that is easy to skip: re-derive live state, do not trust the handoff's copy of it. Branches, worktrees, open pull requests, and issue counts are read again from git and from GitHub at resume, and the handoff's state section is the pointer to what to read, not the answer. AGENTS.md "Session Scope" already says stale context is worse than absent, and a handoff is context by construction.
5. The handoff issue body template
A fixed section order, so a reader finds a given fact in the same place every time.
- Next steps, in priority order. The single most valuable section, and first for that reason. Each item names what to do and what "done" looks like. At most a handful, ranked. A step blocked on something says so and names the blocker below.
- External blockers. Anything the next session cannot resolve on its own: a maintainer decision, a third-party quota or outage, an upstream release, a credential, a reviewer bot that is not running. Each names who must act, what unblocks it, and what is safe to do meanwhile.
- Internal dependencies. The ordering constraints among the next steps, each stated as the dependency and its reason, so a later session can re-rank when circumstances change instead of following an order it cannot audit.
- State. Branch, worktrees and whose they are, open pull requests, what merged, whether a release was dispatched, and whether the primary checkout is clean. These are facts a resume re-derives, listed so the resume knows what to re-derive.
- The parked decision queue. The count and the ranked list required by
GOVERNANCE.md"Communicating with the User", which also requires the questions to be presented in the same act rather than only recorded. - What the last round did. Issues closed and filed, pull requests landed, and the peripheral issues filed along the way.
- What not to repeat. The section the file form could never carry: paths explored that led nowhere, approaches that failed and why, problems discovered during execution, and the cost of each where it is known. An entry here is a claim about a specific attempt, not a general lesson.
- New learnings. Only what is not already durable somewhere else, each with a pointer to where it was recorded. A lesson that belongs in governance goes to governance and appears here as one line and a link, per
GOVERNANCE.md"Durable Knowledge and Self-Improvement". - The metadata block.
Sections 7 and 8 are what the chain exists for, and they are also the two most likely to be padded. The rule for both is that an entry earns its place by being specific enough to change a later session's behaviour.
6. Backlog exclusion
Every enumeration that ranks or counts open issues excludes handoff, because a handoff issue records no work of its own and an open one is always present by design. That is at least backlog-burndown's ranking step and any count of the open backlog. #1433 already records that handoff-reported backlog counts have been wrong, and adding a permanently-open issue class to the count without excluding it makes that worse.
7. The carried rule change
AGENTS.md "Session Scope" currently reads "Hand off in a file, never in context", with a 2 KB cap on a scratch file. That is replaced by the issue rule. The file form is not kept as a fallback: two documented mechanisms means a later session has to work out which one holds the truth, and the fallback is the one that rots.
Three consequences that must be handled in the same change rather than discovered afterwards:
- The section is
verbatimfidelity perspec/section-model.md, so this is carried content reaching every fleet repository, and it needs the whole-unit review pass thatscripts/canonical_review.pyrecords. GOVERNANCE.md"Communicating with the User" sizes the parked-queue account to "the size rule thatAGENTS.md'Session Scope' sets for it". Replacing the 2 KB cap changes what that sentence points at, so the replacement still has to set a size rule for that sentence to name.- The scratch file does not disappear from the fleet.
backlog-burndownkeeps working notes out of the tree for an in-flight run, and that stays true. What changes is that the handoff itself is no longer one of those notes.
Rules That Bind This Task
- Creating, commenting on, and closing an issue are outward-facing writes. Invoking the skill is the authorization for the chain it performs, and nothing else in the run is authorized by it.
- Every identifier passed to a write is captured from a read in the same run, and no write's output is suppressed or forced to success, per
GOVERNANCE.md"Repository Boundaries and Write Safety". - A handoff quotes no data observed in the maintainer's environment beyond what the work itself produced, per
GOVERNANCE.md"Representative Data in Agent-Authored Text". An absolute home path is the routine case here, since a handoff naturally wants to name a worktree. - The skill follows
skill-lifecycle: both distribution trees regenerated byscripts/build_dist.pyin the same commit,--checkclean,AGENTS.md"Where the Rules Live" recording the new skill in its closing paragraphs as new content rather than a table row, andscripts/skills_install.pyre-run per machine after merge. - The script follows the fleet Python profile for the hub
scripts/tree, standard library only, with a test beside the others underscripts/tests/.
Risks
gh-write-guardmay refuse the script's calls. Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469 records that the auto-mode classifier refusesgh pr commentandgh repo createoutright. If it also refusesgh issue closeorgh issue comment, the chain cannot complete unattended, and the design needs to know that before the skill claims it can. Measure it before building, with a positive control through the path, since a refusal and a silent no-op look identical from outside.- An abandoned track leaves an open handoff that reads as current forever.
trackssurfaces age, which makes it visible but does not resolve it. Whether an abandoned chain gets an explicit close path is worth settling during implementation. - The template invites padding. Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469's body runs to roughly 15 KB, over seven times the cap it replaces, and most of that length sits in exactly the two sections this design values most. The per-section rules are the control, and they are prose, so they will need a round or two of use before they hold.
Acceptance
repo-config/configure.sh checkpasses on the hub withhandoffdeclared, andrepo-config/README.md"Labels" states that it is a surface marker and that a handoff issue carries no triage label.- A real end-to-end run on this repository creates a new handoff issue, comments the forward link on Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469, and closes Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469, with each step's output shown.
adoptputs the label and the metadata block on Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469 first, since it predates both. handoff.py currenton a repository with no handoff issue exits with the refusal code and names the reason, distinguishably from an execution failure.handoff.py currenton a track with two open handoffs refuses and names both, and does not pick.chain --grepfinds a string that appears only in a closed handoff two links back, which is the repeated-work failure this whole change exists to fix, demonstrated rather than asserted.python3 scripts/build_dist.py --checkexits 0 and the canonical review pass is recorded for every new level-two section, including the rewrittenAGENTS.md"Session Scope".AGENTS.md"Session Scope" no longer names a scratch file, still sets a size rule forGOVERNANCE.md"Communicating with the User" to point at, and that sentence still resolves.
Out of Scope
- Migrating the historical file handoffs into the chain. They are local-only and Session Handoff, 2026-09-09: #1321 Test 3 Proved, and the Handoff Moves to Issues #1469 already records where they are.
- A fleet-wide
handofflabel rollout to the carriers, which is Name the Label a Repository Reserves for Parked Decisions Where a Carrier Can Read It #1434's problem and is fixed once for every label rather than once per label. - Any automation that writes a handoff without a session asking for one.
Activity
ptr727 commentedon Sep 9, 2026
One risk measured while filing this issue, rather than predicted.
The
gh-write-guardhook refused the first attempt to write this issue body to a scratch file, because the body text contained a repository placeholder in the two-segment shape the guard reads as a cross-owner target. It reported the placeholder as an owner outside this checkout's origin and stopped the command. The body was rewritten to name the target repository in prose instead, which is why the subcommand list carries a required flag stated once rather than repeated per line.This generalises past the filing. Both the script's help text and a handoff issue body will naturally want to write a repository placeholder, and every command that carries one through a shell is subject to the same refusal. Two consequences for the design:
handoff.pystates its target-repository argument in a form the guard does not read as a target, and its own tests do the same.This is the same class as the risk already listed, a guard refusal indistinguishable from a malformed command, and it is the first one confirmed rather than suspected.
Move the Session Handoff to a Chain of Labeled Issues (#1501)
ptr727 commentedon Sep 10, 2026
Maintainer's decision, 2026-09-10: this closes with the #1489 promotion.
Closes #1470goes on that pull request.Recorded here because two of the seven acceptance items above are not met as literally written, and a closing ref that does not say why reads as an oversight later.
adoptputs the label and the metadata block on #1469 first. Done by hand withgh issue editinstead. The state the criterion asks for was reached and #1469 chained correctly, but the mechanism it names no longer exists:adoptwas built, reviewed, and cut. A randomised search over reachable stores found 60 distinct sequences that fork or orphan a track while every command exits 0, in 20000 trials, and every one of them requiredadopt; two controls restricted tonewandlinkfound none in 400 trials and none in a further 150. #1499 carries the reasoning, the measurement, and the four questions a rebuild must answer first.chain --grepfinds a string only in a closed handoff two links back, demonstrated rather than asserted. The live chain is two links, #1502 to #1469, so the closed link is one back rather than two. One back is demonstrated live (1 of 2 links walked match), and two back is covered bytest_grep_finds_a_string_only_in_a_closed_link_two_back, which is an assertion where this criterion asks for a demonstration. It self-resolves on the next handoff round, when the chain reaches three links.Everything else in Acceptance is met live against this repository, and the five sections of Scope other than
adoptshipped whole.Closing rather than holding it open for
adopt, because #1499 already tracks that work with its own acceptance, and an issue kept open solely for work another issue tracks is a duplicate rather than a record.Promote the Host-Tools Carry Rule, the Multi-Interpreter Validator Le…