Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
schema: spec-driven
created: 2026-09-12
follows:
- a-lease-says-who
- a-change-runs-from-the-terminal
84 changes: 84 additions & 0 deletions openspec/changes/a-doctor-says-what-would-stop-a-run/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Design

## Decisions

**Which agents are present is asked of the detection this repository
already has.** `detectAvailableAgentsDetailed`
(`packages/core/src/agent-detection.ts`) probes every id in
`AGENT_REGISTRY` through the default allowlist and reports presence and,
for a CLI agent, the version its own `--version` gave. The REST route,
the VS Code bridge and `webui`'s agent picker all read it.
`readEnvironmentReport` calls that, and turns what it returns into
findings.

Corrected on 2026-09-12, before implementation: this proposal first said
a binary must be resolved and never executed, on the grounds that a
version probe is an invocation nobody allowlisted. That argument reads
well and the decision was wrong — not because the reasoning is bad, but
because the question is already answered in this repository, by code
several surfaces depend on. A second implementation that answered
"is this agent here" differently from the picker in the same build is
exactly the drift this proposal objects to one decision further down,
about the preflight. If the probe is the wrong mechanism, that is a
change to `agent-detection.ts` and to every surface reading it, not a
quiet second opinion inside a new command.

**For a named change, the answer is the preflight's own.**
`--change <id>` calls `resolveChainStart` with the same resolver the CLI
builds for a run and prints its `ChainStartRefusal` — reason and
`configKey` — rather than re-deriving those conditions. A second
implementation of "may this change start" would drift from the first,
and the drift would show up as a doctor that says yes to a run that is
then refused.

**Exit codes follow the CLI's existing contract.** `0` nothing found
would stop a run; `1` something would — the same code `run`, `check` and
`ready` use for "the thing you asked about did not pass"; `2` the report
itself could not be produced. A workspace held by a live run is **not**
a failure: it is a fact, reported, and the workspace is not broken.

Rejected: exiting non-zero for a held workspace. `lease` decided this
already — the question was answered either way — and two commands
disagreeing about whether being busy is a failure is worse than either
answer.

**Findings are a closed list, each with a remedy or nothing.** A finding
is `{ id, severity, statement, remedy? }`, `severity` one of `stops-a-run`
or `worth-knowing`. A missing `openspec` CLI stops a run; no git
identity configured is worth knowing (a lease taken without one is
valid, as `a-lease-says-who` established). Nothing is reported without a
severity, and nothing carries a remedy that is not a command this
repository has.

**No new report of the workspace lease.** Who holds it is read with
`readWorkspaceLeaseHolder` — the reader that never takes it — and shown
the way `lease` shows it.

## Non-Goals

- Installing, configuring, or repairing anything.
- Network or authentication checks.
- Agent version detection.
- A UI surface. Both hosts refuse a run with a reason already; whether
they should carry this report is a separate question and a separate
change.
- Reading or printing any environment variable's value.

## Risks / Trade-offs

**A report that says everything is fine, and a run then fails.** The
mitigation is the decision above: for a named change the report *is* the
preflight, so the two cannot disagree about that change. Without
`--change`, the report is explicitly about the machine and the
workspace, and says so — it does not claim any particular change will
run.

**A check nobody updates.** Every agent checked comes from
`AGENT_REGISTRY`, so an agent added later is covered without a second
list; the test asserts the report covers exactly the registry's
executables, which fails when a new agent is added and the report is
not.

**Protocol impact: none.** No command or event is added or changed; this
is a CLI command over existing core reads. `server` and `extension` are
untouched.
61 changes: 61 additions & 0 deletions openspec/changes/a-doctor-says-what-would-stop-a-run/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# A doctor says what would stop a run

## Why

`resolveChainStart` in `packages/core/src/chain-preflight.ts` already
resolves every precondition of a run before anything is spent — the
change exists, its configuration reads, its autonomy level starts a
chain, a confirmation can be answered, no blocker is unmet, every stage
resolves an agent this build carries. Its header says why: "an
unattended run cannot" discover a missing agent at the stage that needs
it, because the two stages already paid for were paid for nothing.

Every one of those answers is available only by naming a change and
asking to run it. A person setting this tool up for the first time, or
on a new machine, finds out what is missing by being refused — which is
precisely the shape `a-lease-says-who` rejected for the workspace lease:
"a strange way to ask a question, and one that only answers it at the
moment you are being told no."

Raised in review on 2026-09-12 as part of making the tool easier to
start using. What is missing is the same command the lease got: ask the
question directly.

## Capabilities

### New

- `openspec-ui-cli doctor`: what this machine and this workspace have,
and what would stop a run — the runtime against the pinned engines,
the `openspec` CLI, which agent binaries are on the PATH, whether the
workspace's harness configuration reads, who holds the workspace, and
whether a git identity is configured.
- `--change <id>`: the same report plus the real preflight for that
change, so the answer is the one the run would give, not a second
opinion.
- `--format json`, the same shape the reader returns.

### Modified

- Nothing. This reports on what exists; no setting, refusal or default
changes.

## Out of scope

Fixing anything. The doctor installs nothing, writes nothing and
configures nothing. Where a finding has a remedy the repository already
has a command for, it names that command.

Deciding for itself which agents are installed. That is
`detectAvailableAgentsDetailed`'s question, already answered for the
REST route, the VS Code bridge and the agent picker; this report asks it
rather than probing a second way — see `design.md`.

Network checks. Whether an agent's service is reachable, whether a
token is valid, whether GitHub is up: each is a question about a remote
system at one moment, and a report that says "reachable" is stale before
it is read. The doctor reports what is configured locally.

Anything about secrets. No environment variable's value, no token, no
credential is read or printed — a finding may name a variable, never
what it contains.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## ADDED Requirements

### Requirement: What would stop a run can be asked before starting one

It SHALL be possible to ask what this machine and this workspace are
missing, without naming a change and attempting to run it.

The answer SHALL cover the runtime against the versions this repository
pins, the presence of the OpenSpec CLI, the presence of each registered
agent's executable, whether the workspace's harness configuration reads,
who holds the workspace, and whether a git identity is configured.

Each finding SHALL state whether it stops a run or is only worth
knowing, and SHALL name a remedy where the repository has a command for
one.

A workspace held by a live run SHALL be reported as a fact and SHALL NOT
be reported as something that stops a run: a busy workspace is not a
broken one.

#### Scenario: A machine missing something a run needs

- **WHEN** something a run requires is absent
- **THEN** it is named, marked as stopping a run, and the command that
remedies it is named where one exists

#### Scenario: A workspace held by a live run

- **WHEN** a run holds the workspace
- **THEN** the holder is reported, and the report does not treat it as a
failure

### Requirement: An agent's presence is answered by the detection that already exists

Whether an agent is installed SHALL be answered by the same detection
every other surface of this product reads, and SHALL NOT be determined a
second way inside this report.

A command that answered "is this agent here" differently from the agent
picker in the same build would make the product disagree with itself,
and a person would have no way to tell which answer to believe. If the
way presence is detected is wrong, it is wrong for every surface and is
changed in one place.

#### Scenario: Reporting on an installed agent

- **WHEN** the report covers an agent whose executable is installed
- **THEN** it reports what the shared detection reports, including the
version where that detection has one

### Requirement: A report about one change is the preflight's own answer

Where the question is asked about a named change, the answer SHALL come
from the same resolution that would run it, including the setting that
governs any refusal — not from a second implementation of the same
conditions.

Two implementations of "may this change start" drift, and the drift
appears as a report that says yes to a run which is then refused.

#### Scenario: A change whose configuration refuses an unattended run

- **WHEN** the report is asked about a change whose configuration would
refuse to start here
- **THEN** it states that refusal's own reason and the setting that
governs it
83 changes: 83 additions & 0 deletions openspec/changes/a-doctor-says-what-would-stop-a-run/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Everything that stops a run is already decided in one place, and can
only be asked by starting a run and being refused.

## 1. The report

- [ ] 1.1 `packages/core/src/environment-report.ts` exports
`Finding` (`{ id, severity, statement, remedy? }`, `severity` one of
`"stops-a-run" | "worth-knowing"`) and
`readEnvironmentReport({ workspaceRoot }): Promise<EnvironmentReport>`.
- [ ] 1.2 The runtime: the running Node.js and npm versions against the
root `package.json`'s `engines`. Outside the range is `stops-a-run`,
with the pinned range quoted in the statement.
- [ ] 1.3 The `openspec` CLI: resolved on the PATH or not. Absent is
`stops-a-run` — `validate-change` and the `archive` stage both call
it.
- [ ] 1.4 Each agent in `AGENT_REGISTRY`: present or absent, from
`detectAvailableAgentsDetailed` in
`packages/core/src/agent-detection.ts`, including the version it
reports where it has one. Do not probe a second way: the picker, the
REST route and the VS Code bridge all read that function, and a
command answering "is this agent here" differently from the picker in
the same build is the drift this change objects to elsewhere.
- [ ] 1.5 The workspace's harness configuration: `resolveHarnessConfig`
on the global file reads, or the error it raised, as `stops-a-run`.
- [ ] 1.6 The workspace lease, via `readWorkspaceLeaseHolder`: who holds
it, or that it is free. Held is `worth-knowing`, never `stops-a-run`
— a busy workspace is not a broken one.
- [ ] 1.7 The git identity, via `readGitAuthor`: present or not. Absent
is `worth-knowing`, with the remedy naming `git config user.email`. A
lease taken without one is valid.
- [ ] 1.8 No environment variable's value is read or reported. A finding
may name a variable; it may not print what it contains.
- [ ] 1.9 `packages/core/src/environment-report.test.ts`: a report over
a fixture with everything present has no `stops-a-run` finding; a
missing `openspec` produces one; an out-of-range Node version produces
one quoting the range; a held lease produces a `worth-knowing`
finding; every executable in `AGENT_REGISTRY` appears in the report —
the assertion that fails when an agent is added and this is not
updated.

## 2. The command

- [ ] 2.1 `openspec-ui-cli doctor [--cwd <path>] [--change <id>]
[--format text|json]` in `packages/cli/src/doctor-command.ts`, wired
in `packages/cli/src/main.ts` with its own `USAGE` entry and its exit
codes documented there.
- [ ] 2.2 `--change <id>` additionally calls `resolveChainStart` with the
same resolver `runChange` builds, and prints its refusal's `reason`
and `configKey`. Do not re-derive any precondition it answers.
- [ ] 2.3 Exit `0` where no finding stops a run, `1` where one does, `2`
where the report could not be produced. A held workspace alone exits
`0`.
- [ ] 2.4 `--format json` prints the `EnvironmentReport` shape unchanged
from core.
- [ ] 2.5 `packages/cli/src/doctor-command.test.ts`: a clean report exits
0; a `stops-a-run` finding exits 1 and names the remedy; a held
workspace alone exits 0; `--change` on a change whose configuration
refuses prints the preflight's own reason and `configKey` and exits 1;
an unreadable workspace exits 2.

## 3. Documentation

- [ ] 3.1 `README.md`'s "CI CLI (merge gate)" section documents `doctor`
and its three exit codes.
- [ ] 3.2 `HARNESS.md`'s task index gains a row: "Find out what would
stop a run here" → `openspec-ui-cli doctor`.

## 4. Verification

- [ ] 4.1 This change validates strictly. `check(validate-change)`
- [ ] 4.2 `npm run verify` unpiped, after the last edit, with everything
staged. Record the run and the per-package test counts.
- [ ] 4.3 A changeset exists: `core` and `cli` minor.
`check(changeset-present)`
- [ ] 4.4 **Delegated to `claude-cli`**: run `openspec-ui-cli doctor` on
this repository and quote the output and exit code; then run it with
`PATH` stripped of `openspec` and quote the output and exit code.
Evidence: both outputs verbatim, showing the same command reporting a
healthy machine and a broken one.
- [ ] 4.5 **Delegated to `claude-cli`**: while a run holds the
workspace, run `openspec-ui-cli doctor` and quote the lease finding
and the exit code, which must be `0`. Evidence: the output and the
exit code.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
schema: spec-driven
created: 2026-09-12
follows:
- what-can-start-now
- changes-run-side-by-side
- a-graph-of-what-is-running
Loading
Loading