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
12 changes: 12 additions & 0 deletions openspec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ branch, that branch carries exactly one change, and the pull request's
title is the change id, verbatim. Nothing has to be decoded to see what a
pull request is for.

**The branch carries the change id too, and that one is mechanical.** The
merge gate resolves the change from the branch — `--change
"$GITHUB_HEAD_REF"` in `quality.yml` — and a name no active change has is
skipped rather than guessed at, because archive, article and dependabot
pull requests legitimately match nothing. So a branch named anything else
does not fail the gate: it makes the gate pass without checking whether
the change owes anything. `local-llm-acp` landed on 2026-10-01 from a
branch named `fix-local-llm-acp`, with a green gate, owing a delegated
item it had not recorded — and the sweep then refused to archive it,
because the sweep reads the rule the gate had skipped. The title being
right does not help; the gate reads the branch.

**Archiving is a second pull request, and the product makes it.** Once a
change has landed with every task item closed, the workspace sweep
archives it together with every other such change, in one pull request
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
schema: spec-driven
created: 2026-10-02
follows:
- local-llm-acp
77 changes: 77 additions & 0 deletions openspec/changes/local-llm-acp-record-is-put-right/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Design

## Non-Goals

- Changing `isUnrecordedTask`, `describeTaskDebts` or `owesNothing`. The
rule is behaving as designed; the record was written somewhere the rule
does not read.
- Changing the merge gate's scoping rule.
- Re-verifying what 4.4 records, or rewording it.
- Any change to the command or event protocol. This change touches no
source file, so no command and no event is added, removed or altered,
and the `server`/`extension` adapters are untouched.

## Decisions

### Move the record, not the rule

`isUnrecordedTask` closes a delegated or human-only item only when the
indented lines under it carry something. That is a proxy for "somebody
came back and wrote what happened", and it is the only proxy available:
an item's own text already contains the instruction "record evidence in
this task", so a reader that counted the checkbox line could never tell a
discharged item from one that merely describes its own obligation.

Every other recorded item in this repository already writes under the
checkbox — including 6.4 of `local-llm-codes-in-process`, archived the
same week. `local-llm-acp` 4.4 is the outlier, not the rule.

**Rejected: teach the rule to accept a record on the checkbox line.**
It cannot be done without a marker, and inventing one ("a line
containing `Record,`") would be a new syntax to learn, enforced nowhere,
and silently absent from every item written before it. It would also
close items that write a long instruction and no evidence at all, which
is the single thing the rule exists to catch.

**Rejected: tick nothing and reopen 4.4.** The run happened and is
recorded with its id, its agent version and its outcome. Reopening it
would make the file say something false in the other direction — the
error `task-bookkeeping-catch-up` warned about — and would ask a person
to repeat a run whose result is already written down.

**Rejected: archive `local-llm-acp` by hand.** Nobody archives a
finished change by hand; the sweep does it (ADR 0035, ADR 0036).
Reaching past the sweep would hide the defect rather than fix it, and the
next change recorded this way would stall the same way with nothing to
point at.

### Say the branch-name rule where it has teeth

The gate reads `--change "$GITHUB_HEAD_REF"` and skips a name no active
change has. The runbook already requires one change per pull request and
the change id as the pull request's title; it did not say the *branch*
must carry the id, and a branch named `fix-local-llm-acp` turned the
merge gate into a check that passed without checking.

The fix here is one sentence in the runbook, next to the rule it
completes. The alternative — making an unmatched name a failure — is
**rejected in this change**: archive, article and dependabot pull
requests all legitimately match no change, so that rule needs a way to
tell those apart, and designing it here would smuggle a gate change into
a record correction.

## Risks / Trade-offs

- **The runbook sentence is prose, not a check.** A branch named
something else still silently skips the debt check tomorrow. Accepted
deliberately: the mechanical fix is a real design question (which
pull requests must match a change?) and is named as out of scope so it
keeps an owner, rather than being decided in passing here.
- **Moving text risks altering it.** Mitigated by moving the record
verbatim and diffing it: the change is whitespace and line breaks, no
word added or dropped.
- **The sweep might still refuse for a reason this did not cover.**
Mitigated by running the same `describeTaskDebts`/`owesNothing` the
sweep uses against the edited file, and requiring `owesNothing` to be
`true` before this change is called done, rather than inferring it from
the edit looking right.
87 changes: 87 additions & 0 deletions openspec/changes/local-llm-acp-record-is-put-right/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Put the `local-llm-acp` record right

## Why

`local-llm-acp` landed on 2026-10-01 in #803 (`5b6e8383`) with every box
ticked, and it is still sitting in `openspec/changes/`. It will stay
there: no amount of waiting archives it.

[ADR 0035](../../../docs/adr/0035-a-landed-change-is-archived-for-you.md)
has the workspace sweep archive a change that has landed owing nothing,
and "owing nothing" is one answer every reader takes — the sweep and the
merge gate alike — so that "finished" cannot mean two things. That answer
is `owesNothing(describeTaskDebts(...))` in `packages/core`, and for this
change it is `false`.

The item at fault is 4.4, `**Delegated to local-llm-acp**`. Its record —
the run id, the agent's version, the tool calls, the outcome — is real
and was written. It was written *on the checkbox line itself*, with
nothing indented beneath it. `isUnrecordedTask` reads a delegated item as
unrecorded when the lines continuing it are empty, whatever is on the
line, because nothing can mechanically tell a record appended to an
item's sentence from the item's own instructions. So the rule sees a
delegated item that claims a run and writes nothing, and holds the change
open.

Measured on 2026-10-02, at `513a5c96`:

- `describeTaskDebts` over the file reports `open: []` and
`unrecorded: ["4.4 ..."]`, so `owesNothing` is `false`.
- The merge gate, run the way CI runs it but with this change named —
`validate --change local-llm-acp` — returns `"ok": false` and lists 4.4
under `unrecordedItems`.

**Why it landed anyway.** The gate takes `--change "$GITHUB_HEAD_REF"`,
and the branch was named `fix-local-llm-acp` rather than `local-llm-acp`.
A name no active change has is skipped rather than guessed at
(`openspec-validate.ts`), so the debt check never ran. The gate was green
having checked nothing. The pull request's *title* was the change id; the
branch was not, and it is the branch the gate reads.

## What Changes

- 4.4's record moves to the lines under its checkbox, word for word.
Nothing is re-verified and nothing is reworded: the run happened, and
only where it is written was wrong.
- `openspec/README.md` says where a delegated or human-only record goes,
and that the branch carries the change id because the merge gate reads
the branch — the half of "one change is one pull request" that was
implicit until it cost this change its archiving.

## Capabilities

### New Capabilities

- None.

### Modified Capabilities

- `openspec-workbench`: a delegated or human-only item's record is
written under the item, where the rule that closes the item reads it.

## Impact

- `openspec/changes/local-llm-acp/tasks.md` and `openspec/README.md`.
- No source changes, no behaviour changes, no changeset.
- After this lands, the sweep archives `local-llm-acp` on its next pass.

## Explicitly out of scope

- **Teaching the rule to count a record on the checkbox line.** Nothing
mechanically separates "record evidence in this task" from the evidence
appended after it; the indented block is the deliberate proxy for
"somebody came back and wrote this", and every other recorded item in
this repository already uses it. Loosening the rule to accept a long
line would close items that say a great deal and record nothing.
- **Making the merge gate check every change whose `tasks.md` a pull
request touches.** It would have caught this one. It would also fail a
pull request that legitimately carries another change's freshly opened
proposal, whose items are all open by design. That is a rule with its
own blast radius and belongs in its own change, decided on purpose
rather than as a side effect of a record correction.
- **Re-running 4.4's verification.** The run is recorded with its id and
its outcome, and re-running it would prove nothing the record does not
already state.
- **Auditing the bookkeeping of other changes.** `local-llm-acp` is the
only other change active in this repository; there is no backlog here
to sweep.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## MODIFIED Requirements

### Requirement: A change's task record states what has actually been done

A change's task record SHALL reflect the state of the repository. Work
that has shipped SHALL be recorded as done, and work that has not SHALL
NOT be.

A verification item SHALL be recorded as done only after it has been
carried out, never in the same act as the work it verifies.

Where an item can only be carried out by a person, it SHALL remain open
until that person has carried it out, and SHALL NOT be inferred from
related evidence.

Where an item is marked as requiring a person or as delegated to an
agent, the evidence closing it SHALL be written in the lines under the
item, which is where the rule that closes the item reads. An item whose
evidence is written only on its own checkbox line SHALL be treated as
recording nothing, because an item's own text already states what it
obliges and nothing distinguishes that text from evidence appended to
it.

#### Scenario: Work has shipped

- **WHEN** a change's implementation is present in the default branch
- **THEN** its task record shows that work as done

#### Scenario: A verification item has not been run

- **WHEN** a verification item's checks have not been carried out
- **THEN** it remains open, whatever the state of the work it verifies

#### Scenario: An item only a person can carry out

- **WHEN** an item is marked as requiring a person
- **THEN** it stays open until that person reports it done, and passing
automated checks do not close it

#### Scenario: Partial evidence for a verification item

- **WHEN** part of what an item claims has been observed and part has not
- **THEN** the item stays open, rather than being closed on the observed
part

#### Scenario: Evidence written on the checkbox line alone

- **WHEN** a delegated or human-only item is ticked and its evidence is
written on the checkbox line, with nothing in the lines under it
- **THEN** the item counts as recording nothing, and the change owes it
and is not archived, until the evidence is written under the item
93 changes: 93 additions & 0 deletions openspec/changes/local-llm-acp-record-is-put-right/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Tasks

## 1. OpenSpec Artifacts

- [x] 1.1 Write `openspec/changes/local-llm-acp-record-is-put-right/proposal.md`
stating the measured cause (`owesNothing` false, 4.4 unrecorded) and the
branch-name skip that let it land; verify `openspec status --change
local-llm-acp-record-is-put-right` reports the proposal present.
Done 2026-10-02: the proposal records both measurements and names
`fix-local-llm-acp` as the branch the gate resolved nothing from.
- [x] 1.2 Write `openspec/changes/local-llm-acp-record-is-put-right/design.md`
with `Non-Goals`, decisions naming their rejected alternatives, and
`Risks / Trade-offs`; verify `openspec status --change
local-llm-acp-record-is-put-right` reports the design present.
Done 2026-10-02: three rejected alternatives are stated — teaching the
rule to read the checkbox line, reopening 4.4, and archiving by hand.
- [x] 1.3 Write
`openspec/changes/local-llm-acp-record-is-put-right/specs/openspec-workbench/spec.md`
modifying `A change's task record states what has actually been done`
so that a delegated or human-only item's evidence belongs under the
item; verify `openspec validate local-llm-acp-record-is-put-right
--strict` accepts the delta shape.
Done 2026-10-02: the requirement gains the paragraph and the scenario
`Evidence written on the checkbox line alone`; strict validation
passes.

## 2. The record

- [x] 2.1 In `openspec/changes/local-llm-acp/tasks.md`, move item 4.4's
record from the checkbox line to the indented lines beneath it. The
record is moved verbatim: `git diff --word-diff` shows no word added or
removed, only line breaks and indentation. Do not reword it, do not
re-tick it, and do not touch any other item.
Done 2026-10-02: `git diff --word-diff=porcelain` over the file,
filtered to genuine `+`/`-` word lines, printed nothing — the move is
whitespace only. No other item in the file is touched.
- [x] 2.2 `describeTaskDebts(parseTaskChecklist(...))` over the edited
`openspec/changes/local-llm-acp/tasks.md` reports `unrecorded: []` and
`open: []`, and `owesNothing` returns `true`. Record the output here —
the edit looking right is not evidence that the rule reads it.
Done 2026-10-02, running `packages/core`'s own
`parseTaskChecklist`/`describeTaskDebts`/`owesNothing` from source over
the edited file: `owesNothing: true`, `open: []`, `unrecorded: []`. The
same script against `main` at `513a5c96` returns `owesNothing: false`
with 4.4 in `unrecorded`.
- [x] 2.3 `npm run start --workspace @openspec-ui/cli -- validate --cwd
<this worktree> --change local-llm-acp` returns `"ok": true` with no
`unrecordedItems`, where the same command on `main` returns
`"ok": false`. This is the gate the sweep agrees with; record both
results here.
Done 2026-10-02. On `main` (`--cwd C:\Prog\OpenSpec-UI`): `"ok": false`,
`local-llm-acp` `"valid": false` with 4.4 under `unrecordedItems`, exit
code 1. In this worktree: `"ok": true`, both `local-llm-acp` and
`local-llm-acp-record-is-put-right` `"valid": true`, `failedItems` 0.

## 3. The runbook

- [x] 3.1 `openspec/README.md`: in "A branch ends with its pull request",
say that the branch carries the change id verbatim because the merge
gate resolves the change from the branch, and that a branch named
anything else makes the gate skip the open-item check instead of
failing. Name `fix-local-llm-acp` as the measured case.
Done 2026-10-02: a paragraph added directly under "One change is one
pull request", naming `--change "$GITHUB_HEAD_REF"` in `quality.yml`,
why an unmatched name is skipped rather than failed, and the
2026-10-01 landing of `local-llm-acp` from `fix-local-llm-acp` with a
green gate.

## 4. Verification

- [x] 4.1 Run `npm run typecheck --workspaces --if-present` and record the
result here.
2026-10-02: exit 0, all five workspaces, no diagnostics.
- [x] 4.2 Run `npm run lint --workspaces --if-present` and record the
result here, including `lint:english` over the new markdown.
2026-10-02: exit 0 — 0 errors, the same 3 pre-existing unused-var
warnings in `packages/core` that `main` already has. Root `npm run
lint` also passed its script checks (test budgets, openspec config,
publish workflow, articles). `npm run lint:english`: "English policy
check passed."
- [x] 4.3 Run `npm run test --workspaces --if-present` and record the
result here. The relations test in `packages/core` must accept the
`follows: local-llm-acp` this change states.
2026-10-02: exit 0, nothing failed — cli 192, core 2011 and 62 in its
git-fixture project, extension 499, server 118, webui 687; 287 test
files, all passed. The relations test passed with `follows:
local-llm-acp` stated in `.openspec.yaml`, which is what proves the id
resolves.
- [x] 4.4 Run `openspec validate --changes --strict` and record that both
active changes pass.
2026-10-02: "change/local-llm-acp" and
"change/local-llm-acp-record-is-put-right" both tick; "Totals: 2
passed, 0 failed (2 items)".
22 changes: 21 additions & 1 deletion openspec/changes/local-llm-acp/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,27 @@
- [x] 4.1 Run `npm run typecheck --workspaces --if-present` and record success for this change. 2026-10-01, at the root after `git add`: exit 0.
- [x] 4.2 Run `npm run lint --workspaces --if-present` and record success for this change. 2026-10-01: exit 0.
- [x] 4.3 Run `npm run test --workspaces --if-present` and record success including new adapter tests. 2026-10-01, at the root after `git add`, unpiped: exit 0 (cli 192, core 1969, extension 118, server 498, webui 685).
- [x] 4.4 **Delegated to local-llm-acp**: execute one real ACP run that performs a coding task and record evidence in this task (run id and audit line showing ACP updates/tools path). Record, 2026-10-01: run `live-local-llm-acp-1790853181423`, `kind: "implement"`, through `buildDefaultAgentRunners(...).get("local-llm-acp")` from this branch, with `coding-agent` 0.3.0 (the first that speaks the Agent Client Protocol; its change `2026-10-01-agent-client-protocol`) on the PATH and the local LLM settings from the environment: SGLang at `http://192.168.137.33:8000/v1`, model `QuantTrio/Qwen3.6-35B-A3B-AWQ`, the key in `OPENSPEC_UI_LOCAL_LLM_API_KEY`, `OPENSPEC_UI_LOCAL_LLM_ACP_MAX_ITERATIONS=40` and `..._MAX_SECONDS=900`. The task: a scratch repository's change `greeting-takes-a-name`, four open tasks. In 70 s the driver produced `started`, 21 `tool_call` updates and their `tool_call_update`s (list_dir, read_file, replace_text, write_file, run_command `node --test`), the agent's summary as `agent_message_chunk`, `usageReported` (inputTokens 122745, outputTokens 545) and `completed`. Afterwards `node --test` passes 5 of 5, `greet('Ada')` is `Hello, Ada`, and the change's four tasks are ticked. Two earlier runs that day found two faults in `coding-agent`, fixed in that change: SGLang's `"tool_calls": null`, and Qwen3.6's calls passed through as text by SGLang's `hermes` parser.
- [x] 4.4 **Delegated to local-llm-acp**: execute one real ACP run that performs a coding task and record evidence in this task (run id and audit line showing ACP updates/tools path).
Record, 2026-10-01: run `live-local-llm-acp-1790853181423`,
`kind: "implement"`, through
`buildDefaultAgentRunners(...).get("local-llm-acp")` from this branch,
with `coding-agent` 0.3.0 (the first that speaks the Agent Client
Protocol; its change `2026-10-01-agent-client-protocol`) on the PATH
and the local LLM settings from the environment: SGLang at
`http://192.168.137.33:8000/v1`, model `QuantTrio/Qwen3.6-35B-A3B-AWQ`,
the key in `OPENSPEC_UI_LOCAL_LLM_API_KEY`,
`OPENSPEC_UI_LOCAL_LLM_ACP_MAX_ITERATIONS=40` and
`..._MAX_SECONDS=900`. The task: a scratch repository's change
`greeting-takes-a-name`, four open tasks. In 70 s the driver produced
`started`, 21 `tool_call` updates and their `tool_call_update`s
(list_dir, read_file, replace_text, write_file, run_command
`node --test`), the agent's summary as `agent_message_chunk`,
`usageReported` (inputTokens 122745, outputTokens 545) and
`completed`. Afterwards `node --test` passes 5 of 5, `greet('Ada')` is
`Hello, Ada`, and the change's four tasks are ticked. Two earlier runs
that day found two faults in `coding-agent`, fixed in that change:
SGLang's `"tool_calls": null`, and Qwen3.6's calls passed through as
text by SGLang's `hermes` parser.

## 5. Documents

Expand Down
Loading