This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

after-update

specclosedspc-2610170635097509

Summary

This spec delivers the bundle after-update, two intents met in one moment of the agent's first reply after an abcd update:

  • itd-2610161617096084: abcd says what changed, one line per release, with the full summary one command away;
  • itd-2610161616086564: abcd offers, once per update per repository, to run setup again when the update brings something the repository needs.

The two intents' ## Decisions are binding and are not repeated here, except where a step names one. In short: the session-start hook adds one fixed line and writes nothing; after the person's first message the agent runs one abcd verb and relays what it prints, first the summary line per release, then the offer; the person's answer is recorded by a second run of that verb, in abcd's own data directory, keyed on the repository's root commit and the release.

Approach. Five parts, each on code that already exists.

  • The digest. abcd launch ship already composes, for each release, one changelog line per shipped record and proves the lines cite exactly the cut (internal/core/release, Ingest). The cut gains a fourth write beside the archive, the page and the CHANGELOG heading: a digest, a JSON file holding, per release, its tag, the tag it was cut from (Cut.BaseTag) and each line as composed, with the record ids it cites and their impact. The file lives in a Go package, internal/core/releasedigest, which embeds it with //go:embed, so the binary built from the release's tag carries every release up to and including its own. The .abcd/ namespace never reaches a released binary (AGENTS.md, Boundaries), so the digest cannot live there.
  • Release versions, strictly. One parser, launch.ParseReleaseTag, accepts exactly vMAJOR.MINOR.PATCH (digits only, no leading zero, no pre-release or build part) and orders through the existing launch.Semver/CoreGreater. Every version this bundle reads (the running binary's core.Version, the repository's meta.setup_version, the binary record's previous_tag, a digest tag) passes it or takes no part. embargo's private copy of the same pattern calls it (one-canonical-primitive).
  • What waits on this repository. A new ahoy.Waiting(cwd, from, to) returns the questions setup would ask here that were never answered, leaving out those whose latest setup answers record says later (the "decide later" value interview.Write already records in .abcd/.work.local/interviews/), plus the settings whose default changed in a release after from up to to and which this repository takes from the default. The default changes come from a table embedded in ahoy, each entry naming a setup key and the record that changed its default; the digest says which release shipped that record.
  • The record of what was shown and answered. A new core package, internal/core/afterupdate, composes the moment (the summary lines and the offer) from the digest, ahoy.Waiting and its own store under ~/.abcd.noindex/after-update/, and owns that store: one file per repository and release holding the answer, and one per computer holding the newest release whose general news was shown. Only the verb writes it.
  • The front doors. abcd update news prints the moment bare, the full summary with --full, and records the answer with --answer yes|not-now (or that the summary was read, with --seen, when no offer was made). abcd ahoy install --only-waiting is the setup the yes runs: it asks only the waiting set. The session-start hook, which already runs ahoy.Detect, asks afterupdate.Pending (read-only) and, when anything is pending, prints one constant sentence to stdout. Both verbs are wired on the CLI and on their command pages, commands/update.md and commands/ahoy.md.

The moment, as the agent relays it. For Alice, set up with v0.15.0 and now running v0.16.0, with two questions waiting:

abcd v0.16.0: 9 changes, 1 needs you to act (itd-N). Ask for details: abcd update news --full
abcd was updated from v0.15.0 to v0.16.0 since this repository was set up; 2 setup questions wait here. Run setup now? (yes / not now)

The first line is the general news, once per computer; the second is what waits here, once per repository. Neither carries a word from the repository's tracked files: versions pass ParseReleaseTag, the counts are numbers, and the line text comes only from the digest inside the binary. With --full, each release in the range is named, its changes that need action first, each as its digest line with the record id to read more, then the waiting questions' and changed settings' titles.

Settled here

The intents leave these technical choices open. Each is decided on the grounds given; none changes a criterion.

  • The digest's path is declared, not fixed. launch ship writes it to the repository-relative path the repository's .abcd/config.json declares under release.digest (the release namespace claimed through internal/core/layered, read from the repository layer only), and writes no digest where none is declared. abcd's own config declares internal/core/releasedigest/digest.json. Grounds: managed repositories replicate abcd's path, so the cut carries no abcd-only case; go:embed reaches only files under the embedding package's directory, and .abcd/ is denied to released binaries.
  • The digest is JSON with a schema version, read strictly. Read through jsonstrict (unknown fields, duplicate keys and trailing content refused), capped in size, each line passed through termsafe.Sanitize again on read. Grounds: the same posture as every other embedded default (rules.json, guard.json), and the intent's "sanitised as every other text abcd reads at run time".
  • Each digest entry is one changelog line, with every record it cites. A line that cites two records is stored once with both ids, not copied per record. Grounds: the line is the one source (scope condition cond-2610170635096007), and two copies of one line would show twice.
  • A gap in the chain is found, never guessed. Each release stores the tag it was cut from, so walking down from the running release finds every release in the range; a release the chain does not reach, or a range that starts below the first described release, is named as not described, with the release notes' published address (<releaseOrigin>/releases/tag/<tag>, the origin internal/core/update already pins), and the summary says it is partial. Grounds: the intent's "never fills the gap with a guess", and a derived link needs no fetch.
  • The digest is seeded from the derived CHANGELOG sections. The first digest carries every release from v0.7.0 on, whose sections launch ship derived from records and whose lines cite record ids, each line copied from CHANGELOG.md and its impact read from the cited record. Releases rolled by hand (v0.6.9 and earlier) are named as not described. A test holds every digest line equal to its CHANGELOG line. Grounds: scope condition cond-2610170635090370 offers either treatment of hand-rolled sections; naming them is the one that cannot misstate, and the seed lets an update from any recent release summarise in full on the first shipped digest.
  • "Needs you to act" means a breaking record. An entry needs action when any record it cites has impact: breaking, the one field the cut already carries per record (release.Entry.Impact). Grounds: the changelog's own rule states a break as an Added line, so the impact, not the section, is the signal; nothing new is asked of the composer.
  • The default-change table names a record, not a release. Each entry of internal/core/ahoy/defaults/default-changes.json is a setup key and the itd-/iss- record that changed its default; the release comes from the digest. A test refuses an entry whose key is not a setup question key (SetupFixedValues or helpFor answers it). Grounds: versions are derived at the cut (the derived changelog), so a release written by hand when a default changes would be a guess; a record not yet in a digest (a dev build) counts in no release.
  • A changed default counts only where the repository takes the default. A key the repository's layer sets is unaffected by its default, so it waits on nothing. Grounds: the product thinker's "offered only when something is required".
  • "Decide later" is read from the setup answers records of this checkout. A new interview.Latest(place, name) returns, per question id, the newest recorded value across the records of one interview. A checkout with no records (a fresh clone or worktree) counts every unset question as never answered, and so errs towards offering. Grounds: one source, the record setup already writes; the error costs one "not now" and never a missed question.
  • The store is abcd's user-level home, keyed as its other stores are. ~/.abcd.noindex/after-update/<root-sha>/<tag>.json holds a repository's answer for one release, created exclusively (O_CREATE|O_EXCL through fsutil's guarded home-scope maker), so of two sessions answering together the first wins; ~/.abcd.noindex/after-update/news-seen holds the newest release whose general news this computer was shown, replaced atomically and only ever raised. Grounds: the worktree and transcript stores key on the root commit already (adr-2610031751065746); the user-level home is host-agnostic where the plugin's data directory is not; per-user state stays out of the repository.
  • Where the general news starts. The range starts at news-seen, else the binary record's previous_tag (read as ahoy.TakeUnseenUpdate reads it, without its claim), else the repository's setup_version, else the running release alone; each candidate passes ParseReleaseTag or is skipped. Grounds: the newest fact on disk about what this computer last knew wins, and each fallback is the intent's named source (scope condition cond-2610170635095099).
  • version.upgrade gains a direction. It is reported only when the running release is newer than setup_version, and its fix hint names abcd ahoy install --only-waiting; stepVersionStamp never writes a version older than the one recorded. Grounds: Carol's criterion, and a comparison the gap already makes by string equality only.
  • The hook's line is one constant. It carries no version and no count, and reads: "abcd: an abcd update has news for this session; after the person's first message, run abcd update news and relay what it prints at the start of your first reply." Grounds: the fixed-text rule of the session-start stdout (iss-2608241115201044); the verb's output reaches context by the agent's own tool call, the channel every abcd verb already uses, and that output carries only numbers, checked tags and digest text.
  • A session with no person records nothing. The hook writes nothing, and bare update news and --full write nothing; only --answer and --seen write, and commands/update.md tells the agent to run them only with a person's reply. Grounds: the binary cannot tell a headless agent from one with a person, so the record is tied to the act a headless run never performs, the person's answer.
  • Amended 2026-10-17 by the agent, building step 1: a release tag's components are bounded at nine digits, and the refusal never echoes the value. launch.ParseReleaseTag accepts each component as 0 or one to nine digits without a leading zero, so every component fits an int32 on any platform and a whole tag is at most 30 bytes; its error names the form, never the value. Grounds: the spec asks for bounded components without a bound; nine digits is far above any release and keeps the value from a tracked file short, and a parser whose input is often a tracked file must not hand that text to a caller that relays its error.
  • Amended 2026-10-17 by the agent, building step 1: the stamp keeps a recorded release against any running version that is not a release at least as new. stepVersionStamp writes the running version unless the recorded value is a release tag and the running one is older or not a release tag at all (a dev build, a pre-release); a recorded value that is not a release tag is replaced as before. Grounds: "never records a version older than the one recorded" cannot be judged against a dev build, and writing dev over a teammate's committed release is the downgrade the criterion forbids.
  • Amended 2026-10-17 by the agent, building step 2: the digest's shape. {"schema_version": 1, "releases": [...]}, releases newest first, each {"tag", "base_tag", "entries"}, each entry {"line", "records": [{"id", "impact"}]}, where line is the composed wording without the citation suffix the cut appends and the entries keep the CHANGELOG section's order. base_tag is "" only for a repository's first cut. Caps: 8 MiB per document and 4096 releases, and per release the cut's own caps (500 entries, 32 records an entry, 4096-byte lines, 32-byte ids). The file is written in one canonical form (two-space indent, HTML characters unescaped), so an append changes only the release it adds, and a line the reader's sanitising would change is refused at the append rather than stored changed. Grounds: the spec names the fields, not their form; the canonical form keeps a cut's diff to its own release, and storing lines byte-equal to the CHANGELOG is what the seed test holds.
  • Amended 2026-10-17 by the agent, building step 2: a gap names a span, not a guessed list. The walk stops at the first release the chain reaches that the digest does not carry, and the gap reads "the releases after <from> through <that tag>", with that tag's notes address and the release list's (<releaseOrigin>/releases). A from of "" is the running release alone, and a from at or above the running release is an empty range. Grounds: below a missing release the chain cannot say which releases exist, so naming any one of them would be the guess the intent forbids.
  • Amended 2026-10-17 by the agent, building step 2: the release origin moved to core.ReleaseOrigin. internal/core/update now reads its releaseOrigin from the leaf package internal/core, and the digest names notes addresses from the same constant. This touches internal/core and internal/core/update, outside the footprint. Grounds: update depends on ahoy, and ahoy will read the digest (steps 4 and 5), so the digest cannot import update; a second copy of the origin would break one-canonical-primitive.
  • Amended 2026-10-17 by the agent, building step 2: the seed is generated once and held by tests, not by a kept generator. A one-off script copied the seventeen derived sections (v0.7.0 to v0.15.0) and read each cited record's impact from its terminal folder; tests hold every line equal to its CHANGELOG line, every base tag to the release below it, every impact to its record's, and the file to its canonical form. Grounds: from the next cut on the cut itself appends, so a generator would be dead code.
  • Amended 2026-10-17 by the agent, building step 3: what the declared path may be, and where the write goes. Besides a path outside the tree, under .abcd/ or not ending in .json, the cut refuses a path under .git/, one with a backslash, a dot or empty segment, or longer than 512 bytes, and compares the first segment without case (.ABCD/ is .abcd/ on a case-insensitive filesystem). The path's directory must already be a directory inside the tree; the cut creates none. The write goes through an os.Root on the repository root (atomic, keeping the file's mode), so a symlinked directory cannot carry it outside, and a rollback restores the file's bytes or removes the file the cut created. The repository layer alone is read by loading the configuration with no machine layer (the landing's precedent for a branch's own checkout), so a machine's release.digest is never read rather than refused. Grounds: .git/ is not the tree; a created directory would be one more thing to undo; the write path is a value a repository commits, so it is contained the way the archive's create already is.
  • Amended 2026-10-17 by the agent, building step 3: checked early, composed late. The declared path and the file it appends to (a present digest must parse) are checked with the cut's other repository preconditions, before the payload's verdict; the appended bytes are composed only once the payload is accepted, so a payload fault is reported as itself and never masked by a digest refusal. Each cited record's impact is the one the cut carries for it (Cut.Added/Cut.Removed). Grounds: the ingest's existing rule that a repository fault is a stop found before the payload's verdict, and the composer's retry loop reading only payload reasons.
  • Amended 2026-10-17 by the agent, building step 4: a default change names only a key whose setting .abcd/config.json holds. Besides a key that is not a setup question key and a malformed record id, the table refuses a setup key whose answer the repository's layer does not keep in .abcd/config.json (today visibility, docs_target, oracle_backend and scan_deep, the four the install collects), an entry listed twice, and another schema; it is read through jsonstrict and capped at 64 KiB. Grounds: "a changed default counts only where the repository takes the default" needs a place to see whether the repository set the key, and a key with no such place (a status line element, an approval) could only be guessed at; a key gains a slot in the same change that first lists it.
  • Amended 2026-10-17 by the agent, building step 5: a recorded answer of any value is an answer, and only questions wait. A question waits when the setup records hold no answer to it at all; "answered with a value or deferred with later" are both a record, so interview.Latest's value is read only for its presence. A machine-wide question (ahoy.SetupMachineWide) is answered by the home's records, the rest by the checkout's. The questions are the ones the detection's gaps arm, in setup's order: the four config values (the oracle question only where setup asks it), the artefact kind, the house-style question, the git identity, the lineage link, the optional offers, and each tool conventions file's retirement; a follow-up (a README part, a status line element) rides on its lead question. A category approval and the adoption never wait: they are consent to a change, asked whenever there is one, not a setting answered once. Each question is titled by the gap that arms it (core's words), a changed default by its config slot, release and record. A changed default already covered by a waiting question is listed once, as the question, and a key is listed for its newest release only. A setup record that cannot be read is an error, not "nothing answered". Grounds: the product thinker's rule names questions and settings, and an approval re-asked on every run would make an offer of every lingering gap; refusing a record abcd did not write keeps a planted file from silencing a question.
  • Amended 2026-10-17 by the agent, building step 5: Waiting lands unexported and its name is shared. Step 5 adds waiting(cwd, from, to) and waitingIn(cwd, det, from, to) for a caller already holding a detection; step 6 exports both as afterupdate calls them, because the ahoy package's front-door audit refuses an exported function no production code outside it calls. The setup interview's name moves to ahoy.SetupInterview, which the CLI's record writer now names (internal/surface/cli/setup_interview.go, one line). The install test approves every category up front, as the yes to the offer does, so none is put. Grounds: the loud-staging rule the audit enforces, and one-canonical-primitive for the name a reader and a writer share.
  • Amended 2026-10-17 by the agent, building step 6: one exported ahoy.Waiting(cwd, det, from, to), and two read-only accessors. The step 5 pair became one exported function that takes the caller's detection, since the only caller holds one and ahoy's front-door audit refuses an exported wrapper nothing calls. Step 6 also adds, in internal/core/ahoy (outside its package list), SetupVersion(cwd), the recorded setup_version only when it is exactly a release tag, and PreviousReleaseTag(pluginRoot, cwd), the binary record's previous_tag read where and through the same data-directory check TakeUnseenUpdate uses, whether or not the swap was seen, writing nothing. Grounds: ahoy already owns both files' readers, and the strict parse at the reader keeps every other word of the tracked file inside ahoy.
  • Amended 2026-10-17 by the agent, building step 6: the moment's words. The functions keep the spec's names (Pending, Moment, Full, Record, MarkSeen); their results are PendingState, MomentLines and FullSummary. News lines are newest first, each "abcd <tag>: <n> changes[, <k> need(s) you to act (<ids>)]. Ask for details: abcd ahoy after-update --full", naming at most five breaking record ids and counting the rest; a gap is one line naming the span, its notes address and the release list's, and saying the summary is partial. The offer line counts questions and changed defaults separately ("2 setup questions and 1 changed default wait here"). The full summary heads each release with the release it was cut after, marks each entry needing action, cites every record of every entry, then lists what waits. Grounds: the spec's own example lines, extended only where a case it does not show (several breaking records, defaults) needs words.
  • Amended 2026-10-17 by the agent, building step 6: the store's details. An answer is {"schema_version": 1, "release", "answer"} at mode 0600; anything at its name counts as answered. news-seen holds one tag and a newline; a value that is not a release tag reads as absent and the next raise replaces it; the raise compares and writes under ~/.abcd.noindex/after-update/news-seen.lock, so two raises together never lower it. Record refuses an answer that is not yes or not-now, a running binary that is not a release, a setup version that is not an older release, and a repository with no root commit (or one that is not a full object name), before writing anything; it does not require something still to wait, so an answer given to a moment is never lost to a race. A news range starting at or above the running release is no news; with no news-seen, previous_tag or setup_version it is the running release alone. Full(cwd, "") starts at previous_tag, else setup_version, else the running release alone, never at news-seen, so the summary shows again after it was seen; what waits is listed where the setup version is an older release. Pending runs a detection only where the setup version is an older release. Grounds: an exclusive create is the first-wins rule the spec asks for, and a lock is the only way a read-compare-replace stays raise-only under concurrency.
  • Amended 2026-10-17 by the agent, building step 6: the reach audit stays red until the front doors land. afterupdate's exported functions are called only from steps 7 and 8, so internal/reachaudit's ratchet names them (and step 2's unreached releasedigest exports) until then. Grounds: the spec orders the core before its front doors; staging them unexported would make steps 7 and 8 rename the package's whole surface.
  • Amended 2026-10-17 by the agent, building step 7: what the only-waiting run approves, and what it leaves. The run wraps the install's prompter: the adoption and every category approval reach the person's door; a question Waiting returned reaches it too (a README part riding on the README question, a status line element on the status line offer); any other value question takes no answer, as a door's "decide later" does, and any other offer is declined. Every category the detection carries is approved up front, unless --yes or an explicit approval set decides them. The answers file's pre-check walks the same filter, so a question deferred with later needs no answer there. With --yes nothing is answered, and the two value questions a plain --yes settles by default, the artefact kind and the house-style question, are left unwritten with a note naming the run that asks them, so each still waits; the version stamp follows the existing rule, so such a run records the running release and the offer for it ends while abcd ahoy still lists the questions. Waiting counts no changed default where the running build is not a release. Grounds: the step 5 amendment's "the yes approves every category up front", and the criterion that --yes never answers a value question.
  • Amended 2026-10-17 by the agent, building step 7: the verb's edges. --full with --seen is a usage error too, since --full writes nothing; every usage error and every refusal of core's (ErrNotRelease, ErrNoOffer, ErrAlreadyAnswered) exits 2 saying nothing was written. Bare prints nothing when the moment holds nothing, so the agent relays nothing. An answer recorded whose news-seen raise then fails is printed and the error returned, so the answer is never reported lost. The verb lives in internal/surface/cli/update_news.go, and cli.go gains one line each for its registration, the install's flag and the flag's value. Step 2's Embedded, Marshal, NotesURL and ReleasesURL are unexported (internal/core/releasedigest, outside the step's packages), because no front door calls them and the reach audit refuses an exported function nothing outside its package reaches. Grounds: the reach audit's ratchet and the coordination asked of this branch.
  • Amended 2026-10-17 by the agent, building step 8: Pending takes the caller's detection. Pending(cwd, det) reads the detection the session start already ran and runs none of its own; Moment and Record still detect for themselves where the setup version is an older release. The hook asks only when its detection succeeded, prints the constant from the CLI (afterUpdateSessionLine, which a test holds commands/ahoy.md to quote), and reports a failure to work it out as a notice on stderr, never on stdout. The update chapter's "the session start check does not compare versions" is rewritten, since the check now reads setup_version, though it prints no version. Grounds: the step's "with the detection it already has", and the fixed-text rule of session start's stdout.
  • Ruled 2026-10-17 by the product thinker: the verb is abcd update news. The verb step 7 added as abcd ahoy after-update is renamed abcd update news, a sub-verb of abcd update, with the same flags. The first name never shipped, so no alias keeps it. Each news line now ends "Ask for details: abcd update news --full", and session start's fixed line names abcd update news. ahoy install --only-waiting keeps its name. This supersedes the verb's name, its file, its session-line constant and its page in the step 6, 7 and 8 amendments above: the verb lives in internal/surface/cli/update_news.go with the constant updateNewsSessionLine, which a test holds commands/update.md to quote; the install's flag lives in internal/surface/cli/ahoy_only_waiting.go; update.go gains the registration line, and the host steps moved from commands/ahoy.md to commands/update.md. Having a sub-verb makes abcd update a parent, so a lowercase word that is not a release tag (every release tag starts with v and a digit) is refused as an unknown command, exit 2, before anything is resolved or fetched, where it was taken as the name of a release. Grounds: the product thinker's ruling, and the sweep that holds every parent to refuse an unknown sub-verb at exit 2.
  • Amended 2026-10-17 by the agent, after step 9's live run: the fixed line names the plugin's command page and asks for the lines word for word. The live run found two gaps: the agent restated the two lines in its own words, and it ran a bare abcd, which on the person's machine is whatever is first on PATH, an older release with no update news. The constant now reads: "abcd: an abcd update has news for this session. After the person's first message, run /abcd:update news, whose page runs the abcd this plugin ships (never a bare abcd: the one on PATH can be an older release), then begin your first reply with the lines it prints, word for word, before anything else." This supersedes the line quoted under "The hook's line is one constant" above; it still carries no version and no count, and commands/update.md quotes it and tells the agent to run "${CLAUDE_PLUGIN_ROOT}/abcd" update news --json and to begin the reply with each line word for word. Grounds: every byte of the line stays a constant in the binary (iss-2608241115201044), and only the command page reaches this plugin's binary from such a line. The binary's own path from os.Executable is not a constant: the plugin root under the host's plugin cache is named for the marketplace, and a repository's tracked host settings can declare a marketplace under a name it chooses, while a plugin loaded from a directory can sit at any path, so the path is text a repository can choose. A literal ${CLAUDE_PLUGIN_ROOT} on the line would reach the agent unexpanded, since the host sets that variable for hooks and writes it into command pages at load, not into the agent's shell. The command page is where the host writes the plugin root it loaded this session, the same root whose binary printed the line, so the stale-root window iss-2609020113012227 describes does not open here, and the page's resolution ladder (iss-2608230943088357) puts that binary first. A binary naming a command page in its output already has precedent (staleusage.go: "run /abcd:library").

Known limits, stated, not claimed

  • An agent that runs --seen or --answer without a person (against the plugin page) marks the news seen or the offer answered. Nothing in the binary can tell; the plugin page is the control, and abcd update news --full still shows the summary, and abcd ahoy still lists the waiting setup.
  • A host without hooks gets no line; abcd ahoy lists the waiting setup and abcd update news shows the summary (scope condition cond-2610170635092426).
  • A plugin root whose binary has not been swapped shows nothing new: the running binary's version is the "updated to" side (scope condition cond-2610170635092640).
  • Setup asks each of its four config values in turn and saves every one answered. A value deferred with "decide later" stays unset in .abcd/config.json and is listed as remaining, so the run is partial and abcd ahoy still lists it; the values answered beside it, before or after, are saved. A full install and the only-waiting run behave the same way.
  • An agent that ignores the session line and runs abcd update news through an older release on PATH (v0.6.2 to v0.15.0, whose update [tag] takes one positional tag) does not get the news. news passes those releases' tag shape (^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$), and no confirmation is asked for a named tag. A plugin-root binary, the dev shim and a package-manager install are refused before anything is fetched; a PATH-installed copy goes on to fetch releases/download/news/checksums.txt from the release origin, gets a 404, and fails with "release news has no checksums.txt", having downloaded no binary and swapped nothing. It would download and swap only if the origin ever published a release tagged news whose binary matched that release's checksums.txt (and, from v0.14.1, passed the provenance check bound to release.yml on main); checked 2026-10-17, intentdriven/abcd has no such tag or release, so a release must never be tagged news. Releases before v0.6.2 have no update verb and refuse it as an unknown command.

How each criterion is met

itd-2610161616086564 (the offer):

  1. The one-line offer, nothing written before the answer. Step 6 composes the offer line from setup_version, the running release and the waiting count; --full adds the titles (step 6); step 8's hook line leads the agent to it; bare update news writes nothing (step 7's test).
  2. Nothing needed, no offer. afterupdate.Pending reports no offer when ahoy.Waiting is empty (step 6); abcd ahoy keeps listing version.upgrade (step 1).
  3. Yes asks only the waiting set and records the version. ahoy install --only-waiting asks exactly ahoy.Waiting's questions and settings, never one answered with a value or deferred later, and stepVersionStamp records the running release (step 7).
  4. Not now is not repeated. --answer not-now writes the release's record (step 6); Pending reads it (step 6); abcd ahoy still lists the gap with --only-waiting in its fix hint (step 1).
  5. The verb records it, keyed on repository and release, in abcd's data directory; the hook writes nothing. Steps 6 and 8, with a test that the hook leaves the store unchanged.
  6. A new release offers again. The record is keyed on the release (step 6).
  7. No person: nothing run, nothing written; --yes never answers a value question. The hook writes nothing and only the answer writes (steps 6 and 8); a test holds that ahoy install --yes --only-waiting leaves every waiting value question unset (step 7).
  8. Matching versions or a dev build: no offer. ParseReleaseTag refuses dev, and equal versions wait on nothing (steps 1 and 6).
  9. A newer committed setup version: no offer, never a downgrade. Step 1.
  10. No network; nothing unchecked from tracked files. Every input is a file on disk or the embedded digest; setup_version passes ParseReleaseTag before it reaches any output (steps 1 and 6); the zero-network test covers the hook and the verb (step 8).

itd-2610161617096084 (the summary):

  1. One line per release, the full summary one command away, action first. Steps 2 and 6 (--full orders entries needing action first and cites ids).
  2. Skipped releases are all covered and named. The chain walk (step 2).
  3. What waits here apart from abcd's news; news once per computer, waiting once per repository. Two lines from two sources, two records (step 6).
  4. A release the digest lacks is named with where its notes are; never a guess, never presented as complete. Step 2's gap report, rendered by step 6 with partial: true in --json.
  5. No network; only the binary's digest. Steps 2 and 8.
  6. Shown once, again on request. news-seen (step 6); --full shows it again at any time and writes nothing (step 7).
  7. No person: not counted as seen. Only --answer or --seen raises news-seen (step 6).
  8. Text only from the binary's digest, sanitised. releasedigest reads only its embedded file and sanitises every line (step 2); a test runs the verb in a repository with its own CHANGELOG.md and asserts none of it appears (step 7).

Proof

Every step is test-first, each test watched fail before its change. The end-to-end proof is real, per the product thinker's rule that real runs are the proof: before the spec closes, a binary built at a test tag from this branch (with a digest carrying that tag) runs in a scratch repository set up by the previous release's binary, and a live host session's first reply is captured before and after the answer, with the store and .abcd/config.json shown before and after.

Coordination

  • Step 3 changes internal/core/release, which every cut runs through, so it lands while no cut is in flight, and the next cut is the first to write a digest.
  • Steps 7 and 8 change internal/surface/cli, whose tests can raise a keychain prompt on a person's machine; they run in CI or a fenced lane, not in a live session.
  • Every step reserves its footprint with abcd reserve before it starts.

Out of scope

  • Answering the offer from a plain Terminal outside a host session: abcd ahoy and ahoy install --only-waiting already serve it.
  • A digest for any binary but abcd's own: the cut writes one wherever a repository declares it, and only abcd's binary reads one.
  • Rewording a digest line for the summary: the line is the record's changelog line as the cut composed it.

Footprint

  • packages: internal/core/launch, internal/core/embargo, internal/core/releasedigest, internal/core/release, internal/core/ahoy, internal/core/interview, internal/core/afterupdate, internal/surface/cli, .abcd/config.json, commands/ahoy.md, commands/launch.md, .abcd/development/brief/04-surfaces/01-ahoy.md, .abcd/development/brief/04-surfaces/04-launch.md, .abcd/development/brief/04-surfaces/21-update.md
  • tests: internal/core/launch (strict tag parse and order), internal/core/embargo (its tags through the shared parser), internal/core/releasedigest (schema, strict read, sanitising, chain walk and gap report, seed equals CHANGELOG), internal/core/release (the digest write, its order and undo, the declared path, no path no digest), internal/core/ahoy (directional version gap and stamp, default-change table, waiting set, only-waiting install, --yes leaves value questions unset), internal/core/interview (latest value per question), internal/core/afterupdate (pending, moment, store, once per repository and release, once per computer), internal/surface/cli (the verb's plain and JSON output, the hook's constant line and no write, the zero-network harness, surface parity), and the live end-to-end run

Steps

  1. Strict release versions and a directional upgrade gap
    • packages: internal/core/launch, internal/core/embargo, internal/core/ahoy
    • tests: internal/core/launch, internal/core/embargo, internal/core/ahoy
    • Add launch.ParseReleaseTag: exactly vMAJOR.MINOR.PATCH, digits only, no leading zero, components bounded, ordered by CoreGreater; embargo's releaseTagRe calls it.
    • detectVersion reports version.upgrade only when both sides parse and the running release is newer; its fix hint names abcd ahoy install --only-waiting.
    • stepVersionStamp never records a version older than the one recorded.
    • landed: 006217b6a
  2. The digest package
    • packages: internal/core/releasedigest
    • tests: internal/core/releasedigest
    • Define the schema (schema version; per release its tag, the tag it was cut from, and its entries: the line, the record ids, their impact) and embed digest.json with //go:embed.
    • Read it strictly and capped, sanitising every line.
    • Range(from, to) walks the chain down from to, returns each release in the range with its entries (needing action first), and names every release it cannot describe with its published address and a partial flag.
    • Seed digest.json from the derived CHANGELOG sections (v0.7.0 on); a test holds each line equal to its CHANGELOG line.
    • landed: 6ba0199d9
  3. The release cut writes the digest
    • packages: internal/core/release, .abcd/config.json, commands/launch.md, .abcd/development/brief/04-surfaces/04-launch.md
    • tests: internal/core/release
    • Claim the release namespace with the one key digest, read from the repository layer; a path outside the tree, under .abcd/, or not ending in .json is refused before any write.
    • Add the digest as a write in the cut's plan, before the CHANGELOG heading (which stays last), with its undo; the release appended is the cut's NextTag, BaseTag and the validated lines.
    • No declared path writes no digest; abcd's config declares internal/core/releasedigest/digest.json.
    • landed: db2308f1f
  4. The default-change table
    • packages: internal/core/ahoy
    • tests: internal/core/ahoy
    • Embed defaults/default-changes.json: entries of a setup key and the record that changed its default, empty at first.
    • A test refuses an entry whose key is not a setup question key or whose record id is malformed.
    • landed: 4fa84825a
  5. What waits on this repository
    • packages: internal/core/ahoy, internal/core/interview
    • tests: internal/core/ahoy, internal/core/interview
    • Add interview.Latest(place, name): per question id, the newest value across one interview's records.
    • Add ahoy.Waiting(cwd, from, to): the never-answered setup questions the detection's gaps arm, less those whose latest setup value is later, plus the settings whose default changed in a release after from up to to and which the repository layer leaves unset, each with its title.
    • A test drives a dry install over a fixture and holds every key the install prompts for to be a key Waiting can return.
    • landed: 66fc2dd65, 668057055
  6. The moment and its record
    • packages: internal/core/afterupdate
    • tests: internal/core/afterupdate
    • Pending(cwd), read-only: whether news is unseen on this computer and whether an offer is due here (both versions parse, setup_version older than the running release, Waiting non-empty, no answer recorded for this repository and release).
    • Moment(cwd): the summary line per unseen release and the offer line; Full(cwd, from): every release in the range, action first, with record ids, the undescribed releases named, and the waiting titles.
    • Record(cwd, answer) and MarkSeen(): the exclusive per-release answer file under ~/.abcd.noindex/after-update/<root-sha>/ and the raise-only news-seen; an answer also raises news-seen.
    • landed: 89b995d93, 20d51beed
  7. The verb and the setup the yes runs
    • packages: internal/surface/cli, internal/core/ahoy, commands/ahoy.md, commands/update.md, .abcd/development/brief/04-surfaces/01-ahoy.md, .abcd/development/brief/04-surfaces/21-update.md
    • tests: internal/surface/cli, internal/core/ahoy
    • abcd update news [--full] [--answer yes|not-now] [--seen] [--json], a sub-verb of abcd update (renamed from abcd ahoy after-update by the product thinker's ruling of 2026-10-17, no alias), wired on the CLI and commands/update.md; bare and --full write nothing; --answer with --full or --seen is a usage error.
    • ahoy install --only-waiting asks only ahoy.Waiting's set and stamps the running release on completion; with --yes it leaves every waiting value question unset.
    • commands/update.md gives the host steps: relay the moment in the first reply, put the offer through the host's question tool, run --answer only with the person's reply, and on yes run the only-waiting setup.
    • A test runs the verb in a repository whose own CHANGELOG.md is not abcd's and asserts none of its text is printed.
    • landed: d90f0b6db, b1549fd73, ee7cc5884, 336b9a137
  8. Session start's fixed line
    • packages: internal/surface/cli
    • tests: internal/surface/cli
    • The session-start hook asks afterupdate.Pending with the detection it already has and, when anything is pending, prints the one constant line to stdout, apart from the notices count.
    • The tests hold that the line is byte-equal to the constant whatever the repository's config holds (a directive payload in setup_version included); the store and the repository are unchanged after the hook; the zero-network harness (countingTransport, the release-fetcher seam) covers the hook and every form of the verb.
    • landed: e86583387, 0dd101292
  9. The live run
    • packages: internal/surface/cli
    • tests: internal/surface/cli
    • Build at a test tag with a digest carrying it, run in a scratch repository set up by the previous release, and capture a live session's first reply, the answer, and the store and config before and after.
    • Evidence: a live headless host session on a build of 1beb791b9 stamped v0.15.0, in a scratch repository whose config recorded setup_version v0.14.2, printed the session-start line, ran abcd update news and opened its first reply with the news and the offer (restated, not verbatim); after abcd update news --answer not-now a second session printed neither, and .abcd/config.json was unchanged throughout; transcripts and the store and config before and after are in the session scratchpad's live-run/transcripts/ (session1.jsonl, session2.jsonl, 00-before.txt to 03-after-session2.txt).
    • Evidence, re-run after the amended line: a live headless host session on a build of 0ab48ce64 stamped v0.15.0, loaded from a plugin copy of that commit, in a fresh scratch repository whose config recorded setup_version v0.14.2, with a decoy abcd first on PATH that logs any call and exits 1, printed the amended session-start line, opened /abcd:update news through the host's skill tool and ran the page's "<plugin root>/abcd" update news --json with the plugin root the host wrote in; the decoy's log stayed empty, and the first reply began with the two printed lines byte for byte (checked against the command's lines). After update news --answer not-now through the plugin's binary, a second session printed neither the line nor the offer, and .abcd/config.json was unchanged throughout; transcripts and the store and config before and after are in the session scratchpad's live-run/transcripts2/ (session1.jsonl, session2.jsonl, 00-before.txt to 03-after-session2.txt).
    • landed: 1beb791b9

Frontmatter

idspc-2610170635097509
typespec
lifecycleclosed

References2

mentions4