Skip to content

Doctor reports every optional module with one module-unavailable finding #596

Closed

Description

@V3RON

Part of #591.

Scope

Replaces the doctor's agent-device-only finding with one finding kind for
every optional module (ADR 0030 §7). After this PR, simlock doctor
reports a module that is turned on but cannot run as module-unavailable,
with the module's name. The core doctor, the contract's doctor finding
and the CLI's doctor output name no module.

This task runs first in #591. It and #595 both bump the protocol in the
same lines, and it and #597 edit the same main.test.ts tests.

In short

The report simlock doctor prints on stdout, with an agent-device that is
too old.

Before:

{ "findings": [{ "kind": "agent-device-unavailable",
  "message": "found 0.20.3, need 0.21.25 or later",
  "remedy": "install agent-device 0.21.25 or later, then restart the daemon (`simlock daemon stop`, then `simlock daemon start`)" }] }

After:

{ "findings": [{ "kind": "module-unavailable", "module": "agent-device",
  "message": "found 0.20.3, need 0.21.25 or later",
  "remedy": "install agent-device 0.21.25 or later, then restart the daemon (`simlock daemon stop`, then `simlock daemon start`)" }] }

The line simlock doctor writes to stderr does not change:

agent-device  fail  found 0.20.3, need 0.21.25 or later; install agent-device 0.21.25 or later, then restart the daemon (`simlock daemon stop`, then `simlock daemon start`)

If this goes wrong: a module that cannot run is missing from
simlock doctor, or a client on the old protocol misreads the report.

Technical spec

Facts

# Fact Source
F1 The core's DoctorFinding has an agent-device-unavailable member with message and remedy; OptionalModuleFinding is that member. src/core/doctor.ts:119-128
F2 The doctor appends the findings optionalModules() returns, and logs and drops them when it throws. src/core/doctor.ts:157, :258, :571-576
F3 isDrift leaves agent-device-unavailable out of doctor.reconciled's driftFindings. src/core/doctor.ts:263-270, :1197-1204
F4 The contract's doctorFindingSchema has an agent-device-unavailable member. src/contract/schemas.ts:686, :746-750
F5 A contract test parses that member. src/contract/schemas.test.ts:659-668
F6 agentDeviceFindings builds the finding from the supervisor's availability, only when it is unavailable. src/agent-device/host/availability-findings.ts:4-14
F7 The CLI writes each agent-device-unavailable finding as agent-device fail <message>; <remedy>, after the advisory, prerequisite and warm-pool lines. It collapses each run of whitespace to one space and cuts each text to 500 characters. src/cli/index.ts:1090-1094, :1126-1133
F8 A CLI test pins F7's line, cut and order. src/cli/index.test.ts:1424
F9 Daemon tests assert the old kind through doctor.run. src/daemon/main.test.ts:2596-2664
F10 The slow-lane test waits for an agent-device-unavailable row, and checks that none is present. e2e/slow-agent-device.test.ts:203, :526-531
F11 docs/CLI.md documents the finding and its stderr line. docs/CLI.md:2199-2212
F12 The protocol range is { min: 36, max: 36 }, and three tests assert 36. src/contract/protocol.ts:13; src/daemon-protocol/index.test.ts:12; src/gateway/service.test.ts:1237; src/daemon/server.test.ts:219
F13 The doctor's report goes to the client unchecked, and the client parses it with the contract; a member that fails the schema fails the whole report. src/daemon/dispatcher.ts:737; src/simlock-client/client.ts:868-875
F14 A capability name, which is also a module's name, matches ^[a-z][a-z0-9-]{0,31}$. src/contract/schemas.ts:1054
F16 capabilityNameSchema is declared below doctorFindingSchema, and the protocol history table names the old kind for protocol 30. src/contract/schemas.ts:686, :1054; src/daemon/server.test.ts:200
F15 The supervisor's remedy for a too-old agent-device ends with the restart instruction. src/agent-device/host/agent-device-supervisor.ts:84, :231-232

Modules touched

flowchart LR
  ad["agent-device findings *"] --> core["core/doctor *"]
  core --> contract["contract/schemas *"]
  cli["cli/index *"] --> contract
