Development record
sessions-announce-what-they-are-doing
Summary
This spec delivers itd-2609151838312703. Sessions under any account, on any machine or in a hosted cloud session post short fixed-form announcements on the shared remote. A person reads everyone's status with one command. A release cut holds every merge through a reservation that the land step reads.
Precondition: the cloud probe. Before step 1 starts, the product thinker runs the prompt the planning session wrote (decision 8). It records two things:
- whether a hosted session's push to a branch outside
claude/is accepted; - which forge identity the push arrives as.
A refused push sends the branch prefix back to the product thinker. An app identity means cloud announcements show as claimed (C17); nothing else changes.
Approach. One canonical primitive: announcements reuse the reservation code in internal/core/claim rather than copying it (one-canonical-primitive). The design review of 2026-10-15 found that seven parts of that code must generalise:
fetchtakes a namespace instead of the fixed reservation refspec, and keeps a fetched-at stamp per namespace.parseMessagesplits into one trailer collector plus a validator per family. The announcement item grammar is a record id or a pull-request number.parseTiptakes the parser.writeCommittakes a message interface.checkMessageloses its reservation wording.pushArgsandrefNametake a full ref.- A bounded history walk reads the commits added since this checkout last read a branch.
The reservation's lostRace refusal does not carry over: an announcement's lost race is rebuilt on the new tip and retried (C2). Its terminalOnDefault check is the wrong way round for C4. C4 instead asks whether the record is present on the default branch as last fetched, through peers.FoldersAt, and refuses when no default branch was fetched.
Shape on the remote. Each person has one branch, abcd-status/<holder id> (decisions 9, 10 and 20; a branch named by a lower-case username, made before the holder id, is still read) (revised 2026-10-29 by the privacy fix iss-2610290809526033: no login or name of any person is pushed, so the holder id replaced the username). Each announcement is one empty-tree commit carrying validated trailers:
- the kind, one of
merging,merged,asking,answered,handed-over; - the item;
- the sender's holder id;
- the working copy;
- the time sent;
- the expiry, at most 24 hours.
Nothing else is carried. The commit is authored as the person, as reservations are.
When abcd touches the network. Decision 14 and invariant 7 settle this:
abcd announcelists origin's status branches, fetches the default branch and each listed branch whose name is a holder id or a lower-case GitHub username by its own refspec, then pushes.- Amended 2026-10-16 in the step 2 review: this line said the sender's own status branch. A fetch naming one branch fails while origin holds none, which is the case at a person's first announcement, so every status branch origin lists is fetched; a later change in the same review fetches each by name rather than by a pattern, so a branch whose name differs only in case cannot stop the fetch on a case-insensitive disk, as invariant 7 states.
abcd statusfetches every status branch, the reservations and the default branch, then reads the forge's activity record for each new update (C17).- The land step fetches the reservations before it judges a release hold (C7).
- The prompt hook only renders the last completed fetch from disk, with its age (C12), and never touches the network.
How each criterion is met:
- C1, C2:
claim.Announcewrites the commit through the generalised writer and pushes with one plain push. A rejected push is rebuilt on the fetched tip and pushed again, up to three times; after that the sender sees "not sent" and the reason. Nothing is ever force-pushed. - C3: the validator refuses an unknown kind, a malformed item, any text outside the trailers, and an expiry over 24 hours. Each refusal names the field and the limit, and nothing is pushed.
- C4, C5:
- C4: a record absent from the default branch as last fetched is refused before the push.
- C5: a record the embargo store holds is refused through
abcd embargo check. A store that exists but cannot be checked also refuses. - Dependency: C5 waits for the embargo work to reach the default branch (scope condition).
- C6, C9, C18: the reader keeps per-checkout state in the local tier:
- which announcements this checkout has shown, keyed by commit;
- when each was first fetched.
The prompt hook renders each unshown, unexpired announcement once. Rendering takes only the validated fields, through termsafe (invariant 13), so the commit's raw text is never shown. Each rendered announcement ends with the line that it informs and grants nothing. An announcement's life is counted from its first fetch, not from the sender's clock. The hook shows at most ten lines, the newest per item, then one more line with a count and the command that lists the rest.
- C7, C8:
- The subject: the reservation subject grammar gains a release subject (
rel-vX.Y.Z). It needs no footprint, sits outside the live cap, and ends when the tag is on the remote or the holder releases it. - C7: the land fence (
internal/core/implement/loop/reservation.go,landFence) refuses while any other holder's release reservation is live, naming the holder and the release. - C8:
abcd statusand the prompt hook show the hold line. - The issue: this resolves iss-2609091037191879.
- The subject: the reservation subject grammar gains a release subject (
- C10: a failed fetch writes its time and reason to the local tier. The hook then shows "status unread since <time>: <reason>". An empty list is shown only after a fetch that succeeded. A checkout with no
originshows nothing.- Decided 2026-10-16 in step 3's build (build note 19): every fetch keeps its failure per namespace, and clears it when that namespace's fetch next succeeds. The hook says "status unread since 09:12 UTC" and "reservations unread since 09:12 UTC" each on its own, the time being that namespace's last successful fetch, in UTC. A namespace never read here says "never read here", with the time the fetch failed. Refined in step 3's fix round 2: the hook gives the reason in fixed words, "the last fetch failed;
abcd statussays why", since git's text can carry a remote's words; onlyabcd statusshows git's reason.
- Decided 2026-10-16 in step 3's build (build note 19): every fetch keeps its failure per namespace, and clears it when that namespace's fetch next succeeds. The hook says "status unread since 09:12 UTC" and "reservations unread since 09:12 UTC" each on its own, the time being that namespace's last successful fetch, in UTC. A namespace never read here says "never read here", with the time the fetch failed. Refined in step 3's fix round 2: the hook gives the reason in fixed words, "the last fetch failed;
- C11: each of these is shown, and every other branch is still read:
- a commit whose sender trailer is not the branch's owner, marked doubtful with the reason;
- a non-fast-forward tip relative to the last read, marked doubtful with the reason;
- a branch name that is neither a holder id nor a lower-case username, counted and never named (amended in step 3's fix rounds: whoever pushed the branch chose the name, so no reader shows it or keeps it).
- Decided 2026-10-16 in step 3's build (build notes 12, 24 and 25): a commit that does not read as an announcement, one whose tree is not the empty tree among them, is shown as doubtful by its branch and the reason, and none of its text is shown. A walk from a mark that returns More, Merged or Rewritten marks the branch "continuity not verified", and the announcements it added are still read; so does a first read, with no mark, that returns More (decided 2026-10-16 in step 3's fix round, dcn-2610161100416718), since a first read stopped at the walk's limit could bury a live announcement under ended ones, whatever times its commits carry: within the bounded walk no signal settles whether a line older than what the read took in can still stand (decided in step 3's fix round 3, dcn-2610161737571276, refining dcn-2610161320457650, whose narrowing to reads whose oldest commit was recent it replaces); the shown-set keeps a repeat from showing twice.
abcd statuslists the status branches on origin first and fetches only the names that are lower-case usernames, each by its own refspec, so a case-colliding twin never breaks the fetch; every other name is counted, never named and never kept: the listing keeps how many there were and a digest of them, and a reader that cannot read that count back says it is unknown, never zero (step 3's fix round 2).
- C9, a first read's history: an announcement whose own expiry passed more than an hour before this checkout first fetched it is history a first read brought in, not news, and is not counted anew from that fetch. A sender's clock up to an hour slow is still counted from the first fetch (decided 2026-10-16 in step 3's build).
- C12: the hook reads only the local tier. A test with a stalled remote proves it returns within one second. One intake takes in at most 100 new commits across every status branch, the newest of each branch first, and a branch with more is left unmarked so the next intake takes in the rest (refined in the step 3 round-4 review: a mark at the tip passed the rest over for good); once nothing is left, the branches after it are not walked, every branch left is counted in the block as not read yet, never as doubtful, and the next intake starts at the branch after where this one stopped, so a branch that keeps gaining commits holds no other back (decided in the same round, dcn-2610161907315656, refining dcn-2610161737571276); what an intake keeps (each commit read, each doubt, each branch's read mark) is written without a flush to stable storage, since its loss costs only a read made again. So the first intake of a flood fits the hook's deadline with flushing on (decided 2026-10-16 in step 3's fix round 3).
- Decided 2026-10-16 in step 3's build (build notes 21 to 23): the status block is read under a 600 ms deadline inside the hook's one-second budget. The walk and the batch read of new commits run under that context, and the read runs beside a timer, so a slow read never holds the prompt. When the deadline passes first, or the read fails, the block is one line, "status unavailable", with the reason, never nothing. Refined in step 3's fix round 2: the reason is in fixed words, one for a read that ran out of time and one for a read that failed, and git's own text goes to stderr only; the hook sets its one-second deadline when it starts, and the status read takes what the drain, the rules and the memory notes leave of it, less 150 ms for writing and marking the block, at most 600 ms; the render, the read of what was shown, the prune and the marking all run inside that deadline. The block is appended to the injected text after the rules and the memory notes, before the session title the session-names change sets, so both emit paths carry it.
- Decided the same day: the block joins the rules loader's error path. A
rules.jsonthat will not load returns early, and the status block is emitted there too: in plain form as the whole output, and in the--jsonenvelope astextbesideerror. The two subsystems share the hook, and neither may switch the other off, as the transcript drain already holds. When session names lands, that path's title, when one is owed, carries the block as its additional context. - Decided the same day: where the block is rendered. The render lives with the reader in
internal/core/claim(PromptNotes), not ininternal/core/rules. The rules package knows nothing of the status branches, and the hook joins the two texts at its one emit point. The render and stalled-remote tests are ininternal/core/claimand in the hook's surface test.
- C13, C14:
.githooks/pre-pushgains a second receipt-less row,abcd-status/. It reusesreservation_shape: a fast-forward of commits that change no file. Every other push under the prefix is gated as any branch is. - C15:
- The ruleset:
.abcd/work/rulesets/status-protection.jsonmirrors a ruleset onrefs/heads/abcd-status/**that blocks deletion and non-fast-forward pushes with no bypass. It is applied by the person with the rulesets README'sgh apicommand, as every ruleset in the folder is: invariant 10 lets abcd change a remote only through a dedicated, confirmed apply verb, and none exists for rulesets. It went live on 2026-10-17 as ruleset 25302453, and the mirror matched it on 2026-10-22. - The ADR: adr-2610150729333337 is the trust rule.
- The brief: invariants 7 and 10 gain clauses naming
abcd announce,abcd statusand the status branch, written in the change that builds them.
- The ruleset:
- C16, C17:
- C16:
abcd statuslists announcements newest first, with sender, kind, item, time and time left. Beside them it lists who holds which reservation, and any release being cut.--jsongives the same. - C17: each new update's pusher is read from the forge's activity record (
GET /repos/{owner}/{repo}/activity?ref=). On a branch named by a username, the sender shows as verified when the record names the branch's owner, doubtful when it names someone else, and claimed when the forge cannot say. On a branch named by a holder id the forge is not asked and the sender shows as claimed, since an id names no account the record could match (revised 2026-10-29 by the privacy fix iss-2610290809526033: no login or name of any person is pushed, so the holder id replaced the username). Claimed is the native default; the forge lookup is the adapter (invariant 2). A record whose page is not full but names no push to the tip this checkout holds is one the forge has not caught up on, and is read as a record not read in full: nothing it leaves open is kept, and the next fetch asks again (decided in step 3's fix round 4, dcn-2610161907326838).
- C16:
Surfaces. abcd announce joins the agents' command set beside abcd reserve. abcd status joins the people's set. Each has a plugin command page, so it is reachable from both the CLI and the plugin markdown surface.
Out of scope here. These are captured, not built:
abcd peerskeeps its name and its behaviour (iss-2610150726331622);- the status board is renamed the dashboard (iss-2610150726321422).
Footprint
- packages: internal/core/claim, internal/core/implement/loop, internal/core/rules, internal/surface/cli, .githooks, .abcd/work/rulesets, commands
- tests: internal/core/claim (announce, read, expiry, flood, doubtful-branch and race tests), internal/core/implement/loop (release land-fence test), internal/core/rules (prompt-hook render and stalled-remote test), the pre-push hook's receipt-less shape test, smoke (announce and status end to end against a local bare remote)
Steps
- Generalise the reservation primitive (namespace fetch, trailer collector with per-family validators, message interface, full-ref push, bounded history walk), with no change in reservation behaviour
- packages: internal/core/claim
- tests: internal/core/claim (every existing reservation test unchanged and passing)
- Announcements and
abcd announce(C1 to C5, C13, C14), the receipt-less row, the ruleset mirror and the ADR, with the invariant 7 and 10 clauses- packages: internal/core/claim, internal/surface/cli, .githooks, .abcd/work/rulesets, commands
- tests: internal/core/claim (announce, validation and race tests), the pre-push shape test
- Reading:
abcd statusand the prompt hook (C6, C9 to C12, C16 to C18)- packages: internal/core/claim, internal/core/rules, internal/surface/cli, commands
- tests: internal/core/claim (read, expiry, flood, doubtful-branch tests), internal/core/rules (render and stalled-remote tests), smoke
- The release hold (C7, C8)
- packages: internal/core/claim, internal/core/implement/loop
- tests: internal/core/claim (release subject tests), internal/core/implement/loop (land-fence test)
Frontmatter
| id | spc-2610150728396006 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610150728396006-sessions-announce-what-they-are-doing.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609151838312703Sessions under any account, on any machine or in the cloud announce what they are doing through short fixed-form announcements on the shared remote, a person reads everyone's status with one command, and what is really a hold stays a reservationimplements
mentions20
- adr-2610150729333337A status branch moves without a preflight receipt
- dcn-26101611004167182026-10-16 — A status branch this checkout has never read that holds more than one read covers is shown as doubtful, continuity not verified: more was added than one read covers, the same as a branch read from a mark (spc-2610150728396006 step 3 fix round, build note 24 extended; decided by the facilitator on delegation). A first read that stopped at the walk limit could otherwise bury a live announcement under a thousand ended ones with no sign.
- dcn-26101613204576502026-10-16 — A first read of a status branch that runs past the walk limit is shown as doubtful only while something beneath what it read could still stand: when the oldest commit it read does not read as an announcement, or was sent, by the time it carries, less than a day and an hour before the read (the longest life plus C9's one-hour slack), refining dcn-2610161100416718 (spc-2610150728396006 step 3 fix round 2, decided by the facilitator on delegation). Status branches only grow, so every new checkout's first read of a long-lived one passed the limit and showed a doubt for history that had all ended. The bound is the oldest commit's send time rather than its own expiry, since a commit beneath it, sent earlier with a longer life, can outlast it.
- dcn-26101617375712762026-10-16 — A first read of a status branch that runs past the walk limit is always shown as doubtful, continuity not verified, whatever times its commits carry, refining dcn-2610161320457650 and dcn-2610161100416718 (spc-2610150728396006 step 3 fix round 3, on review). Within the bounded walk no signal settles whether a line older than what the read took in can still stand, so the read cannot say that nothing beneath it is live.
- dcn-26101619073156562026-10-16 — A status branch holding more new commits than one intake takes in is no longer shown as doubtful: it is left unmarked, the branches after it are not walked once nothing is left to take in, the block counts each branch left as not read yet, and the next intake starts at the branch after where this one stopped; continuity not verified stays only for a walk that itself ran out, refining dcn-2610161737571276 (spc-2610150728396006 step 3 fix round 4, on review). A read mark at the tip passed the commits the cap left over for good, and a lasting doubt told the person nothing they could act on.
- dcn-26101619073268382026-10-16 — A forge record of pushes whose page is not full but that names no push to the tip this checkout holds is read as a record not read in full: nothing it leaves open is kept, and the next fetch asks again (spc-2610150728396006 step 3 fix round 4, on review). The forge records a push a little after it lands, so such a record is one that has not caught up yet, and keeping no pusher for those commits would have left them claimed for good.
- dcn-26101619490324582026-10-16 — The status intake keeps every branch it left unread, each with the turn it was left at, and walks them before any other branch, the one left first first; a branch walked and left again goes behind every branch already waiting, so no branch waits behind more than the branches left before it. Once the cap has cut a branch short, it takes half its share from its newest unread commits and the rest from its oldest, and its read mark moves up to the newest commit beneath which every commit is read, so every unread commit is read within a bounded number of intakes. The forge is asked about the branch waiting longest first, and each branch asked goes behind every branch still waiting; a tip whose newest covering push came from an app is kept as asked until the tip moves, so every branch is asked within a bounded number of fetches. This refines dcn-2610161907315656 (spc-2610150728396006 step 3 fix round 5, on review): one place to start from, a newest-first read of each branch and a fixed newest-first order of asking each let a branch that keeps gaining commits keep other announcements unread or unasked for good.
- dcn-26101620324462862026-10-16 — The status intake splits a branch's share between its newest and its oldest unread commits whenever the last intake left the branch unread: one the cap cut short in a walk, and one left without a walk once the cap was spent. The newest side takes the rounded-down half and the oldest the rest, so a share of one takes the oldest unread commit and every walk of a branch left moves its read mark. No tip is kept as asked because an app pushed it: a branch leaves the forge's queue once every standing announcement on it has a verdict, so an announcement an intake reads later at the same tip is still asked about. This refines dcn-2610161949032458 (spc-2610150728396006 step 3, on review), which named only a branch the cap cut short and kept a tip an app pushed as asked until it moved.
- iss-2609091037191879A release cut in progress is invisible to every other session, so a change landing on main during a cut is discovered in the changelog rather than chosen. abcd should let launch declare a cut lease that peer sessions, merges and record closes can see: a session about to merge or close a record during a lease is told which cut it would join and who is cutting, and the cutting session rules on the ordering. The convention is in AGENTS.md (concurrent sessions); this seeds the mechanical form, alongside session presence (iss-2608220750029993) and itd-33.
- iss-2610110918418459The implement loop's landing never writes a spec step's '- landed:' line, so a spec the loop landed part of still reads those steps as unlanded: a later 'spec close --remainder' carries steps that already merged into the remainder, and a fresh 'abcd build' of the same spec opens a lane for them again. Found by the fidelity audit of itd-2609212103565953 (receipt rcp-2257cc3a38d9, criterion ac-3 MET_WITH_CONCERNS); spc-2609202134338445's Progress already says the landing does not mark a non-closing lane's step.
- iss-2610150726321422The board bare abcd prints is called the status board, but it is the dashboard in the terminal: abcd dashboard already serves the same view to a browser over Tailscale, and the board is moving to that HTML form. Two names for one thing, and the word status is now wanted for abcd status, the people's view of what other sessions announce and hold (itd-2609151838312703 decision 21). The product thinker ruled on 2026-10-15 that the status board should be renamed the dashboard. About 14 files say status board: README.md, commands/abcd.md, docs/how-to/install.md, docs/reference/cli/commands.md and the brief.
- iss-2610150726331622abcd peers does two unrelated things: it lists records that sibling worktrees and local branches hold that this checkout does not (an agent's duplicate check before filing), and it fetches and lists the reservations on the shared remote. The second duplicates abcd status from itd-2609151838312703, the people's view of what everyone else is doing. The product thinker ruled on 2026-10-15 that peers keeps its name for its future meaning, which sessions are running now, local first and later over a private network such as Tailscale or WireGuard (the session register itd-2609150819440345), and that the local duplicate check belongs under abcd status --local.
- iss-2610171626398856The v0.15.1 release review found at CHANGELOG.md:19-72: INCOMPLETE (judgement call). The 0.15.1 section has no Added line for user-facing surfaces that are new since v0.15.0 and present in this binary. (1) `abcd status`, a top-level verb in `abcd --help` with a user-invocable commands/status.md (block: people); commands/status.md does not exist at v0.15.0. (2) `abcd announce` (commands/announce.md). Both come from the planned itd-2609151838312703 and its open spc-2610150728396006. (3) `abcd implement load --race-gate` and the `load.preflights` / ABCD_LOAD_PREFLIGHTS setting (internal/core/implement/load_slot…
- iss-2610222040438015spc-2610150728396006's C15 says the status-branch ruleset (.abcd/work/rulesets/status-protection.json) is applied through the rulesets' own apply verb (invariant 10), but abcd has no such verb: the ruleset was applied by hand on 2026-10-17 with gh api (it is live as ruleset 25302453), the way the rulesets README tells a person to; so the criterion names a mechanism that does not exist and cannot be checked as written
- iss-2610262038222403The brief index says spc-N references trace a predecessor implementation's spec store; the status chapter cites spc-2610150728396006 as this repository's design record, by step.
- iss-2610262038278710The glossary defines a step as a landable piece of a spec; the land chapter uses "step" for its own gate, push and enqueue acts while also citing a spec's step.
- iss-2610290809526033Reservation and announcement commits that abcd pushes to origin name the holder by their forge login: the Abcd-Holder and Abcd-Sender trailers carry it, and the announcement branch is named after it, so every reservation publishes which account made it in text and in the branch list.
- itd-2610120738075547Work handed to a cloud session comes back, or says why not
- spc-2610120941441282a-person-hands-an-item-from-their
- spc-2610161044461448drain-many-accounts