Skip to content

docs: correct CLI contract drift on the --json surface - #85

Open
dichovsky wants to merge 1 commit into
mainfrom
docs/cli-contract-json-surface-drift
Open

docs: correct CLI contract drift on the --json surface#85
dichovsky wants to merge 1 commit into
mainfrom
docs/cli-contract-json-surface-drift

Conversation

@dichovsky

@dichovsky dichovsky commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes #28

docs/design/cli-contract.md is authority level 3 and the --json surface it describes is a declared stable contract, so a wrong statement in it is load-bearing: scripts written against the documented vocabulary mishandle real finding codes, an undocumented record type, two error-code causes, and a mispredicted browser side effect. All six drifts in the issue reproduce on current main. Per the authority order in docs/README.md, the code is the correct side of each — no FR-*/NFR-* requires a doctor Node finding (FR-K01, srs.md:706-709, enumerates diagnostics generically and names none) and none governs the browser opener — so this is a documentation correction with no behavior change.

What changed and why

1. Doctor finding codescli-contract.md:559-577. The closed list gained VERSION_FLOOR and RESUME_DRIFT, with both details shapes documented in the register the neighbouring SETUP_DRIFT sentence already uses: VERSION_FLOOR is warn with target / detected / minimum / verified_on (src/doctor.ts:131-141); RESUME_DRIFT is warn with session (src/doctor.ts:411-416). The doctor --system bullet (:66-71) also gained the VERSION_FLOOR case, since that is the passage a reader consults to learn what --system reports.

The "reuses the error-code vocabulary" sentence was rewritten, and it was materially more wrong than the issue alleges. doctor emits seventeen codes; only DEPENDENCY_MISSING, UNSUPPORTED_SCHEMA, INTEGRITY, TEAM_DRIFT, INVALID_CONFIG, and UNSAFE_PATH exist in the ErrorCode union (src/errors.ts:8-26). The rest are diagnostic-only, so the old claim was false for eleven codes, not just the two #28 names.

RESUME_DRIFT's description states only what doctor observes: collectResumeFindings (src/doctor.ts:399-417) checks for the file's existence and never parses or validates the clean-stop marker, and listResumableSessions (src/launcher/resume.ts:143-146) returns [] whenever any crew-owned session is live, while runTeamResume (:195-200) refuses only for a live session of that name. So the finding cannot promise that team resume would fail, and the prose no longer claims it does.

2. resume_result envelopecli-contract.md:691-698, immediately after the stop_result example. Field names and order match src/format.ts:289-296 (renderTeamResumeResult, :278) exactly, and FR-U49 independently specifies the same shape. The team resume bullet (:358) now names the record, for parity with the team stop bullet. The command itself was already specified at :343-351 — only the --json envelope was missing.

3. Console route listdocs/design/architecture.md:155. Added GET /api/resumable-sessions (src/ui/server.ts:258, handled at :606) to the enumerated GET routes. See Out of scope for why the resume POST is not named.

4. crew ui --json openercli-contract.md:385-390. src/ui/index.ts:118 gates the opener on !options.json && options.open, so --json alone never opens a browser; the contract attributed the skip to --no-open.

5. Implies-launch flagscli-contract.md:283-285. All seven flags (--workers, --task-file, --worktree, --no-worktree, --no-relay, --no-attach, --print) carry .implies({ launch: true }) at src/cli.ts:288, :293, :298, :306, :308, :310, :313, so crew team dev --print is valid — the spelling README.md:104 already uses. Confirmed empirically against the vendored commander 15.0.0, including the three negated flags, with --client/--json correctly not implying it. The grammar block at :266-268 is untouched: it shows the flags under the canonical --launch form, which is a subset, not a contradiction.

6. Error-code scopescli-contract.md:474 (UNSUPPORTED_PLATFORM now names the crew join --platform value, src/agents.ts:34-39) and :481 (LAUNCH_FAILED now names a Console server that fails to start, src/ui/index.ts:108). The Agent-lifecycle section did omit the code, as the issue says; it is added at :118-119.