Loading

Surface and extension points

Contract and event changes

doctorFindingSchema's member becomes
{ kind: z.literal("module-unavailable"), module: capabilityNameSchema, message: z.string(), remedy: z.string() }
(F4, F14). capabilityNameSchema moves above doctorFindingSchema in
src/contract/schemas.ts, which uses it at load. This PR bumps the
protocol by one. CHANGELOG.md gets a line
under Unreleased: agent-device-unavailable is now module-unavailable
with module.

doctor.reconciled does not change. The finding never enters its
driftFindings (F3). Searched both EVENTS.md files for the kind: no entry
names it.

Other code on the same state

Rules in play

  • architecture.md rule 21: the core doctor, the contract's doctor
    finding and the CLI's doctor output name no module.
  • testing.md rule 2: F10's not.toContain must look for a kind it could
    see.
  • documentation.md: docs/CLI.md (F11).

Tests

Seam: Doctor with optionalModules, modelled on src/core/doctor.test.ts.
Also doctorFindingSchema in src/contract/schemas.test.ts, simlock doctor in src/cli/index.test.ts, and startDaemon → doctor.run in
src/daemon/main.test.ts (F9).

  • a doctor run returns each module-unavailable finding optionalModules gives, unchanged
  • a module-unavailable finding is not in doctor.reconciled's driftFindings
  • doctorReportSchema parses a module-unavailable finding, and refuses one without module, without message or without remedy
  • doctorReportSchema refuses a module that fails the capability name check
  • doctorReportSchema refuses the old agent-device-unavailable kind
  • agentDeviceFindings returns module: "agent-device" with the supervisor's message and remedy when unavailable, and nothing when starting, running or off
  • F8's test, for writeModuleUnavailable: simlock doctor writes agent-device fail <message>; <remedy> on stderr, whitespace collapsed, at most 500 characters per part, after the advisory, prerequisite and warm-pool lines (as today)
  • simlock doctor writes <module> fail … for a finding of another module
  • F9's tests, now asserting { kind: "module-unavailable", module: "agent-device" } through doctor.run: too-old version, command not found, none when off, none while starting
  • the protocol version is one above the base branch's

Done when

  • Every Tests line passes.
  • git grep -n agent-device-unavailable -- src e2e ':(glob)docs/*.md' prints only
    the refusal test in src/contract/schemas.test.ts and the protocol 30
    row of the history table in src/daemon/server.test.ts (F16), which
    stays as history.
  • F10's two assertions name module-unavailable. On a Mac with an iOS
    runtime: scripts/slow-e2e.sh e2e/slow-agent-device.test.ts passes.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 15, 2026
  2. V3RON commented on Oct 15, 2026

    @V3RON
    ContributorAuthor

    Spec updated: findings from the first spec check folded in (order is now #596 → #595 → #597 → #598; split loaders, own registry component, routes under the module's name, concurrent stop steps per ADR 0030's amendment).

    Written by an agent.

  3. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 15, 2026
  4. self-assigned this
    on Oct 15, 2026
  5. V3RON commented on Oct 15, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done: PR #599 built tests-first. All 8 spec test files pass, CI is green, and review round 1 passed with 0 blocking findings. The PR is ready, labelled needs-hardware, and sits in the stack above ADR PR #592.

    Not done: Done when line 3, the slow lane scripts/slow-e2e.sh e2e/slow-agent-device.test.ts. It did not run.

    Findings: This Mac meets the lane's other checks (iOS 26.4 and 27.0, 36 GiB). But SIMLOCK_E2E_AGENT_DEVICE is not set and .slow-lane.env does not exist, so the lane has no built agent-device checkout to use.

    Blocked on: a hardware run of line 3 on a machine with an agent-device checkout at the commit vendor/README.md names. #595, #597 and #598 depend on this task, so they wait.

    Written by an agent.

  6. added
    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.
    and removed
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Oct 15, 2026
  7. removed their assignment
    on Oct 15, 2026
  8. added a commit that references this issue on Oct 15, 2026
    4a5654b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions