Skip to content

feat(cli)!: one --refresh[=live|machine] flag for status, system and maintain (ADR-0063) - #263

Merged
pacphi merged 48 commits into
mainfrom
feat/one-refresh-flag
Sep 29, 2026
Merged

pacphi merged 48 commits into
mainfrom
feat/one-refresh-flag

Conversation

@pacphi

@pacphi pacphi commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Summary

One refresh vocabulary across the CLI (ADR-0063): --refresh[=live|machine] on ak status, ak system and ak maintain.

Strength What it runs, in order
--refresh Maintenance evidence → inventory rebuild → local status re-check (including online version lookups)
--refresh=live the above, plus the live checks (a failed check is a warning)
--refresh=machine a machine measurement first, then everything --refresh runs

Each stage prints one line, --json adds a refresh stage list, and a failed stage exits 1.

What changed around it:

  • Live checks (formerly ak x verify) run through ak status --refresh=live.
    • --only a,b narrows the checks, and only the named checks decide the exit code.
    • Quick checks: aqe-embedding, deja-vu, mcp, memory, providers, security. Slow proofs: aqe, harvest, learning, memory-routes.
  • ak host check-connection is the CLI twin of the dashboard's connection check. It shows the same consent text, works on managed hosts only, and answers in JSON on every path.
  • ak host reset-routes replaces ak host refresh. pick, off and reset-routes honour --dry-run.
  • ak sync:
    • --skip versions|self|ruvnet-brain|ruvector also skips that part's online lookup.
    • --dry-run previews real version lookups, records nothing, and removes its temporary npm cache.
    • A failed lookup keeps the recorded value and retries once per TTL window.
  • Codex quota is requested only when host-setup evidence says Codex is found (ADR-0010).
  • --json failures print one JSON object on stdout, with recovery guidance for an unreadable kit.json.
  • Docs and ADRs state the new vocabulary only: ADR-0010, 0023, 0025, 0044, 0048, 0053, 0055 and 0063, with the ADR index extended past 0052. A vocabulary guard test keeps retired spellings out of current docs.
  • The dashboard is unchanged. Its Refresh control and POST /api/refresh are the next remediation program's work.

Breaking changes

Every retired spelling below now exits 2 with the generic parser error. None has an alias or a hint. Every replacement was checked against the parser.

Retired Use instead Notes
ak x verify ak status --refresh=live --only learning,memory-routes,security,aqe,providers,harvest,deja-vu Named checks run in parallel, each capped at 6 min. ak status --refresh=live alone runs the quick set.
ak x verify all same as above; add ,mcp for the Codex MCP check all used to omit mcp
ak x verify learning | security | aqe | providers | harvest | deja-vu | mcp ak status --refresh=live --only <same name>
ak x verify memory ak status --refresh=live --only memory-routes --only memory is the quick round trip, without the route observation
ak status --live ak status --refresh=live
ak status --deep ak status, or ak status --refresh to re-probe
ak system --deep ak system --refresh=machine add --project-trees to measure project working trees
ak maintain scan ak maintain --refresh
ak maintain scan --refresh-inventory ak maintain --refresh
ak maintain scan --deep [--refresh-inventory] ak maintain --refresh=machine
ak maintain scans start --deep ak maintain --refresh=machine, then ak maintain scans start
ak maintain plan --deep … ak maintain --refresh=machine, then ak maintain plan …
ak maintain recipes refresh none (removed until a recipe registry exists) recipes list|accept|withdraw remain
POST /api/maintenance/v2/recipes/refresh none (route removed)
ak host refresh / ak x host refresh ak host reset-routes / ak x host reset-routes
ak usage prompts --deep ak usage prompts --show-text
CI: node bin/agentic-kit.mjs x verify <suite> node bin/agentic-kit.mjs status --refresh=live --only <suite> memory → memory-routes

Behaviour changes under unchanged spellings:

  • Bare ak maintain / ak maintain --json reads the last saved report (verb report); it used to run the provider scan. Add --refresh[=live|machine] to refresh first. --refresh and --project-trees on any other verb, and --only on any maintain verb, are usage errors (exit 2).
  • ak status --refresh also refreshes Maintenance evidence and rebuilds the inventory before the local re-check, prints one line per stage, and exits 1 when a stage fails.
  • ak system --refresh[=live|machine] runs the same stages and exits 1 on a failed stage.
  • ak sync --dry-run performs the online version lookups a real sync performs (network access), records nothing, and uses a temporary npm cache it then removes. Host evidence is still read as last recorded.
  • A failed online version lookup keeps the recorded latest version and retries once per TTL window.
  • --json failures: a rejected option prints { error, exitCode: 2 } on stdout, with the message and help on stderr (help used to go to stdout). An unreadable kit.json prints { error, exitCode: 1, recovery }.
  • ak host pick|off|reset-routes --dry-run preview and write nothing. ak host adapters --dry-run exits 2.
  • Live-check evidence is written with source status-refresh-live. Records written by the older live checks still read back, labelled "an earlier live check".
  • /api/limits: codexUnavailable.reason can now be host-not-found or host-unconfirmed, meaning Codex was not asked. The field is additive.

Testing

Not in this PR (next remediation program)

  • The dashboard half: one Refresh control, POST /api/refresh, and read-only GET routes.
  • Command-level usage errors under --json for usage/models/host/audit/heal and ak x ….
  • --dry-run --json refusals answering one JSON object.
  • Two offline selfDrift edge cases.

🤖 Generated with Claude Code

readLimits gates the app-server spawn on providers.mjs's new
recordedHostPresence('codex'), reading the host-setup evidence detectHosts
already records for every host on each /api/status poll instead of probing.
Absent, stale (>6h) or PATH-invalidated evidence reads as 'unconfirmed', never
'not-found'; either non-'found' outcome serves the last cached figure (if any)
and skips the spawn, and the Limits panel now names both new reasons instead
of falling back to its generic sentence.
…not requested

codexFailedShort prefixed every Codex unavailable reason with "last refresh
failed", which is false for host-not-found/host-unconfirmed: B6b-D1's
presence gate skips the app-server spawn entirely for those, so no refresh
was ever attempted. Those two reasons now render "not refreshed" instead,
alongside every other reason keeping "last refresh failed" unchanged. Also
matches the two new CODEX_WHY strings' apostrophes (&rsquo;) to their
neighbours.
…arting with ak status

src/lib/refresh.mjs holds the shared refresh operation (ADR-0063): the
strengths (a bare --refresh, =live, =machine), one ordered stage table,
the stage runner, the CLI stage set over one lazily built collector,
Maintenance service and management facade, and the stage renderer. The
bin rewrites a bare --refresh to --refresh= before parseArgs for commands
whose refresh option takes a value.

ak status takes the flag. --deep and --live are gone and get the parser's
generic unknown-option error. --refresh=live runs the same live checks the
old --live ran, then re-checks local evidence so the rows show them. With
--json, every human line goes to stderr while the stages run and stdout
carries one JSON object with a refresh summary. A failed stage exits 1.
The stale-result hint on remembered live checks now names
ak status --refresh=live.

humanOutputToStderr moves from sync.mjs to output.mjs unchanged.
…sals for stray arguments and half-injected services

The status help said a refresh under --json sends its stage lines to
stderr; no stage lines print there at all, only what a stage itself
writes. The live stage now announces its start with a dim line, since the
checks can run for a minute with nothing else on screen. A refresh with a
positional (ak status --refresh live) is a usage error naming the
one-token spelling instead of silently running the bare strength.
cliRefreshStages refuses a Maintenance service or facade injected without
the other, because the half it would build uses the default control root.
A test pins that the machine stages' ticker stays off stdout under --json.
The live checks and slow proofs move to src/lib/live-checks.mjs. --only
names checks for --refresh=live and decides the exit code; without it the
quick checks that apply run (mcp whenever Codex is enabled) and a failure
stays a warning. Slow proofs (learning, harvest, aqe, memory-routes) run
only when named. Results are remembered as status-refresh-live; records
from the earlier sources read as an earlier live check. ak x verify is gone
and gets the generic unknown plumbing command error. The nightly deep
proofs call the new spelling.
Rows and failed refresh stages still print and reach --json, but with
--only the exit code is 1 only when a named check failed, was
inconclusive or did not run. ak system refuses --only: it reports no live
checks. The live-check evidence store takes its id list from the refresh
vocabulary, and a named check that does not apply says on its line that
its result is not remembered.
`ak sync --skip versions` (and `--skip self`, `--skip ruvnet-brain`) still
made the forced pre-plan lookup for that part, and the plan read fetched
again whenever the 24 h cache had expired. A skipped part is now never
looked up: the plan read and the converge proof get what kit.json recorded
for it, through a new `versionEvidence` on status's collect() context.

The plan-time lookups move out of sync.mjs into
src/commands/sync/plan-versions.mjs: refreshPlanVersions makes the forced
lookups for every part --skip does not name and returns cache-only evidence
for the parts it does; skippedVersionEvidence is recomputed for the converge
proof, so installed versions are read after the apply phase. The health
snapshot reads the same cache-only drift when versions is skipped. A sync
that skips nothing behaves as before (ADR-0063).

versions.mjs gains cachedOnlyLatest, a fetchLatest that never touches the
network. ruvnet-brain.mjs drift() gains cacheOnly, which reports the recorded
release with no lookup and no write; its cached read and its lookup become
two small helpers. converge takes an injectable brainDrift for tests.
…ding it

A dry run now makes the lookups a real sync makes before it plans (npm
versions for ruflo, agentic-qe and npm-managed hosts, the kit's own tags,
Ruflo's release dates, the Brain release while ak manages it) and records
nothing: driftReport, selfDrift and the Brain drift take record: false, and
the release dates land in a copy of kit.json. The results reach the plan
read through versionEvidence. Every npm view in the preview runs with its
npm cache in a per-run folder under the OS temp directory (logs off, update
notifier off), removed once the lookups are done, so ~/.npm is untouched.
When every lookup fails, the dry run prints one line saying versions were
not checked online and the plan uses the recorded ones.

A registered ruvector is read the way a real sync's plan read does, looked
up once its cache has expired, again without recording. --dry-run
--no-upgrade makes no lookup and reads every part from the cache. Before
this change, a dry run on an expired cache wrote kit.json and ~/.npm.

A failed lookup never erases a good one: when the Brain's GitHub release or
ruvector's npm lookup answers nothing, the recorded latest (and the Brain's
asset fact) stays, last is not restamped so the next call retries, and the
drift reports it as a cache fallback. This holds on every path, including
ak status --refresh. Each Brain drift helper now returns its own source.

--skip ruvector leaves ruvector's lookup out, like versions, self and
ruvnet-brain: both sync reads get the recorded ruvector drift.

The skip tests now answer newer versions for the skipped part, so a leaked
lookup changes that part's kit.json record and fails the byte comparison.
… TTL window before retrying

When the Brain's GitHub release or ruvector's npm lookup answers nothing,
the recorded latest (and the Brain's asset fact, install record and held
refresh) stays in kit.json and is reported as a cache fallback, and last is
restamped. An offline ak status or dashboard poll therefore waits on these
lookups once per TTL window instead of on every read, and ak status
--refresh still forces a retry. A new observedAt field keeps when the
recorded latest was actually seen, carried forward across failures, so the
Brain row's observation label never names the failed attempt. record: false
and cacheOnly still write nothing.

The dry-run preview makes its temporary npm cache folder only when an npm
lookup runs, and a failed removal of that folder no longer fails the
preview. Its offline line gives the age of the parts it could not check,
or the oldest of them when they differ. The sync command tests' dry runs
now answer every version lookup themselves, so a machine with npm in
/usr/bin never queries the real registry from the suite.
No registry is configured for any installation, so ak maintain recipes
refresh, the ADR-0048 v2 route, the facade method, and the
recipeRegistry/fetchImpl service options that existed only for it can
never succeed. Remove every user-reachable path; the recipe store's
verified-staging function (allowlist/HTTPS/redirect/signature checks,
maintenance/management/recipes.mjs) and its own tests stay as the
library a future registry calls.

The retired recipes refresh subverb gets the parser's generic
unknown-subverb error, and the retired v2 route answers 405 exactly
like any other unknown method or route.
…ever build a default service beside injected stages
…aits one TTL window

driftReport and selfDrift now follow the rule the Brain and ruvector lookups
already use. When every npm lookup fails, the recorded versions (seen, and the
kit's best candidate) stay in kit.json and last is restamped, so an offline
ak status, dashboard poll or drift nudge waits on the registry once per TTL
window instead of on every read. force (a real ak sync, ak status --refresh)
still retries. observedAt keeps when the recorded versions were actually seen
(per package for driftReport, and now for selfDrift too), taken from the
previous last for a record written before it existed, so the dry-run offline
line still gives the age of the recorded versions. A partial kit lookup whose
cached candidate still wins renews nothing, as before.

record: false never writes. The reads for a part --skip names now pass
cacheOnly (no lookup, no write) instead of a lookup that always fails, which
the new rule would otherwise have restamped; cachedOnlyLatest is removed.
Two tests that read kit.json after a failed forced lookup now compare only
what they meant to: the versions part of the record, and the config the live
checks saw.
…e sync goes on

Before it reads the plan, a real ak sync re-checks each enabled host and the
shared host setup. A probe that threw used to end the whole sync. Each host's
probe and the host-setup probe are now guarded: a warning names the host (or
host setup) and the error, and the plan reads that host as ak last recorded
it. kit.json itself is still read unguarded, so an unreadable config keeps its
own error and recovery text.
The non-dry sync tests inside withOpencodeCli put /usr/bin on PATH, so on a
machine with /usr/bin/npm their forced version lookups would have queried the
real npm registry. realSync now injects offline lookups, as the dry-run helper
already did, and so does the child-process helper the --json tests use.
…kip lines print once

A --json invocation now always gets exactly one JSON object on stdout, with
every human line (the message, the help, the recovery commands) on stderr.

- An option the parser rejects answers { error, exitCode: 2 }; ak sync answers
  its own empty result shape with error (jsonUsageError). A retired spelling
  gets the parser's generic unknown-option text, with no hint.
- The usage errors ak status, ak system and ak maintain raise themselves (a
  bad --refresh value, a stray strength argument, --only or --project-trees
  misuse, an unknown maintain verb) answer { error, exitCode: 2 }, each
  through its command's one usage-error path.
- An unreadable kit.json answers { error, exitCode: 1, recovery } from the
  bin, and ak sync --json adds recovery to its result. configErrorRecovery in
  config.mjs builds the same mv or Move-Item commands the human output prints.

Without --json the output is unchanged. ak sync no longer repeats a skipped
item's "skipped by request" line in the verdict when the plan announcement
already printed it; the JSON skipped list is unchanged. The help for sync,
status, system and maintain, and the sync --json notes in UPGRADING and
INSTALLATION, describe the JSON failure answers.
A failed lookup restamps last even when nothing was ever recorded, so the
dry-run offline line fell back to that last and said "this plan uses the
versions ak recorded just now" (or "2d ago" for an expired ruvector record)
where it should say ak has recorded none. Each part's last now stands for an
observation only when its record holds a latest: seen entries, the kit's
best, the Brain's or ruvector's latest.

A stable install whose recorded kit candidate came from the next channel can
never use it, so its record is never fresh and every offline lookup restamped
and rewrote kit.json. That failure now writes nothing, as before the restamp
rule. Dropping the candidate instead would make an empty record look fresh
and stop the lookup for a TTL window once the registry is back, so ak status
could not show a real latest.
…json after -- is positional

ak maintain's catch-all turned every thrown error into a usage refusal, so
an unreadable kit.json under a verb that reads it (discovery, sources add)
answered { error, exitCode: 2 } with no recovery. A config error now reaches
the CLI's fatal handler: { error, exitCode: 1, recovery } under --json, the
recovery commands otherwise, as for every other command.

A rejected option answered in JSON when --json appeared only after the --
terminator, where it is a positional. Only the tokens before -- now decide.
Add tests/kit/refresh-vocabulary-guard.test.mjs (R16-R18) to guard the CLI
vocabulary and rewrite every retired spelling it finds (ak x verify, ak
status --live/--deep, ak system --deep, ak maintain scan, --refresh-inventory,
ak host refresh, ak usage prompts --deep, ak maintain recipes refresh) to its
current CLI spelling across README, docs/, claude/ and src/ comments, with no
old -> new tables. Dashboard docs keep today's control names and state the
new CLI equivalents beside them, as this branch closes CLI-only. Corrects two
stale DASHBOARD.md claims (Maintenance does reload on the shared poll; the
quoted re-measure status text does not exist) and documents the two new
"not refreshed" Codex reasons. Replaces the ledger-only B6b-D1 / Branch 6b
Task 1 labels in source/test comments with ADR-0010, and drops the dangling
(R17) suffix from three test assertion messages.
…y narration

Recipe trust and the live-check evidence glossary row named what used to
exist instead of only the current state (R17); AQE-EMBEDDINGS.md named the
wrong live check for the unmanaged-backend note (the full aqe proof, not
aqe-embedding); the troubleshooting "run everything" example named memory
redundantly alongside memory-routes, which already covers it.
ADR-0048 header: this branch's CLI-equivalents line stays the Updated line; Branch 9's M1b line becomes the first Earlier update.
ak maintain --refresh=live --only <check> accepted the flag and ran the
live checks, but always exited 0 regardless of a failed named check: unlike
ak status, ak maintain never renders live-check results or reports their
verdict, so the flag was silently accepted and then ignored (final review
I-1). --only is now refused with exit 2 for every verb, including the
default report, mirroring ak system's own refusal.

refreshedReport also built two unrelated collector instances for one
--refresh: defaultStages built its own for the machine/inventory stages and
the management facade, while createMaintenanceService() (no args) built a
second, cold one for the maintenance stage and the later service.report()
read (m-2). The collector is now built once in refreshedReport and threaded
through both.

renderReport printed only the scan-required finding's generic label ("Run
Maintenance scan") on an unmeasured machine, naming no command — a new
user's first hint invited the retired ak maintain scan verb. It now also
prints the finding's own nextAction.steps[0], the exact refresh command
(m-3).
The post-command drift nudge ran driftReport() (which shells npm view)
whenever mod.run() returned, without checking its exit code. A run-level
usage refusal a command raises itself — e.g. a rejected
ak status --refresh=bogus — reached the nudge exactly like a successful
run and spent a network call a parse error never did (final review m-1).

The nudge (and the local-artifact-drift check beside it) is now also
skipped when the command's own exit code is 2.
ak system --help said a bare --refresh (or --refresh=live) "refreshes
Maintenance evidence and the inventory, then reprints this same snapshot",
omitting that it also re-checks local status (including the online version
lookups) and that =live additionally runs the live checks (final review
m-6).
…options

The guard's adjacency regexes required a retired flag directly after the
command word, so a bracketed usage line (system [--deep] [--json], status
[--json] [--live]) and ak usage prompts --deep behind unrelated prose slipped
through undetected — exactly how four current-state docs kept the retired
spellings past the original guard (final review I-2). Two new patterns
tolerate intervening single-line bracketed options for status/system, and
scan the whole line for prompts ... --deep.

Also drops the ledger/plan task label from the file header in favor of an
ADR-0063 reference (final review m-8).
machine-footprint.md and ubiquitous-language.md still showed
ak system [--deep] [--json]; ubiquitous-language.md also claimed ak system
and ak about are "both read-only twins" of their dashboard areas, which is
no longer true of ak system --refresh. USAGE-SCORECARD-METRICS.md and
prompt-telemetry.md still named --deep as ak usage prompts's text-bearing
flag; the current flag is --show-text (final review I-2).
ADR-0010 and ADR-0063 said /api/limits and ak status both go through
readLimits to ask codex app-server for its quota. readLimits has exactly
one caller, dashboard-server.mjs's /api/limits handler; no status section
imports quota.mjs. Both ADRs now say /api/limits only (final review I-3).

Both ADRs also cited ledger-only ids (B6b-D1, B6b-D3, M-11) that live only
in the gitignored SDD ledger. They now point at the committed branch 6b
plan's named rulings (R9, R17) or its "Closing this branch" section instead
(final review I-3).

Also, ADR-0063's --only bullet read as spanning all three refresh commands;
it now says --only is ak status's alone, and that ak system/ak maintain
both refuse it outright (paired with the ak maintain code fix). Three
precision nits from final review m-5: setup.mjs's collectIntegrationFacts
call passes no cwd; selfRecord restamps on a total failure with no cached
candidate at all (only a partial answer, or an unusable cached candidate,
saves nothing); and ak sync --dry-run's online lookups redirect to, and
clean up, a temporary npm cache under the OS temp folder rather than
writing "nothing at all".
README.md's compact command table listed ak host status|pick|reset-routes|off,
omitting check-connection even though the usage block and ak --help --all
both list it (final review m-7).
…llector

The m-2 fix built refreshedReport's shared collector unconditionally, even
when deps.refreshStages was injected — a fully-injected test (refreshStages
+ service) used to trigger zero real collector or facade construction
(defaultStages was bypassed entirely); it now dynamically imported
footprint/index.mjs and constructed a real createSystemCollector() for
nothing, contradicting the doc comment two lines above and the plan's
hermeticity rule.

Collector construction is now skipped outright when deps.refreshStages is
injected: the half-injection guard already guarantees deps.service is
present in that case, so nothing downstream would have read it anyway.

Also adds the one test the review's own wording implies but the diff didn't
yet prove: --refresh=live WITHOUT --only stays valid (runs the live stage,
reads service.report(), exits 0).
@pacphi
pacphi merged commit e957737 into main Sep 29, 2026
18 checks passed
@pacphi
pacphi deleted the feat/one-refresh-flag branch September 29, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant