Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
ca40439
docs(openspec): propose the-agent-asks-the-operator
VeryComplexAndLongName Oct 7, 2026
a3d621b
feat(core): an agent asks the operator, and the run waits for the answer
VeryComplexAndLongName Oct 8, 2026
5a03009
the-agent-asks-the-operator: answers from every surface, docs and checks
VeryComplexAndLongName Oct 8, 2026
84e8c5e
the-agent-asks-the-operator: record the gate after rebasing onto main
VeryComplexAndLongName Oct 8, 2026
dc12fce
the-agent-asks-the-operator: refuse past a question asked in the chan…
VeryComplexAndLongName Oct 8, 2026
2b95e52
the-agent-asks-the-operator: allow the copilot-cli fallback prompt it…
VeryComplexAndLongName Oct 8, 2026
f74e4f9
the-agent-asks-the-operator: typing an answer in the run panel no lon…
VeryComplexAndLongName Oct 8, 2026
73d4071
Run and chain panels offer every pending permission request
VeryComplexAndLongName Oct 8, 2026
edad103
A permission request the agent withdraws leaves the panels
VeryComplexAndLongName Oct 8, 2026
1f25d0c
local-llm-acp is told to ask with ask_operator
VeryComplexAndLongName Oct 8, 2026
3bd2904
A question answered while the agent waits in its turn leaves the run …
VeryComplexAndLongName Oct 8, 2026
2544b2e
Question ids stay unique across a chain's stages; the card's answer f…
VeryComplexAndLongName Oct 8, 2026
85b2522
A permission answered anywhere leaves every panel
VeryComplexAndLongName Oct 8, 2026
9395aec
Every open permission request is held until settled; each answer is s…
VeryComplexAndLongName Oct 8, 2026
cfaf747
the-agent-asks-the-operator: 4.4 performed by the operator
VeryComplexAndLongName Oct 8, 2026
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
9 changes: 9 additions & 0 deletions .changeset/the-agent-asks-the-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@openspec-ui/core": minor
"@openspec-ui/webui": minor
"@openspec-ui/server": minor
"@openspec-ui/cli": minor
"openspec-ui-vscode": minor
---

The agent asks the operator, and nothing goes on without an answer (the-agent-asks-the-operator, ADR 0042). Agent stages are told to print `Question for the operator: <question>` for each decision the change's files leave open, and `local-llm-acp` can ask mid-turn with its `ask_operator` tool. Each question is kept in the change's `decisions.md` and the audit log. A run that asked waits instead of completing, under every autonomy level, and once every question is answered it runs the stage again with the answers (`update` after `propose` or `review`). Agent runs on a change with an open question are refused, naming it. Questions are answered on the change's card, in the AI and chain panels, in the Human-Only Inbox, with `openspec-ui-cli answer`, or in `decisions.md` itself; `openspec-ui-cli status` and the supervisor say how to answer a waiting run. The run and chain panels also offer every permission request still pending, each with its own Allow and Deny, rather than only the latest: an agent that runs tool calls side by side asks for several at once. A new `permissionSettled` event says in the run's stream that a permission request was answered, on whichever surface, or withdrawn by the agent (it cancelled the request, or its turn ended with it open), and the request leaves every panel, as do the requests of a stage that has ended.
36 changes: 36 additions & 0 deletions HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ it — start there and come back here for the detail.
| Find out what would stop a run here | — | `openspec-ui-cli doctor`, and `doctor --change <id>` for one change |
| Learn why a run failed, or whether one has gone quiet | — | [`supervisor`](#supervisor), and `openspec-ui-cli advise` |
| Close a task, or open a change's tasks, from its card | [How to](docs/how-to/close-a-task-from-its-card.md) | `openspec-ui-cli task` |
| Answer a question an agent asked | — | [A question for the operator](#a-question-for-the-operator), and `openspec-ui-cli answer` |

The harness sequences CLI-agent runs (or a mechanical action) across the
stages of one OpenSpec change: `propose → review → apply → verify →
Expand Down Expand Up @@ -82,6 +83,41 @@ them coherent, runs `openspec validate <name> --strict`, and changes no
code. Its reply says what it changed and which findings it left, and why.
`propose` still writes only the artifacts a change does not have yet.

### A question for the operator

An agent stage (`propose`, `review`, `update`, `apply`, `verify`) is told
not to decide what the change's files leave open, and to print a line of
its own for each such decision: `Question for the operator: <question>`
(ADR 0042). Only that line is read, as the verdict line is; prose is not.
`local-llm-acp` is told to ask in the middle of its turn instead, with its
`ask_operator` tool, and goes on with the answer as the tool's result.

- **Where it is kept.** Each question is appended to the change's
`decisions.md` - `## Q-<run>-<n>: <question>`, who asked it and when, and
`- Answer: (open)` - in the directory the run works in, usually the
change's own worktree. The answer replaces `(open)`, with who gave it and
when; the audit log records both. The file is committed with the change.
- **The wait.** A run whose turn ended with a question still open does not
complete, under every autonomy level, alone or in a chain: its status
says it waits for your answer, and the chain does not start its next
stage. The stage's `timeout` does not count the wait. When every
question is answered, the stage runs again with the questions and
answers in its prompt - `update` after `propose` or `review`, the same
command after `apply` or `verify` - and the run or the chain goes on from
there. A cancel ends the wait; the questions stay open.
- **The refusal.** While a change's `decisions.md` holds an open question,
in the checkout or in the change's own worktree, an agent run on it is
refused, naming the question. Read-only commands are not.
- **Answering.** On the change's card (**Answer...**), in the AI or chain
panel watching the run, in the Human-Only Inbox, with
`openspec-ui-cli answer <change> <Q-id> "<answer>"` (`answer <change>`
alone lists its open questions), or by replacing `(open)` in
`decisions.md` by hand. A waiting run reads the file every few seconds,
so an answer from another host or a terminal reaches it too.
`openspec-ui-cli status` prints each waiting run's questions with the
command that answers them, and the supervisor points out a run that has
waited past `supervisor.waitingAfterSeconds`.

## Two configuration files

- **Global**: `openspec/agent-harness.json` — the workspace-wide default.
Expand Down
86 changes: 86 additions & 0 deletions docs/adr/0042-the-agent-asks-the-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 0042: The Agent Asks the Operator, and Nothing Goes On Without an Answer

Status: Accepted

Date: 2026-10-07

## Context

A tester on 2026-10-07: "Running propose again ... visualises that the
operator (me) must decide on four questions. Normally, I would write these
into the chat window - but how is that conceived to work here?" It is not.

The product can already stop and wait for a person in two places: a
checkpoint between stages under `semi-autonomous`, and an ACP agent's
permission request. Both block until answered, are written to the run's
status record as `waiting`, and are pointed out by the supervisor when they
wait long (ADR 0039). It can hear an agent through a marker line, `Starting
task <n>` (ADR 0029). It can carry the operator's words to an agent at the
start of a stage (the-operator-can-say-something-to-a-run).

What it cannot do is notice that an agent needs a decision. The questions
live in the prose of a reply, the run completes, and a chain goes on to the
next stage with them unanswered. An external CLI agent cannot be paused in
the middle of its turn for a typed answer: Claude CLI runs non-interactive,
and ACP has a permission request but no question. The product's own agent,
`local-llm-acp` (ADR 0038), runs in process, and can.

The owner decided on 2026-10-07: the answers are kept in the change's
`decisions.md` and in the audit log; any run, in a chain or by itself, that
raises a question stops and waits for the answer; and the in-turn question
for `local-llm-acp` is built now, not later.

## Decision

1. **An agent asks with a marker.** Every agent stage's instruction says:
for each decision you need from the operator, print a line of its own,
`Question for the operator: <the question>`, and do not decide it
yourself. The product reads such lines from what the run says, as it
reads `Starting task` (ADR 0029). Prose is not read: no marker, no
question.
2. **`local-llm-acp` asks in its turn.** It gets a tool, `ask_operator`,
whose call emits the same question and blocks the agent loop until the
answer arrives, which is then its result. Under every autonomy level.
3. **A question is an event, an answer a command.** A non-terminal event
`question` (question id, text) and a command `answerQuestion`
(question id, answer), as `permissionRequest` and
`resolvePermission` are. The run's status record says `waiting` with a
kind `question`, so the card, `status` and the supervisor see it.
4. **A run that asked stops and waits.** A run whose turn ended with
unanswered questions does not complete and a chain does not start its
next stage: the run waits, as at a checkpoint, under every autonomy
level, for a single run as for a chain. When every question is answered,
the stage runs again with the questions and answers in its prompt -
`update` for `propose` and `review` (ADR 0041), the same command for
`apply` and `verify` - and the chain goes on from there. A cancel ends
the wait as it ends any.
5. **`decisions.md` holds them; nothing starts past them.** Each question
is written to `openspec/changes/<change>/decisions.md` when it is asked,
with its stage, run, time and `Answer: (open)`, and its answer, who gave
it and when, when it is answered; the audit log records both. The file
is the source of truth across restarts: an agent run on a change whose
`decisions.md` holds an open question is refused, naming the question,
until it is answered. Read-only commands are not refused.
6. **Answered from every surface.** The card's "Waiting on you" and the AI
panel show each open question with a field for its answer; the
Human-Only Inbox lists them; `openspec-ui-cli answer <change>
<question-id> <text>` answers from a terminal. An answer is written to
`decisions.md` in the run's own working directory, and a run that waits
reads it there, in whichever host it runs: the file is the channel.

## Consequences

- Work stops where it needs a person, and says so where people look.
- A change carries its decisions in a file that is reviewed with it.
- The protocol gains an event, a command and a waiting kind.
- An agent that ignores the instruction decides alone, as today; the
marker makes the difference visible, not impossible.

## Alternatives considered

- **Read questions out of prose.** Rejected: "Whether to keep the target
filter" is a question to a reader and a statement to a parser.
- **Only show the questions, and let work go on.** Rejected by the owner:
nothing goes on without an answer.
- **Answers in the audit log only.** Rejected by the owner: decisions are
part of the change, reviewed with it.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Alternatives / Consequences.
| [0039](0039-the-supervisor-advises.md) | The supervisor advises, and acts only where a change allows it | Accepted |
| [0040](0040-local-models-research-the-web.md) | Local models research public HTML pages through bounded web tools | Accepted |
| [0041](0041-the-plan-is-updated-from-its-review.md) | The plan is updated from its review | Accepted |
| [0042](0042-the-agent-asks-the-operator.md) | The agent asks the operator, and nothing goes on without an answer | Accepted |
| [0043](0043-a-change-is-made-in-a-working-directory-of-its-own.md) | A change is made in a working directory of its own, and every agent is told so | Accepted; amends 0022 decision 3 |

New architecture-impacting changes must add an ADR and reference it from the related OpenSpec change.
2 changes: 2 additions & 0 deletions openspec/changes/the-agent-asks-the-operator/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-07
109 changes: 109 additions & 0 deletions openspec/changes/the-agent-asks-the-operator/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
## Context

See ADR 0042. What exists: `permissionRequest`/`resolvePermission` and the
checkpoint as blocking waits, with `waiting` in the status record
(`agent-status.ts`); the `Starting task` marker read from a run's stream
(`task-marker.ts`, `agent-status.ts`); operator messages handed to the next
stage (`buildOperatorMessagesSection`); the run's message channel between
hosts (ADR 0028); the Human-Only Inbox over `tasks.md`; the supervisor's
"run waits on you" suggestion.

## Decisions

1. **The marker.** Every agent stage's instruction (`plan`, `review`,
`update`, `implement`, `verify`) ends with: "If you need a decision from
the operator that the change's files do not make, do not make it
yourself: print a line of its own reading `Question for the operator:
<the question>`, one line per question, with the options you see in
it, and stop before work that depends on the answer." `operator-
question.ts` reads it as `task-marker.ts` reads its marker: start of
line, list and quote marks and emphasis tolerated, the text after the
colon kept, an empty one ignored, the same text twice in one run read
once.
2. **The event and the command.** `question` (non-terminal): `questionId`
(`Q-<first 8 letters and digits of the run id>-<n>`) and `text`; the
stage, agent and time are in `decisions.md`, not repeated on the event.
`awaitingAnswers` lists the questions the run waits on when its turn
ends, and `questionAnswered` (`questionId`, `answer`, `by`) says each
answer was recorded. `answerQuestion`: `questionId`, `answer`; who
answered is the answering host's git identity. The runner emits
`question` as soon as it reads the marker, so the card shows it while
the agent still works; the status record's `waiting` becomes
`{ kind: "question", questions: [...] }` on `awaitingAnswers`.
3. **Waiting instead of completing.** `withOperatorQuestions`
(`operator-questions-runner.ts`) wraps every default runner, so the
agent runner itself is unchanged. It holds the agent's own `completed`
while any question the pass asked is still open, as the chain holds a
`failed` under `act` (the-supervisor-changes-agents): it emits
`awaitingAnswers`, and blocks on the answers, reading `decisions.md`
every 3 s and at once when an `answerQuestion` reaches the same runner.
When the last one is answered, it runs the stage again in the same run -
`update` for `plan` and `review`, the same command otherwise - with a
prompt section "The operator's answers" holding each question and
answer, and its outcome is the run's. Only questions still open when
the pass ends are waited on: a question `ask_operator` had answered in
the turn needs no second pass. A chain runs its stages through these
runners and so waits in the stage; its checkpoint logic is untouched,
and the stage's time limit does not count the wait. The terminal-event
contract (ADR 0012) holds: one terminal event, last.
4. **`ask_operator`.** A tool of `local-llm-acp` with one argument,
`question`. Its execution says the question as a marker line, so it is
recorded through the same path, and waits for its answer in
`decisions.md` (read every 1.5 s); the answer is the tool's result, and
the turn goes on. It is not offered under `local-llm` (text only).
`local-llm-acp`'s stage instruction tells it to ask with the tool rather
than print the line: told the line, the model printed it and ended its
turn (live, 2026-10-08), which still works but costs a second pass.
5. **`decisions.md`.** `decisions-file.ts` appends, never rewrites another
entry:

```
## Q-<run>-<n>: <question>

- Asked: <time> by <agent>, stage <stage>, run <run id>
- Answer: (open)
```

and on answer replaces that entry's `Answer:` line with the answer,
followed by `- Answered: <time> by <who>`. Written in the change's own
worktree. The audit log records an entry for each question and each
answer, with an `operatorQuestion` field (`questionId`, `text`, and
`answer` and `by` once answered). `openQuestions(changeDir)` reads the
file.
6. **The refusal.** The same wrapper refuses an agent command (`plan`,
`review`, `update`, `implement`, `verify`) on a change whose
`decisions.md` holds an open question - in the command's own directory
or in the change's own worktree, so a run started from the checkout
does not go past a question its chain asked there - with "<change> has an open
question for the operator: <Q-id> "<text>". Answer it first - on the
change's card, with `openspec-ui-cli answer <change> <Q-id> "<answer>"`,
or in its decisions.md." A chain is refused at its first stage, which
runs through it. Read-only commands run.
7. **Surfaces.** A card with open questions offers **Answer...**, a field
per question; it reads them from the change's `decisions.md` through
the survey, so they show whether or not the run that asked is alive.
The AI panel and the chain panel show the questions of the run they
watch the same way. The Human-Only Inbox lists open questions from the
checkout's changes and from each change's own worktree, first, and
answers them (an inline **Answer This Question...** in VS Code, a field
in the standalone app). `openspec-ui-cli answer <change>` lists a
change's open questions and `answer <change> <Q-id> <text>` answers one;
`status` prints each waiting run's questions with that command. An
answer is written to `decisions.md` in the run's own working directory;
the waiting run reads it there, so the file is the channel between
hosts and across restarts, not the run's message channel.
8. **The supervisor.** A run waiting on a question is "waiting on a person"
(ADR 0039) and is pointed out after `waitingAfterSeconds`, with the
`answer` command beside `status`; under `act`, a waiting run is never
repeated or moved, and no stop is offered for it.

## Risks / Trade-offs

- **A run waits as long as nobody answers**, holding its process. The wait
is visible everywhere, the supervisor points it out, and a cancel ends
it; the question stays open in `decisions.md`.
- **Two hosts answer at once.** The first answer recorded wins; the second
is refused as answered, and said so.
- **An agent asks what the files already decide.** The operator answers
with a pointer; the instruction tells the agent to ask only what the
files do not decide.
54 changes: 54 additions & 0 deletions openspec/changes/the-agent-asks-the-operator/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Why

From a tester's feedback on 2026-10-07: an agent ended its run with four
decisions it needed from the operator, and the product offered no way to
answer them; a chain would have gone on to the next stage with them open.
The owner decided the same day: questions are caught, the run stops and
waits, nothing goes on without an answer, the answers are kept in the
change's `decisions.md` and in the audit log, and the product's own agent
asks in its turn from the start. ADR 0042 records the decision.

## What Changes

- Every agent stage's instruction asks for `Question for the operator: <…>`
lines for decisions the agent needs; the product reads them as markers.
- `local-llm-acp` gets an `ask_operator` tool that blocks its turn until
answered.
- A `question` event and an `answerQuestion` command; a `question` waiting
kind in the run's status.
- A run that asked waits instead of completing, in a chain or alone, under
every autonomy level; once answered, the stage runs again with the
answers (`update` for propose and review, the same command otherwise).
- `openspec/changes/<change>/decisions.md` holds every question and answer;
the audit log records both; an agent run on a change with an open
question is refused.
- Questions are answered from the card, the AI panel, the Human-Only Inbox
and `openspec-ui-cli answer`.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `execution-core`: the marker, the event and command, `decisions.md`, the
refusal of a run past an open question.
- `acp-agent-adapters`: `ask_operator` in `local-llm-acp`.
- `agentic-harness`: a chain waits on a question, and re-runs the stage
once answered.
- `shared-ui`: open questions and their answer fields on the card and in
the panel; the Inbox's questions.
- `ci-cli`: `openspec-ui-cli answer`, and questions in `status`.

## Impact

- `packages/core`: `protocol.ts`, `agents/shared.ts`,
`operator-question.ts` (new: marker reader), `decisions-file.ts` (new),
`agent-runner.ts`, `agent-status.ts`, `harness-chain-runner.ts`,
`agents/local-agent/tools.ts` and `agent-loop.ts`, the Human-Only Inbox
reader, `supervisor.ts`.
- `packages/webui`, `packages/extension`, `packages/server`, `packages/cli`.
- `docs/adr/0042-the-agent-asks-the-operator.md`, `HARNESS.md`.
- Depends on `the-plan-is-updated-from-its-review` for `update`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## ADDED Requirements

### Requirement: The product's own agent asks in its turn

`local-llm-acp` SHALL offer a tool `ask_operator` taking one argument,
`question`. Calling it SHALL emit a `question` event and write the question
to `decisions.md` as any question is, and SHALL block the agent's loop
until the question is answered; the answer SHALL be the tool's result, and
the turn SHALL go on. `local-llm` SHALL NOT offer it.

#### Scenario: Asking mid-turn

- **WHEN** `local-llm-acp` calls `ask_operator` with "Keep the target
filter?" and the operator answers "Yes"
- **THEN** the loop waits until the answer, the tool's result is "Yes", and
the agent continues its turn
Loading
Loading