Development record
when-a-person-runs-abcd-s-check-on-purpose-abcd-also-says
Summary
This spec delivers itd-2610031026190632 (abcd's update check says when a harness is older than abcd needs). The file name and slug are the ones minted on filing; the intent's title is the one that describes the work.
When a person runs abcd update --check, the report gains one line for each
agent harness abcd knows of that is installed on this computer, and a
harnesses field in the JSON beside the unchanged check object. A harness
below the version abcd needs gets a warning naming the version installed, the
version needed, and the harness's own update command. A harness at or above
it gets no warning. A harness abcd cannot read a version from is named as not
checked, with the reason. The line is read from this computer alone: it never
reaches the network, even within the one command that does.
Nothing about the reading is new. The installed-version reading and the floor are owned by spc-2610031156364295 step 5, which ships first; this spec widens that one reading from one harness to a table of them, gives each outcome a reason, and puts the result in the update check.
The settled records are designed to here, never reopened: the intent's decisions 1 to 9, its confirmed criteria H1 to H7, and its four scope conditions.
Scope
In:
internal/core/ahoy: the harness table (each harness abcd knows of, its command, whether abcd has an adapter for it, its floor with the floor's source cited beside it, and its own update command), the reading step 5 writes widened to take any table entry's command and to say why a version was not read, and one function that checks every entry against its floor.internal/core/runner: the runner's command admission (launcher.admit,proc.go, line 79) and its shipped route names (runnerNames,runner.go, line 49) exported for the table to use and to be checked against (open question 2).internal/surface/cli/version.go: aHarnessesfield onversionOutput(lines 36 to 48), a sibling ofCheck(line 47) and filled only whenrunVersionruns withcheckset (lines 94 to 96), and one text line per harness after thenext:line (lines 109 to 117).internal/surface/cli/update.go: the verb's long help (lines 47 to 49) and the--checkflag's help (line 109) widened from abcd alone to abcd and the agent harnesses on this computer; the regenerateddocs/reference/cli/commands.mdthat carries both.- Pages and record: the plugin page
commands/update.md("Only asking", lines 27 to 37, which tells the agent which fields to relay);docs/how-to/install.md(lines 296 to 299, the paragraph on the check); the brief's04-surfaces/21-update.md(lines 23 to 28, the check) and04-surfaces/12-version.md(line 45, the report the check adds to). - H7's dated receipt, in the local tier.
Builds on and reuses:
- spc-2610031156364295 step 5: the installed-version reading (the command on
the search path, one subprocess, a short timeout, the first
major.minor.patchit prints) and the one floor it defines. Reused, never restated: this spec changes that code in place, and step 5's own test (TestHostVersionWarning) stays as it is. - itd-111 and adr-38: the update check is the one explicit check; the harness line adds no fetch to it, so adr-38's tiers and brief invariant 7 are untouched (decision 7).
- The update check's existing release-fetcher seam (
newReleaseFetcher,version.go, line 22) and its recording stand-in (recordingFetcher,version_check_test.go, line 16), for H1.
Out:
- Any lookup of a harness's newest release, and any mention of a release above the floor (decisions 4 and 7).
- The running session's own copy of a harness, and a newer version already downloaded but not yet running: itd-2610031325050110 (decisions 6 and 8).
abcd --version,abcd ahoy, the session start and the status line: none of them reads a harness's version for this line (decision 3).TestOnlyUpdateCheckTouchesTheNetwork(version_check_test.go, line 28) is unchanged.- Setup's own warning for a harness below the floor: step 5's, kept as it is; the cost of a second place stating it was accepted in decision 7.
- Whether a harness's own settings hold its updates (open question 3).
- Hosts that run only inside an editor, with no command on the search path (scope condition 4).
Approach
The harness table, beside the reading and the floor
Step 5 writes one reading for one harness and one floor constant beside it.
This spec turns the floor constant into the first row of one table in the
same file, harnessFloors, each row holding:
Name: the harness's route name as the runner spells it (claude,opencode), or a plain name for a harness abcd has no adapter for;Command: the command looked for on the search path;Adapter: whether abcd has an adapter for it;FloorandFloorSource: the version abcd needs and where that version was established, cited beside it, or empty when abcd needs no particular version (scope condition 3);UpdateStep: the harness's own update command (open question 3).
Today the table has three rows. The two adapters: claude, whose floor is
step 5's, and opencode, which has none. One harness with no adapter,
gemini, the terminal harness spc-2610031156364295's conventions-file
registry already names (open question 1). A harness the table does not name
is never looked for.
TestHarnessTableCoversEveryAdapter asserts the table's adapter rows are
exactly the runner's shipped route names, so a third adapter added to the
runner without a row fails a test rather than going unchecked.
One reading, with a reason for every outcome
Step 5's reading takes the command name from the table row instead of a literal, and returns a result rather than a silent nothing:
- the version read, the first
major.minor.patchthe command printed; or - the reason none was read: not on the search path; refused before it was run (open question 2); no answer within the timeout, or a non-zero exit; no version printed.
Step 5's setup warning keeps its behaviour by treating every reason as "raise nothing", as it does today. A row with no adapter is never run: it is looked up on the search path and nothing more, since abcd does not know what its version command prints.
CheckHarnessFloors() walks the table in order and returns one
HarnessFloor per row that applies:
| Row | Verdict | Fields set |
|---|---|---|
| adapter, version read, below the floor | below_floor |
installed, needed, update step |
| adapter, version read, at or above the floor | at_or_above_floor |
installed, needed |
| adapter, version read, no floor in the table | no_floor |
installed |
| adapter, no version read | not_checked |
reason |
| no adapter, command on the search path | not_checked |
reason: abcd has no adapter for it |
| no adapter, command absent | not listed |
Versions are compared part by part as numbers, never as strings, so 1.10.0
sits above 1.9.2. No verdict reads as current: at_or_above_floor and
no_floor say what was compared and nothing more (brief invariant 16). A
row with no adapter is listed only when its command is present, since
naming every absent harness abcd has no adapter for would only add noise.
CheckHarnessFloors takes no fetcher and no network client, and nothing it
calls does: the reading is a subprocess on this computer, and the floor is in
the binary.
The update check carries the lines
versionOutput gains
Harnesses []harnessLine `json:"harnesses,omitempty"`
beside Check, never inside it (criterion H5), each harnessLine carrying
name, verdict, and, where they apply, installed, needed, reason and
update_step. runVersion fills it only when check is set, after
runReleaseCheck, and independently of it: a failed or refused release
fetch leaves the harness lines exactly as they would otherwise be.
The text render prints one line per harness after the next: line, in the
report's existing column, every value passed through termsafe.Sanitize
since a harness's own output is untrusted:
harness: claude 1.4.0 is older than the 1.5.0 abcd needs; update it with `claude update`
harness: opencode 1.18.31 (abcd needs no particular version of it)
harness: gemini not checked: abcd has no adapter for it
A harness at or above its floor prints one line saying so, with no warning word:
harness: claude 1.9.2 meets the 1.5.0 abcd needs
The numbers in these samples are made up, as the intent's criteria are.
The help, the page, the docs and the brief
The verb's long help and the --check flag's help say the check also reads
the version of each agent harness installed on this computer, offline, and
warns only below the version abcd needs. The plugin page tells the agent to
relay each harnesses entry's verdict and, where present, update_step
verbatim. The how-to paragraph and the two brief chapters say the same in
host-agnostic words: "the agent harness" and "the harness's own update
command", with no harness's name and no version string outside fenced sample
output, so the harness/* docs-lint rules pass without a new allow escape.
How each acceptance criterion is met
H1. TestHarnessLinesNeverGoOnline, in internal/surface/cli beside
TestOnlyUpdateCheckTouchesTheNetwork, puts a fake harness below its floor
on the search path and runs update --check --json twice: once with the
recording stand-in behind newReleaseFetcher, asserting exactly one
LatestTag call, and once with a stand-in that fails every call, asserting
the harnesses field is byte-identical between the two runs and still carries
the below_floor warning.
H2. TestUpdateCheckWarnsBelowTheFloor: a fake claude printing
1.4.0 against a test floor of 1.5.0 gives exactly one harness: text line
naming both numbers and claude update, and a JSON entry with verdict
below_floor, installed 1.4.0, needed 1.5.0 and update_step
claude update. The table's floor is substituted through a package seam, so
the test never depends on the real floor.
H3. TestUpdateCheckQuietAtOrAboveTheFloor: fakes printing 1.5.0 and
1.9.2 against a floor of 1.5.0 give no warning line and two JSON entries
with verdict at_or_above_floor. In the core, TestHarnessFloorVerdicts
holds every row of the verdict table above, and
TestHarnessVersionsCompareAsNumbers holds 1.10.0 above 1.9.2.
H4. TestUpdateCheckNamesUncheckedHarnesses: a fake gemini present (no
adapter), no opencode on the search path (adapter, command absent), and a
fake claude printing unknown (no version printed). Each reads
not_checked with its own reason, in the text and the JSON, and none reads
at_or_above_floor. TestHarnessNotCheckedReasons, in the core, holds every
reason the reading can give, the timeout and the refused command included.
H5. TestUpdateCheckObjectIsUnchanged compares the check object, taken
from the JSON as raw bytes, with
internal/surface/cli/testdata/update-check-object.json, captured from
today's code with the recording stand-in in the step's first commit, before
any other change. The comparison runs with harnesses present and with none,
and asserts harnesses is a sibling key of check at the top level.
H6. go run ./cmd/abcd lint docs (preflight's docs-lint gate) is clean
over the regenerated command reference and the how-to page, and
TestUpdateCheckDocsAddNoAllowEscape asserts the count of
docs-lint: allow markers in docs/how-to/install.md and
docs/reference/cli/commands.md equals the count at the step's base,
written into the test.
H7. A dated receipt,
.abcd/.work.local/logs/harness-floor-check-<yyyy-mm-dd>.md, taken on the
product thinker's machine with go run ./cmd/abcd update --check from the
source checkout. It records the date, the commit, each harness line quoted,
and, for each adapter's command, what that command's own version flag prints
the same day, and states that each line agrees with that output and with the
table's floor.
Open design questions
These are for the technical facilitator; the records do not settle them. Each is designed to the marked option, and the reason is given beneath it.
- Where a harness with no adapter comes from (H4's first case).
(a) A short list of harnesses abcd knows by name but has no adapter for,
each named only when its command is on the search path and never run,
seeded with
gemini, the one terminal harness the records already name (designed to): the case H4 names occurs on a real machine, at the cost of a list to keep. (b) No such list: abcd names nothing it has no adapter for, and H4's first case is only a test fixture, so decision 5's "named as not checked" never happens for a harness without an adapter. (c) The harness this session runs in, read from its environment: names the harness actually in use, but each host's environment marker is its own undocumented contract, and a terminal verb may run in none.- (a): it is the only option under which the criterion describes something a person can see, and it reads nothing abcd does not own.
- Decided: (a), on 2026-10-03 without a question: naming a known harness only when its command is present answers H4's first case without running anything.
- Which command is run. The reading runs a program found on the search
path. (a) Through the runner's admission, exported as
runner.Admit: a command that resolves to a relative path, sits inside the checkout the check runs from, or is writable by group or others is not run and reads asnot_checkedwith that reason (designed to): the same rule that already decides which harness binary abcd runs a role through, applied to the one reading, step 5's setup warning included. (b) A plain search-path lookup, as step 5 is written: fewer moving parts, butabcd update --checkrun inside a cloned repository could start a program that repository placed on the search path.- (a): one rule for running a harness's binary, and the check never runs
repository content.
internal/core/runnerimports noahoycode, so the import adds no cycle. - Decided: (a): one admission rule for the one shared reading.
- (a): one rule for running a harness's binary, and the check never runs
repository content.
- The harness's own update step. (a) One fixed command per adapter, the
one the harness documents for updating itself (
claude update,opencode upgrade) (designed to): no settings read and no install layout read, at the cost that an install another tool manages is pointed at a command that may hand off to that tool. (b) One per install shape, read from where the command resolves (the design review's finding 4): the right command per shape, but each shape is the vendor's undocumented layout and moves between releases. (c) (a), plus "held" when the harness's own settings stop its updates (the intent's proposal): it avoids naming a command the person turned off, but reads settings keys abcd has not confirmed against the vendor's documentation, and for the second adapter a settings file abcd reads nowhere today.- (a): it names the harness's own way to update, which is what the press release promises, and adds no new read of another tool's configuration. The "held" half of the intent's proposal is not built; a person who has turned updates off sees the command and chooses.
- Decided: (a): one fixed update command per adapter, each confirmed in its tool's help.
- A harness abcd needs no particular version of. The intent proposes
naming it as having no floor to check against, never as current.
(a) A
no_floorverdict, printed as "abcd needs no particular version of it" (designed to): the reading is shown and nothing is claimed beyond it. (b) Leave it out of the report: quieter, but a harness abcd ran a version command on would then be invisible.- (a): it is the intent's proposal, and brief invariant 16 requires the report to say what it compared.
- Decided: (a): no_floor never reads as current (invariant 16).
Footprint
- packages: internal/core/ahoy, internal/core/runner, internal/surface/cli, commands/update.md, docs/how-to, docs/reference, .abcd/development/brief/04-surfaces
- tests: TestHarnessTableCoversEveryAdapter, TestHarnessFloorVerdicts, TestHarnessVersionsCompareAsNumbers, TestHarnessNotCheckedReasons, TestHarnessCommandAdmission, TestHostVersionWarning (unchanged), TestHarnessLinesNeverGoOnline, TestUpdateCheckWarnsBelowTheFloor, TestUpdateCheckQuietAtOrAboveTheFloor, TestUpdateCheckNamesUncheckedHarnesses, TestUpdateCheckObjectIsUnchanged, TestOnlyUpdateCheckTouchesTheNetwork (unchanged), TestUpdateCheckDocsAddNoAllowEscape; the command reference's drift test after regeneration; docs-lint and record-lint clean; the dated receipt in the local tier
Steps
- The harness table and one reading with a reason for every outcome
- criteria: the core halves of H2, H3 and H4
- packages: internal/core/ahoy, internal/core/runner
- tests: TestHarnessTableCoversEveryAdapter, TestHarnessFloorVerdicts, TestHarnessVersionsCompareAsNumbers, TestHarnessNotCheckedReasons, TestHarnessCommandAdmission, each watched fail first; TestHostVersionWarning and the runner's adapter tests pass unchanged
- waits on spc-2610031156364295 step 5, which writes the reading and the floor this step widens
- needs: none
- The update check carries the harness lines
- criteria: H1, H2, H3, H4, H5
- packages: internal/surface/cli, docs/reference, commands/update.md
- tests: TestUpdateCheckObjectIsUnchanged, its testdata captured from the base in the step's first commit; TestHarnessLinesNeverGoOnline, TestUpdateCheckWarnsBelowTheFloor, TestUpdateCheckQuietAtOrAboveTheFloor, TestUpdateCheckNamesUncheckedHarnesses, each watched fail first; TestOnlyUpdateCheckTouchesTheNetwork unchanged; the command reference regenerated with
go generate ./internal/surface/cli - lands after step 1
- needs: 1
- The how-to, the brief, the receipt and the close
- criteria: H6, H7; the intent's close
- packages: docs/how-to, .abcd/development/brief/04-surfaces
- tests: TestUpdateCheckDocsAddNoAllowEscape; docs-lint and record-lint clean; the dated receipt taken at the step's branch tip before the pull request; a docs-fidelity review recorded for HEAD, then
abcd spec close spc-2610031342374947(the intent already declaresimpact: additive) with aDelivers: itd-2610031026190632trailer - lands after step 2
- needs: 2
Frontmatter
| id | spc-2610031342374947 |
|---|---|
| type | spec |
| lifecycle | open |
.abcd/development/specs/open/spc-2610031342374947-when-a-person-runs-abcd-s-check-on.md
open on GitHub ↗ · commit history ↗
References1
- itd-2610031026190632abcd's update check says when a harness is older than abcd needsimplements
mentions4
- adr-38Implicit checks are disk-only — the network answers only an explicit ask
- itd-111A Stale abcd Never Answers Silently
- itd-2610031325050110abcd notices a harness update downloaded but not yet running
- spc-2610031156364295evaluate-whether-claude-md-can-be-removed-safely-now-that