Also, the doctor --system / Node rowcli-contract.md:63-71. SYSTEM_DEPENDENCIES = ['tmux', 'git'] (src/doctor.ts:49) and no Node finding exists, so "Node" is dropped and the passage states that the >=24.15 floor is enforced pre-command by the installed shim (assertNodeFloor, bin/crew.ts:25, before the dynamic import('../src/run.js'); NODE_FLOOR = '24.15.0', bare message, exit 1, no [CODE] prefix). This is the Acceptance's second branch. No Node check was added to src/doctor.ts.

Verification

Tests: none added or updated. Docs-only — two Markdown files, zero code — explicitly exempt under CONTRIBUTING.md ("tests are not applicable for docs-only changes") and .github/ISSUE_TRIAGE.md.

Run under Node 24.19.0 with the pinned toolchain, on the final pushed tree (rebase onto origin/main @ 97d7f88 was a no-op — the branch was already on the tip):

Command Exit Result
npm run typecheck 0 clean
npm run lint 0 clean
npm run format:check 0 "All matched files use Prettier code style!"
npm run build 0 dist/ + dist/ui-assets/main.js
npm run test:coverage 1 1395 passed, 3 skipped, 2 failed — both pre-existing

Pre-existing failure proof. The two failures are tests/integration/commands/doctor.test.ts:319 and :373. Both chmod 000 a directory and expect the read to fail, which has no effect for uid 0; this sandbox runs as root. Proven rather than assumed: the change was stashed, git status --porcelain confirmed empty, and the file re-run on the clean tree produced the identical 2 failed / 31 passed.

Because that run failed, vitest suppressed the coverage table — the 95% thresholds were not evaluated locally. This PR's CI run (Node 24.18.0, non-root) is authoritative. The diff touches no src/** or bin/** file, so coverage cannot have moved.

Note on wrapping: the repo sets no proseWrap, so Prettier defaults to "preserve" and never reflows Markdown — format:check passes either way. The finding-codes paragraph was hand-wrapped to the file's ~95-column convention; the word-level diff confirms the adjacent SETUP_DRIFT prose is re-wrapped only, with no content change.

Review

Two reviewers ran per .github/ISSUE_TRIAGE.md: the general code reviewer, and — the diff being docs-only — a documentation-accuracy reviewer verifying the prose against the code it describes, in place of the TypeScript reviewer. Findings are posted in full on this PR: no CRITICAL, 1 HIGH, 4 MEDIUM, 9 LOW. The one allowed fix pass was taken and covered the HIGH plus nine other items; the HIGH was resolved by reverting this PR's edit to the human-output bullet at :768-769 (now byte-identical to main), because the false claims it carries are a distinct defect #28 does not name. Nothing CRITICAL or HIGH survives.

Out of scope

Deliberately untouched:

Distinct problems found during implementation and review, filed rather than folded in:

Related open PRs

None. Checked at file level: no other open PR touches docs/design/cli-contract.md or docs/design/architecture.md. (#81EXAMPLES.md, src/ui/server.ts — merged into main before this branch's final rebase.)

@dichovsky
dichovsky force-pushed the docs/cli-contract-json-surface-drift branch from 01e7565 to 809893b Compare August 6, 2026 15:51
@dichovsky
dichovsky marked this pull request as ready for review August 6, 2026 15:51

@dichovsky dichovsky left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routine review. Roster per .github/ISSUE_TRIAGE.md: the general code reviewer (always), plus — the diff being docs-only — a documentation-accuracy reviewer verifying the new prose against the code it describes and against the authority order, in place of the TypeScript reviewer. No security reviewer: the diff touches no src/store/, src/process.ts, src/which.ts, src/fs-safe.ts, src/setup/, or the Launcher.

CRITICAL: none. HIGH: 1. MEDIUM: 4. LOW: 9. One fix pass is being taken; what it does and does not cover is listed at the end.


HIGH

1. docs/design/cli-contract.md:767-769 — the human-output bullet this PR edited makes two false claims about crew ui. The bullet reads "crew ui prints the authenticated local-only URL and whether it opened the browser … --no-open reports that the browser was not opened", to which this PR appended "; --json opens no browser at all". Only the appended clause is true. renderUiStarted (src/format.ts:326-334) prints exactly two lines — Console listening at <url> (workspace <path>) and the token warning — and neither mentions the browser; runUi then calls the opener silently and swallows its failure with no output (src/ui/index.ts:118-124). crew's human surface never reports whether the browser was opened, and --no-open produces no output of its own.

MEDIUM

2. cli-contract.md:63-73 — the rewritten doctor --system bullet omits VERSION_FLOOR, the one system finding this same PR adds to the closed list. The bullet enumerates the DEPENDENCY_MISSING and SETUP_DRIFT cases but not installed-but-below-minimum, though it is the passage a reader consults to learn what --system reports.

3. cli-contract.md:570-572 — the RESUME_DRIFT description states a stronger guarantee than the code delivers. It says the finding reports "a cleanly stopped session … which team resume can no longer relaunch". Doctor never parses or validates the clean-stop marker (src/doctor.ts:406-417), and listResumableSessions returns [] whenever any crew-owned tmux session is live (src/launcher/resume.ts:143-146), whereas runTeamResume refuses only when a session of that name is live (:195-200). With one Crew running, every other stopped session is flagged RESUME_DRIFT even though crew team resume <session> would relaunch it. Overstating in a level-3 contract document is a real defect.

4. cli-contract.md:376-378 vs :480 — the LAUNCH_FAILED scope widening is only half-applied. The table row now names Console startup failure, but the crew ui section still says an invalid or unavailable explicit port "makes the command fail" without naming a code — the same omission the issue complained about for join/UNSUPPORTED_PLATFORM, which this PR did fix. It also elides that the two cases differ (USAGE for an invalid port value, LAUNCH_FAILED for an unavailable one).

5. architecture.md:155 + PR body — the POST /api/team/resume carve-out is honestly disclosed, but its stated authority rationale is wrong, and I wrote it. The PR body justifies the narrowing with "ADR-0012 (level 1) … still exclude[s] it". But docs/adr/0017-console-now-view-theme-and-agent-archive.md:76-79 — also accepted, also level 1, and later — says "The Console's action surface is no longer limited to Message/Task/Team/maintenance operations (FR-U19 amended); reviewers of future Console changes should treat FR-U19's enumeration, not 'whatever's already wired,' as the actual boundary". ADR-0017 therefore supersedes ADR-0012's closed list and delegates the boundary to FR-U19 — and since #64 merged, FR-U19 (srs.md:806-811) reads "…stopping a Team, resuming a Team, peeking at a pane…". The live conflict is not ADR-vs-route; it is cli-contract.md:400-402's "and nothing else" (level 3) contradicting FR-U19 (level 2), which by the authority order makes the CLI contract the bug. The carve-out itself stands on its own terms — that line enumerates GET routes and lumps every write endpoint as "the action POSTs", so no POST belongs there — but the reasoning offered for it was stale and a reviewer who accepted it as written would cement a wrong authority reading. This also means #25 is closer to resolved than the earlier retraction on that issue concluded.

LOW

  1. cli-contract.md:350-358 — the team resume bullet does not name its --json record, breaking parity with the team stop bullet ("--json emits one stop_result record").
  2. cli-contract.md:480 — the widened LAUNCH_FAILED cell is the only table cell containing two sentences; "the owned session is torn down. Also the Console server failing to start" can be misparsed as teardown applying to the Console.
  3. cli-contract.md:569-570 — "the verified_on date that minimum was checked" is ungrammatical.
  4. cli-contract.md:572-573 — the reflow leaves a short line mid-paragraph in a file otherwise filled to ~90 columns.
  5. cli-contract.md:386 — "the machine surface" is a coinage used nowhere else in docs/; the house phrasings are "machine output" and "the --json form … is the contract for machines".
  6. cli-contract.md:282-285 — the implies-launch bullet's second sentence restates the first, and "launch-only flag" is a new term where the file says "launch flags".
  7. cli-contract.md:564 — "Six of them" hard-codes a count into a closed list, adding a second thing to update whenever a finding code is added.
  8. architecture.md:155 — the GET enumeration this PR rewrote still omits /api/events (src/ui/server.ts:253-258, handled at :578-580); the list names four of six GET routes even after the fix.
  9. architecture.md:157 — the adjacent actions.ts row lists handlers but omits listResumableTeamSessions (src/ui/actions.ts:455), so the newly documented route has no handler named one line below. Pre-existing non-exhaustiveness (it also omits peekPane/archiveAgent/restoreAgent).
  10. src/doctor.ts:92 — source-side, not this PR's prose: the comment on collectParticipantFindings still says "Version is not a doctor finding — it is surfaced by crew setup", which collectVersionFloorFindings (:114-143) has since falsified.

Verified correct (not re-listed as findings)

The documentation reviewer independently re-derived, rather than trusting the PR body: the seventeen-code finding list is exactly what src/doctor.ts can emit — including the pushConfigReadFailure ternary at :340-341 — with nothing missing and nothing listed that doctor cannot emit; the six codes named as shared are exactly the intersection with the ErrorCode union (src/errors.ts:8-26), and all eleven others genuinely have no counterpart. Both new details shapes and severities match. The .crew/generated/<session>/resume.json path is correct against src/doctor.ts:401-409 and src/launcher/artifacts.ts:51-53. The resume_result example matches renderTeamResumeResult (src/format.ts:288-296) field-for-field, panes/relay/attached are literally the same LaunchResult object launch_result renders, and "exactly one … before attaching" holds (src/launcher/session.ts:285-293). --json does suppress the opener independent of --no-open. The Node passage checks out end to end: NODE_FLOOR = '24.15.0', enforced by assertNodeFloor before the app graph is imported, failing with a bare message and exit 1 and no [CODE] prefix. All seven implies-launch flags were confirmed empirically against the vendored commander 15.0.0, including the three negated ones, with --client/--json correctly not implying it. Both error-table rows and the new Agent-lifecycle sentence are reachable as described. Scope discipline is clean: no drive-by polish, no reformatting of untouched paragraphs, and CONTEXT.md vocabulary is used correctly throughout. FR-U49 independently specifies the same resume_result shape this PR documents.

What the fix pass covers

Taking findings 1, 2, 3, 6, 7, 8, 9, 10, 11, 12. Finding 1 is resolved by reverting the appended clause rather than correcting the bullet: the false pre-existing claims are a distinct defect #28 does not name — they concern what human output contains, not when the browser opens — so the scope rule sends them to their own issue rather than into this diff. The authoritative in-scope statement remains at :386-387.

Not folded in, filed separately instead: findings 4, 13, 14, 15. Finding 5 needs no code change — the carve-out stands; the PR body's rationale is corrected in place.


Generated by Claude Code

The `--json` output is a declared stable contract, so scripts written against
the documented vocabulary must be able to trust it. Six statements had drifted
from the implementation, each of which would mislead such a script:

- The doctor finding-code list omitted `VERSION_FLOOR` and `RESUME_DRIFT`, and
  claimed every finding code reuses the error-code vocabulary — only six of the
  seventeen have an `ErrorCode` counterpart.
- The `resume_result` envelope emitted by `crew team resume --json` was
  undocumented, and `/api/resumable-sessions` was missing from the Console
  route list in architecture.md.
- `crew ui --json` never opens a browser, but the contract attributed that skip
  to `--no-open` alone.
- `doctor --system` was said to check Node; it does not, and no requirement asks
  it to — the `>=24.15` floor is enforced pre-command by the executable shim, so
  the contract now says that instead. Its finding list also omitted the
  `VERSION_FLOOR` case the same command reports.
- Every launch flag implies `--launch` (so `crew team dev --print` is valid),
  which the contract documented only inside the `--launch` form.
- `UNSUPPORTED_PLATFORM` also covers `crew join --platform <unknown>` and
  `LAUNCH_FAILED` also covers a Console server that fails to start; neither
  cause appeared in the meaning table.

Per the authority order in docs/README.md the docs are the bug in all six
cases, so this corrects the documents and leaves the code untouched.

Closes #28

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

CLI contract drift: doctor finding codes, resume_result, ui --json opener, implies-launch flags, error-code scopes

2 participants