spec-ctx 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,17 +25,82 @@ Three things go wrong.
25
25
  | Each session starts cold. | `status`, `task next`, and the generated handoff say where the work stands. |
26
26
  | The agent ticks `[x]` by hand. | `[x]` appears only after the Verify command of the task exits 0. The run leaves an evidence file. |
27
27
 
28
- A later edit can break a task that passed. `check --strict` names each task whose files changed after its Verify passed.
28
+ A later edit can break a task that passed. `check --strict` names each task whose files changed after its Verify passed. While a change has an open task or no task, it prints a `deferred:` line for the delta items it did not test yet.
29
29
 
30
30
  No LLM call. No daemon. The same files give the same output.
31
31
 
32
+ ## Try it in two minutes
33
+
34
+ Paste this block in a shell. It builds a toy project, then runs the loop on one task. It needs Node 22, `git`, and `npx`.
35
+
36
+ <!-- try:begin -->
37
+ ```bash
38
+ set -e
39
+ mkdir -p spec-ctx-try && cd spec-ctx-try && git init -q
40
+ mkdir -p openspec/specs/user-auth openspec/changes/add-mfa/specs/user-auth src tests
41
+ cat > openspec/specs/user-auth/spec.md <<'MD'
42
+ # user-auth
43
+
44
+ ## Requirements
45
+
46
+ ### Requirement: Login with a password
47
+ The system SHALL let a user log in with a username and a password.
48
+
49
+ #### Scenario: Correct password
50
+ - **WHEN** a user enters a correct password
51
+ - **THEN** the user is logged in
52
+ MD
53
+ cat > openspec/changes/add-mfa/proposal.md <<'MD'
54
+ ## Why
55
+ Passwords alone are weak.
56
+
57
+ ## What Changes
58
+ - Ask for a TOTP code after the password.
59
+ MD
60
+ cat > openspec/changes/add-mfa/specs/user-auth/spec.md <<'MD'
61
+ ## ADDED Requirements
62
+
63
+ ### Requirement: TOTP second factor
64
+ The system SHALL ask for a TOTP code after a correct password.
65
+
66
+ #### Scenario: Correct code
67
+ - **WHEN** a user enters a correct TOTP code
68
+ - **THEN** the user is logged in
69
+ MD
70
+ cat > openspec/changes/add-mfa/tasks.md <<'MD'
71
+ ## 1. Login
72
+
73
+ - [ ] 1.1 Add the TOTP prompt to the login form
74
+ MD
75
+ printf '## Context\nThe login form has one step.\n' > openspec/changes/add-mfa/design.md
76
+ printf 'export const prompt = "password";\n' > src/login.ts
77
+ npx -y spec-ctx init --all
78
+ npx -y spec-ctx adopt
79
+ sed -i.bak 's/^- \[ \] 1.1 Add the TOTP prompt to the login form$/- [ ] 1.1 Add the TOTP prompt to the login form [REQ-UA-002] Verify: `grep -q totp src\/login.ts`/' openspec/changes/add-mfa/tasks.md && rm openspec/changes/add-mfa/tasks.md.bak
80
+ printf '// Covers: REQ-UA-002\n' > tests/login.test.ts
81
+ npx -y spec-ctx status
82
+ npx -y spec-ctx task next
83
+ npx -y spec-ctx task start add-mfa/1.1
84
+ npx -y spec-ctx task done add-mfa/1.1 || echo "refused, as expected"
85
+ printf '// REQ-UA-002\nexport const prompt = "password, then totp";\n' > src/login.ts
86
+ npx -y spec-ctx task done add-mfa/1.1
87
+ npx -y spec-ctx context add-mfa/1.1
88
+ npx -y spec-ctx context src/login.ts
89
+ npx -y spec-ctx handoff write
90
+ ```
91
+ <!-- try:end -->
92
+
93
+ On this toy project the bundle footer shows more than 100 percent of the spec text, because the tree is tiny. On a real repo the share is small. Step 4 of the next section shows 3.6 percent.
94
+
95
+ `status` prints a `next:` line that asks for plan approval with `handoff ask --plan`. The `specctx-run` skill handles that step. This demo skips it.
96
+
32
97
  ## See it work
33
98
 
34
99
  Six commands on a small project. Each clip is one command.
35
100
 
36
101
  ### 1. Give every requirement an ID
37
102
 
38
- OpenSpec names a requirement by its header text. A reword breaks every link to it. `adopt` gives each requirement and scenario an ID that stays. Run it once on a project that has specs. A new project gets IDs from `init --all`.
103
+ OpenSpec names a requirement by its header text. A reword breaks every link to it. `adopt` gives each requirement and scenario an ID that stays.
39
104
 
40
105
  ![The adopt dry run lists each header and its new ID](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/1-adopt.svg)
41
106
 
@@ -51,7 +116,7 @@ You see: one change, two tasks done, one open, and the hint.
51
116
 
52
117
  ### 3. What is next
53
118
 
54
- `task next` picks the first open task. It prints the requirement IDs the task cites and the Verify command, the shell command that proves the task is done.
119
+ `task next` picks the first open task. It prints the requirements the task cites and its Verify command, the shell command that proves the task is done.
55
120
 
56
121
  ![Task add-mfa/1.3 with its requirement and Verify command](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/3-next.svg)
57
122
 
@@ -59,7 +124,7 @@ You see: task `add-mfa/1.3`, its requirement, and `grep -q totp src/login.ts`.
59
124
 
60
125
  ### 4. Only the spec this task needs
61
126
 
62
- `task start` prints the `context` answer for the task. It holds the task line and the requirements it cites with their scenarios. It holds the code and test lines that tag them, the decisions, and the deltas and designs of its change. All of it fits a character budget. The last line is the size of that text next to the size of all spec text.
127
+ `task start` prints the context of the task. It holds the task line, the requirements it cites with their scenarios, the code and test lines that tag them, and the decisions. The blocks fit a character budget. The last line compares that size with all spec text.
63
128
 
64
129
  ![The bundle of one task and its size footer](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/4-start.svg)
65
130
 
@@ -67,7 +132,7 @@ You see: 711 characters, 3.6 percent of the tree. The agent reads this instead o
67
132
 
68
133
  ### 5. Done means Verify passed
69
134
 
70
- `task done` runs the Verify command. On exit 1 the box stays open. On exit 0 the box flips to `[x]` and an evidence file records the files that were checked. The agent cannot tick the box by hand.
135
+ `task done` runs the Verify command. On exit 1 the box stays open. On exit 0 the box flips to `[x]` and an evidence file records the files that were checked.
71
136
 
72
137
  ![A refused done, the edit, and a passing done](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/5-done.svg)
73
138
 
@@ -75,24 +140,30 @@ You see: a red refusal, the edit, then a green done line.
75
140
 
76
141
  ### 6. Hand off to the next session
77
142
 
78
- `handoff write` puts the state in one file: the change, the done tasks, the current task, and the open questions. The next session starts from it. The questions live in `.ctx/questions.json`. `handoff ask` adds one and `handoff resolve <n>` answers it. A plan or archive question needs `--approve` or `--reject`, and a rejection keeps the stop.
143
+ `handoff write` puts the state in one file: the change, the done tasks, the current task, and the open questions. The next session starts from it.
79
144
 
80
145
  ![The head of the handoff file](https://raw.githubusercontent.com/sonqh/spec-ctx/main/docs/demo/clips/6-handoff.svg)
81
146
 
82
147
  You see: the head of `.ctx/handoff.md`.
83
148
 
84
- The transcript of all six clips is in `docs/demo/loop.txt`. The clips run on a copy of `tests/fixtures/no-id-repo` plus the six capabilities in `docs/demo/specs/`. `bash docs/demo/record.sh` records them again.
149
+ The transcript of all six clips is in `docs/demo/loop.txt`. `bash docs/demo/record.sh` records them again.
85
150
 
86
151
  ## Quickstart
87
152
 
153
+ spec-ctx needs Node 22. Run it in a project after `openspec init`.
154
+
88
155
  ```bash
89
156
  npx -y spec-ctx init --all # config, ID templates, rules, and the agent skill
157
+ npx -y spec-ctx adopt # only when your specs have no IDs yet
90
158
  npx -y spec-ctx task next # the next task and the command that proves it
91
159
  npx -y spec-ctx task start <task> # only the spec that this task needs
92
160
  npx -y spec-ctx task done <task> # runs that command. A pass ticks the box.
93
161
  ```
94
162
 
95
- A project with specs but no IDs needs `specctx adopt` first. See [Two paths](#two-paths).
163
+ - `adopt --dry-run` shows the ID that each header gets and writes nothing.
164
+ - `adopt --dry-run` exits 1 when headers need IDs. A script with `set -e` must allow that code.
165
+ - [`docs/getting-started.md`](https://github.com/sonqh/spec-ctx/blob/main/docs/getting-started.md) walks the fresh path with real output.
166
+ - [`docs/reference.md`](https://github.com/sonqh/spec-ctx/blob/main/docs/reference.md) holds the install options, the path for an existing project, every command, and the config file.
96
167
 
97
168
  ## How it finds the context
98
169
 
@@ -105,29 +176,9 @@ graph LR
105
176
  R -->|"Covers:"| X["tests/login.test.ts"]
106
177
  ```
107
178
 
108
- The ID is the stable key. A reword of a header does not change it. The index is the graph of IDs: tasks, requirements, scenarios, decisions, code tags, and test tags. `bundle` walks the edges to the configured depth inside a character budget. A decision record joins the graph through its `Refs:` line. The demo fixture has none, so the graph above shows no decision.
179
+ The ID is the stable key. A reword of a header does not change it. The index is the graph of IDs: tasks, requirements, scenarios, decisions, code tags, and test tags. A decision record joins the graph through its `Refs:` line.
109
180
 
110
- ### Ask for the context
111
-
112
- `specctx context <task|ID|path|change>` is the one command to run before you edit. It takes one input and prints the spec text that relates to it, with one provenance line for each block. A task gets the requirements it cites and the code and test lines that tag them. It also gets the decisions that name them, and the deltas and designs of its change. A file gets the requirements it tags and the code and test lines of those requirements, so its own tests appear. `task start` prints this same answer. This is the output for task `045/2.1` of this repository, with the text of each block left out:
113
-
114
- ```text
115
- $ specctx context 045/2.1
116
- -- 045/2.1 | openspec/changes/archive/2026-10-30-045-query-by-file/tasks.md:11 | done | requested
117
- -- 045/D2 | openspec/changes/archive/2026-10-30-045-query-by-file/design.md:22 | archived | design named in 045/2.1
118
- -- 045/D3 | openspec/changes/archive/2026-10-30-045-query-by-file/design.md:23 | archived | design named in 045/2.1
119
- -- REQ-IDX-008 | openspec/specs/index/spec.md:133 | active | delta in change of 045/2.1
120
- -- REQ-IDX-009 | openspec/specs/index/spec.md:146 | active | delta in change of 045/2.1
121
- -- REQ-QRY-005 | openspec/specs/query/spec.md:76 | active | delta in change of 045/2.1
122
- -- REQ-QRY-006 | openspec/specs/query/spec.md:93 | active | ref from 045/2.1
123
- -- REQ-QRY-008 | openspec/specs/query/spec.md:106 | active | delta in change of 045/2.1
124
- -- src/graph/files.ts:1 | src/graph/files.ts:1 | active | implements REQ-QRY-006
125
- -- tests/query/file-edges.test.ts:1 | tests/query/file-edges.test.ts:1 | active | covers REQ-QRY-006
126
- ```
127
-
128
- `bundle` gives the same answer for one input. It also takes several inputs, `--block`, and `--all`. When the text passes the budget, blocks drop in this order. First go the archived decisions and the entries two steps away. Then go the specs. Then go the code and test lines. The input and the requirements it cites stay.
129
-
130
- `specctx related` prints each neighbor of an ID and the reason for the link. This is the output for the demo project after its last step:
181
+ `specctx context <task|ID|path|change>` is the one command to run before you edit. It walks the edges inside a character budget and prints one provenance line for each block. When a block does not fit, a `truncated:` line names it. `related` prints each neighbor and the reason for the link:
131
182
 
132
183
  ```text
133
184
  $ specctx related REQ-UA-004
@@ -138,212 +189,17 @@ code src/login.ts:1 active src/login.ts:1 implements REQ-UA-004
138
189
  test tests/login.test.ts:1 active tests/login.test.ts:1 covers REQ-UA-004
139
190
  ```
140
191
 
141
- ### Ask about a file
142
-
143
- `related`, `context`, and `bundle` take a file path where they take an ID. Run it before you edit a file. The reason on each line tells you how strong the link is. `implements` comes from a code tag. `covers` comes from a `Covers:` tag. `mentions` comes from a path in backticks in a task line of an active change. This is the output for one file of this repository:
144
-
145
- ```text
146
- $ specctx related src/util/hash.ts
147
- adr ADR-004 active decisions/ADR-004-tasks-md-is-the-view-evidence-is-the-record.md:1 ref from REQ-TASK-004
148
- req REQ-CHK-003 active openspec/specs/check/spec.md:64 ref to REQ-TASK-004
149
- req REQ-STAT-002 active openspec/specs/metrics/spec.md:19 ref to REQ-TASK-004
150
- req REQ-TASK-004 active openspec/specs/task-workflow/spec.md:89 implements, tag at src/util/hash.ts:1
151
- req REQ-VER-004 active openspec/specs/verify-runner/spec.md:37 ref to REQ-TASK-004
152
- scn SCN-TASK-004-1 active openspec/specs/task-workflow/spec.md:94 scenario of REQ-TASK-004
153
- task 002/3.3 done openspec/changes/archive/2026-10-24-002-ids-and-tasks/tasks.md:17 task cites REQ-TASK-004
154
- ```
155
-
156
- A path that is not a file in the repository exits 2. A file with no links prints `no links: <path>` and exits 0.
157
-
158
- ### Ask about a change or a task
159
-
160
- `related`, `context`, and `bundle` take a change key or a folder name where they take an ID. A change reaches the requirements and scenarios of its delta specs and the capabilities they touch. This is the output for an archived change of this repository:
161
-
162
- ```text
163
- $ specctx related 045
164
- req REQ-IDX-008 active openspec/specs/index/spec.md:133 delta in change/045
165
- req REQ-IDX-009 active openspec/specs/index/spec.md:146 delta in change/045
166
- req REQ-QRY-005 active openspec/specs/query/spec.md:76 delta in change/045
167
- req REQ-QRY-006 active openspec/specs/query/spec.md:93 delta in change/045
168
- req REQ-QRY-008 active openspec/specs/query/spec.md:106 delta in change/045
169
- scn SCN-IDX-008-1 active openspec/specs/index/spec.md:138 delta in change/045
170
- scn SCN-IDX-008-2 active openspec/specs/index/spec.md:142 delta in change/045
171
- scn SCN-IDX-009-1 active openspec/specs/index/spec.md:151 delta in change/045
172
- scn SCN-IDX-009-2 active openspec/specs/index/spec.md:155 delta in change/045
173
- scn SCN-QRY-005-1 active openspec/specs/query/spec.md:81 delta in change/045
174
- scn SCN-QRY-005-2 active openspec/specs/query/spec.md:85 delta in change/045
175
- scn SCN-QRY-005-3 active openspec/specs/query/spec.md:89 delta in change/045
176
- scn SCN-QRY-006-1 active openspec/specs/query/spec.md:98 delta in change/045
177
- scn SCN-QRY-006-3 active openspec/specs/query/spec.md:102 delta in change/045
178
- scn SCN-QRY-008-1 active openspec/specs/query/spec.md:111 delta in change/045
179
- scn SCN-QRY-008-2 active openspec/specs/query/spec.md:115 delta in change/045
180
- scn SCN-QRY-008-3 active openspec/specs/query/spec.md:119 delta in change/045
181
- scn SCN-QRY-008-4 active openspec/specs/query/spec.md:123 delta in change/045
182
- cap cap/index active openspec/specs/index/spec.md:1 capability touched by change/045
183
- cap cap/query active openspec/specs/query/spec.md:1 capability touched by change/045
184
- ```
185
-
186
- A task reaches the same delta entries and the design decisions that its line names, for example `specctx related 045/2.1`. A requirement reaches its capability, and `related cap/query` lists the requirements of one capability. A modified requirement shows its main version beside the delta. The index stores only the `change` and `cap` entries and the design links of tasks. The other edges come from the folder structure when you ask. `query` hides the `change` and `cap` entries unless you pass `--kind change` or `--kind cap`.
192
+ The reference has the budget rules and the lookups by file, by change, and by task.
187
193
 
188
194
  ## The life cycle for agents
189
195
 
190
196
  `init --all` installs one skill, `specctx-run`. The user types one prompt, such as "build this" or "continue". The skill loops until the change reaches the archive or a human must decide.
191
197
 
192
- - It runs `specctx next --json`. The engine reads the files on disk and prints one action: `plan`, `cite`, `start`, `work`, `fix`, `repair`, `gate`, `archive`, `ask`, or `stop`.
193
- - It acts on the action with the exact commands, runs `specctx handoff write`, and repeats.
194
- - It stops twice for a human by default: at the plan, and before archive. A Verify command that fails three times writes a question and stops too.
195
- - `specctx-bootstrap`, `specctx-next`, and `specctx-done` stay for one release as aliases. Each one says: run `specctx-run`.
196
-
197
- The commands the skill runs for one task:
198
-
199
- ```bash
200
- specctx init --all # config, ID templates, rules, and the skill
201
- specctx next --json # the next action, its task, and its command
202
- specctx task start <task> # the bundle of that task and its size
203
- specctx task done <task> # run Verify, write evidence, set the checkbox
204
- specctx gate --strict # check, baseline, linters, openspec validate, length
205
- specctx handoff write # .ctx/handoff.md for the next session
206
- ```
207
-
208
- The rule: the agent never opens `openspec/specs/` by hand. It uses `task start` or `context`. It never ticks a checkbox and never runs `/opsx:apply`. The skill is in [`skills/`](https://github.com/sonqh/spec-ctx/blob/main/skills/). Install it alone with `npx skills add sonqh/spec-ctx` or `specctx init --skills`.
209
-
210
- ## Install
211
-
212
- spec-ctx needs Node 22. Run it in a project after `openspec init`.
213
-
214
- ```bash
215
- npx -y spec-ctx init --all # from npm
216
- npm install -g spec-ctx # puts specctx on your path
217
- npx -y github:sonqh/spec-ctx#<sha> init --all # from git, for an unreleased commit
218
- ```
219
-
220
- The git form builds the package on install with Node only. Pin a commit SHA. A branch name such as `main` also works, but it moves.
198
+ 1. It runs `specctx next --json`. The engine reads the files on disk and prints one action, such as `start`, `fix`, or `archive`.
199
+ 2. It runs the exact commands of that action.
200
+ 3. It runs `specctx handoff write` and repeats.
221
201
 
222
- The command writes `spec-ctx.toml`, copies the templates with ID placeholders to `openspec/templates/spec-driven-ids/`, adds the spec-ctx `rules` and `operations` to `openspec/config.yaml`, and installs the `specctx-run` skill and its three aliases into `.claude/skills/` and `.agents/skills/`. The project keeps the stock `schema: spec-driven`. Upstream OpenSpec injects the rules into every artifact instruction. It pins the OpenSpec version it finds in `[gate] openspec`. It adds four lines to `.gitignore`: `.ctx/index.json`, `adopt-report.md`, `.ctx/current.json`, and `.ctx/gate-log.jsonl`. The rest of `.ctx/` is meant for git, `questions.json` included.
223
-
224
- The examples below use `specctx`. With `npx`, write `npx -y spec-ctx` in its place. Each GitHub release also carries a single binary for Linux and macOS.
225
-
226
- ## Two paths
227
-
228
- [`docs/getting-started.md`](https://github.com/sonqh/spec-ctx/blob/main/docs/getting-started.md) walks the fresh path on upstream OpenSpec with real output, for a developer who knows OpenSpec.
229
-
230
- ### Fresh project
231
-
232
- ```bash
233
- npx -y @fission-ai/openspec@latest init --tools <tool>
234
- npx spec-ctx init --all
235
- specctx gate --strict
236
- ```
237
-
238
- `--tools <tool>` names your agent, such as `claude`, and skips the interactive picker. `--no-animation` keeps logs clean. `init --all` merges the ID rules into an existing `rules` key of `openspec/config.yaml` item by item and prints `merged openspec/config.yaml rules (<n> added)`.
239
-
240
- ### Existing OpenSpec project
241
-
242
- ```bash
243
- npx spec-ctx init --all
244
- specctx adopt --dry-run # the ID that each header gets. Writes nothing.
245
- specctx adopt # writes the IDs into main specs and active deltas
246
- specctx baseline write
247
- specctx gate --strict
248
- ```
249
-
250
- - `adopt --dry-run` exits 1 when headers need IDs. A script with `set -e` must allow that code.
251
- - `adopt` gives an ID to every requirement and scenario header without one, in `openspec/specs/` and in the deltas of active changes. A delta header gets the main ID by its text or through its RENAMED line. A delta header that matches neither keeps its text and goes into `adopt-report.md`.
252
- - `adopt` derives a capability code from each folder name, for example `UA` for `user-auth`. It writes the codes into `spec-ctx.toml`. Rename a code there before the IDs spread.
253
- - `adopt` runs `check` before and after the write. A finding that the write added undoes every file. A second run prints `nothing to adopt`.
254
- - `baseline write` records the debt of the day in `.ctx/baseline.json`. The debt has five kinds. Ticked tasks without evidence. Legacy tasks without a citation or a Verify code span. Delta headers that match no main requirement. Hard lint findings per file and rule on the gate globs. `check` hides that debt and prints `baselined: <n>`. `gate` fails the lint step only when a count rises. New debt still shows.
255
- - `gate` compares `.ctx/baseline.json` with the copy at the merge base of `HEAD` and `[gate] base`, default `origin/main`. A new entry fails the `baseline` step. Commit a grown baseline with a subject that starts with `baseline:` to pass once. In CI, check out with `fetch-depth: 0`, so the merge base exists. Without git or a merge base the step prints `skipped`.
256
- - `[gate] markdown` in `spec-ctx.toml` names the markdown globs that `gate` lints. `init` writes `[]`, so the gate lints nothing until you opt in. The comment in the file names the globs this repository uses.
257
- - Legacy tasks stay in the baseline until an agent adds `[REQ-...]` citations and a `Verify:` code span. `specctx baseline prune` then drops them. No command adds a single entry.
258
-
259
- Both paths have an end to end test on upstream OpenSpec: `tests/paths/fresh.sh` and `tests/paths/existing.sh`. `tests/paths/rules-only.sh` shows that the merged `rules` reach the stock `spec-driven` schema.
260
-
261
- ## Commands
262
-
263
- | Command | Use |
264
- | --- | --- |
265
- | `build` | Write `.ctx/index.json`. Every other command rebuilds a stale index first. |
266
- | `status` | Active changes with task counts, the current task, and open questions. |
267
- | `next` | The next action of the loop from the files on disk: `plan`, `cite`, `start`, `work`, `fix`, `repair`, `gate`, `archive`, `ask`, or `stop`. `--json` prints one object. Flag: `--change`. |
268
- | `query <term...>` | Entries that hold every term. Flags: `--kind`, `--change`, `--limit`, `--all`. |
269
- | `context <task\|ID\|path\|change>` | The spec text, code lines, and test lines that relate to one input, in a budget. `task start` prints the same answer. Flags: `--budget`, `--depth`. |
270
- | `related <ID\|path\|change>` | Linked entries with the reason for each link. A file path or a change works too. Flags: `--depth`, `--all`. |
271
- | `bundle <ID\|path\|change...>` | The answer of `context` for several inputs. Flags: `--budget`, `--depth`, `--all`, `--block`. |
272
- | `check` | ID and link integrity. `--strict` adds test coverage and evidence. `retitle-needs-renamed` names a retitled delta header that needs a RENAMED pair before `openspec archive`. |
273
- | `id new`, `id retire` | Allocate and retire IDs. |
274
- | `task next`, `start`, `verify`, `done`, `reopen` | The task state machine. |
275
- | `handoff write`, `ask`, `resolve` | The session handoff and its open questions in `.ctx/questions.json`. |
276
- | `init` | Config file, ID templates and rules, and skills. Flags: `--schema`, `--skills`, `--all`, `--force`. |
277
- | `adopt` | IDs for a project without them. `--dry-run` writes nothing. |
278
- | `baseline write`, `prune` | The debt ratchet in `.ctx/baseline.json`. |
279
- | `gate` | `check`, the baseline against the merge base, both markdown linters, `openspec validate`, and the 500 character length check on active deltas. |
280
- | `lint tells`, `lint ste` | The two markdown linters. Flags: `--baseline`, `--disable`, `--json`, `--selftest`. |
281
- | `help <command>` | The page of one command. `specctx <command> --help` prints the same page. |
282
-
283
- Every command takes `--root <path>` and `--json`. A task ID takes the change key, such as `008/1.1` or `add-mfa/1.1`, or the full folder name. Outside an OpenSpec project every command but `init` exits 2. Exit codes: 0 for success, 1 for findings or a failed Verify command, 2 for a usage error, 3 for an internal error.
284
-
285
- ## Config reference
286
-
287
- `spec-ctx.toml` sits at the project root. Every key is optional.
288
-
289
- ```toml
290
- [scan]
291
- specs = ["openspec/specs/**/*.md"] # main specs
292
- changes = ["openspec/changes/**/*.md"] # active and archived changes
293
- decisions = ["decisions/*.md"] # decision records
294
- code = ["src/**"] # files with code tags
295
- tests = ["tests/**"] # files with Covers tags and scenario IDs
296
- ignore = [] # globs that the scan skips
297
-
298
- [ids]
299
- caps = [] # capability codes. Empty means any code is valid.
300
- task_pattern = '\d+\.\d+' # the task number in a tasks.md checkbox
301
- brackets = false # true: adopt, id new, and init --schema write [REQ-UA-001] in headers. The scan reads both forms.
302
-
303
- [ids.paths]
304
- UA = "user-auth" # capability path of each code. adopt writes the derived ones.
305
-
306
- [bundle]
307
- budget = 8000 # characters
308
- depth = 1
309
-
310
- [verify]
311
- timeout_s = 600
312
-
313
- [gate]
314
- markdown = [] # globs that gate lints. Empty means no lint. This repo opts in.
315
- openspec = "npx -y @fission-ai/openspec@1.15.0" # the command that runs OpenSpec. init pins the version it found.
316
- base = "origin/main" # the ref whose merge base holds the baseline that gate compares with
317
-
318
- [loop]
319
- max_attempts = 3 # Verify failures on one task before task done writes a question
320
- archive = "ask" # "ask" stops before archive for a human. "auto" lets the loop archive.
321
-
322
- [cli]
323
- run = "specctx" # the prefix of every command that spec-ctx prints. init writes npx -y spec-ctx@<version> under npx.
324
- ```
325
-
326
- With `brackets = true`, `init --schema` installs templates with `[REQ-<CAP>-NNN]` and `[SCN-<CAP>-NNN-N]` in the headers. The merged rules show the same form. Task citations, `Refs:` lines, and tags stay bare. After you change the value, run `specctx init --schema` again. It switches each template and rule that you did not edit. It keeps the ones you edited, unless you add `--force`.
327
-
328
- Two environment variables change the engine:
329
-
330
- | Variable | Effect |
331
- | --- | --- |
332
- | `SPECCTX_OPENSPEC` | The command that `gate` runs for `openspec validate` and `next` runs for `openspec status`. It overrides `[gate] openspec` for one run. At `init` time it is written into the file as it is, and no version detection runs. Tests and offline machines set it. |
333
- | `SPECCTX_BASELINE_SUBJECT` | The commit subject that the `baseline` step of `gate` tests for `baseline:`. It replaces the subject of `HEAD`. Tests set it. |
334
-
335
- ## Markdown linters
336
-
337
- `specctx lint tells` and `specctx lint ste` replace `scripts/tells-lint.py` and `scripts/ste-lint.py` from the ste-humanize skill. They take the same flags and give the same output and exit codes. They need Node only.
338
-
339
- ```bash
340
- # before
341
- python3 scripts/tells-lint.py --baseline 0 README.md
342
- python3 scripts/ste-lint.py --baseline 0 --disable synonym-rotation README.md
343
- # after
344
- npx spec-ctx lint tells --baseline 0 README.md
345
- npx spec-ctx lint ste --baseline 0 --disable synonym-rotation README.md
346
- ```
202
+ It stops for a human at the plan and before archive. A Verify command that fails three times writes a question and stops too. The agent never opens `openspec/specs/` by hand, never ticks a checkbox, and never runs `/opsx:apply`. `specctx-bootstrap`, `specctx-next`, and `specctx-done` stay for one release as aliases of `specctx-run`. The skill is in [`skills/`](https://github.com/sonqh/spec-ctx/blob/main/skills/).
347
203
 
348
204
  ## GitHub Action
349
205
 
@@ -354,29 +210,7 @@ npx spec-ctx lint ste --baseline 0 --disable synonym-rotation README.md
354
210
  strict: true
355
211
  ```
356
212
 
357
- The action runs `specctx gate`: `check --strict`, both markdown linters, `openspec validate --all --strict`, and the length check.
358
-
359
- ### Rerun
360
-
361
- ```yaml
362
- on: pull_request
363
- permissions:
364
- contents: read
365
- jobs:
366
- rerun:
367
- runs-on: ubuntu-latest
368
- steps:
369
- - uses: actions/checkout@v4
370
- with: { fetch-depth: 0 }
371
- - uses: sonqh/spec-ctx@v0
372
- with:
373
- rerun: true
374
- base: origin/${{ github.base_ref }}
375
- ```
376
-
377
- With `rerun: true` the action runs `specctx gate --rerun` and no other step. It runs the Verify command of each task that the pull request closed and compares the exit code with the evidence. `base` is the ref for the merge base. Without `rerun`, the action ignores `base`.
378
-
379
- On `push`, the default base gives a merge base equal to HEAD, so the rerun compares nothing. It prints one line and passes. Pass `base: ${{ github.event.before }}` on push. A push that creates a branch gives forty zeros. The rerun prints `rerun: base is the zero commit (a new branch), nothing to compare` and passes. After a force push the old commit may be missing from the clone. The rerun then prints `rerun: base <ref> is not in this clone. Fetch it or use fetch-depth: 0` and exits 1.
213
+ The action runs `specctx gate`: `check --strict`, both markdown linters, `openspec validate --all --strict`, and the length check. With `rerun: true` it runs the Verify command of each task that the pull request closed. The reference has the full workflow.
380
214
 
381
215
  A Verify command is code from the pull request. The boundary:
382
216
 
@@ -387,28 +221,44 @@ A Verify command is code from the pull request. The boundary:
387
221
  - For fork pull requests, turn on the repository setting that asks for approval before a workflow runs.
388
222
  - Check out with `fetch-depth: 0`. The merge base needs full history.
389
223
 
224
+ Tests and offline machines set `SPECCTX_OPENSPEC` to the command that runs OpenSpec.
225
+
390
226
  ## Status
391
227
 
392
- - Published: `spec-ctx@0.4.1` on npm, with tags `v0.4.1` and `v0` and binaries on the GitHub release.
393
- - Archived: 001 to 025, 027 to 034, and 036 to 049. No change is open. See `CHANGELOG.md` for each release.
394
- - A version bump on `main` publishes the next release. See `docs/CONTRIBUTING.md`.
395
- - Upstream: the ID scheme is proposed as a comment on [OpenSpec#1967](https://github.com/Fission-AI/OpenSpec/issues/1967#issuecomment-6414314730). The schema bundle is submitted to the community table in [OpenSpec#2157](https://github.com/Fission-AI/OpenSpec/issues/2157). No pull request until a maintainer asks.
228
+ - Published: `spec-ctx@0.5.0` on npm, with binaries on the GitHub release. See `CHANGELOG.md` for each release.
229
+ - Archived: 001 to 025, 027 to 034, 036 to 053, and 055 to 057. No change is open.
230
+ - Upstream: the ID scheme is proposed in [OpenSpec#1967](https://github.com/Fission-AI/OpenSpec/issues/1967#issuecomment-6414314730). The schema bundle is in the community table of [OpenSpec#2157](https://github.com/Fission-AI/OpenSpec/issues/2157).
396
231
 
397
232
  ## Measured
398
233
 
399
- `specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-10-30.md`. Its total row:
234
+ `specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-11-05.md`. Its total row:
400
235
 
401
236
  | change | context_ratio | evidence_ratio | stale_catches | cycle_time_median_s | debt |
402
237
  | --- | --- | --- | --- | --- | --- |
403
- | total | 0.0189 (n=135) | 0.94 (236/252) | 65 (of 118 reopens) | 60 (n=135) | 0 (0 baselined + 0 new) |
238
+ | total | 0.0164 (n=154) | 0.94 (257/273) | 65 (of 122 reopens) | 61 (n=154) | 0 (0 baselined + 0 new) |
404
239
 
405
- - `context_ratio`: the size of one task bundle divided by the size of all spec text. Lower is better. The meaning changed on 2026-10-30 with change 047-context. Before, `task start` printed the cited requirements. Now it prints the `context` answer, which holds more. `bundle_chars` in the evidence of older tasks keeps the old size, so the two do not compare.
406
- - `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. The rest have a manual note. Higher is better.
407
- - `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks. A higher share means the check found the drift before a person did.
408
- - `cycle_time_median_s`: the median number of seconds from `task start` to `task done`. Lower is faster.
240
+ - `context_ratio`: the size of one task bundle divided by the size of all spec text. Lower is better. Change 047 on 2026-10-30 made the bundle hold more, so older values do not compare.
241
+ - `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. Higher is better.
242
+ - `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks.
243
+ - `cycle_time_median_s`: the median seconds from `task start` to `task done`.
409
244
  - `debt`: findings that the baseline hides plus new findings. Zero is the goal.
410
245
 
411
- The value in brackets is the count of inputs. A value without inputs reads n/a.
246
+ The value in brackets is the count of inputs.
247
+
248
+ ## Report a bug
249
+
250
+ spec-ctx keeps a small run journal on your machine. It helps the team see where first runs fail. Nothing leaves your machine unless you file a report.
251
+
252
+ - What it holds: one JSON line per command in `.ctx/local/runs.jsonl`. Each line has the time, the version, the command name, the exit code, the run time, and signals such as `init-ok` or `empty-context`.
253
+ - What it never holds: flag values, paths, IDs, change names, spec text, or error messages.
254
+ - Where it is: `.ctx/local/` in the project root. A `.gitignore` of `*` in that folder keeps it out of git. The file rotates at 256 KB.
255
+ - How to turn it off: set `SPECCTX_JOURNAL=0`.
256
+ - How to file a bug:
257
+ 1. Run `specctx report > report.md`.
258
+ 2. Read `report.md`. Remove what you do not want to share.
259
+ 3. Run `gh issue create -R sonqh/spec-ctx --body-file report.md`.
260
+
261
+ `specctx report` only prints. It never runs `gh` and never uses the network.
412
262
 
413
263
  ## Contribute
414
264
 
@@ -122,6 +122,65 @@ export function weakVerifyFindings(entries) {
122
122
  export function weakVerifyWarnings(entries) {
123
123
  return weakVerifyFindings(entries).map((f) => `weak-verify ${f.id} ${f.file}:${f.line}`);
124
124
  }
125
+ /**
126
+ * REQ-CHK-010 REQ-CHK-008 REQ-CHK-009 one predicate for the delta test rule and the deferred notice (052/D1).
127
+ * A gap is a delta requirement or scenario of an active change that no test names. The rule reports the gaps of
128
+ * a change that has a task and no open task. The notice lists the gaps that an open task or a missing task hides.
129
+ */
130
+ export function deltaGaps(entries, data) {
131
+ const covered = new Set(entries.filter((e) => e.kind === "test").flatMap((e) => e.refs));
132
+ const named = new Set(data.scn_named ?? []);
133
+ // an ID that a REMOVED block of an active change names waits for archive, so it needs no test
134
+ const pending = new Set();
135
+ for (const e of entries) {
136
+ if (e.status !== "removed")
137
+ continue;
138
+ pending.add(e.id);
139
+ for (const sc of e.scenarios ?? [])
140
+ pending.add(sc);
141
+ }
142
+ const out = new Map();
143
+ const slot = (change) => {
144
+ let g = out.get(change);
145
+ if (!g)
146
+ out.set(change, (g = { open: 0, all: 0, gaps: [] }));
147
+ return g;
148
+ };
149
+ for (const e of entries) {
150
+ if (e.kind !== "task" || e.change === "main" || e.file.includes("/archive/"))
151
+ continue;
152
+ const g = slot(e.change);
153
+ g.all++;
154
+ if (e.status !== "done")
155
+ g.open++;
156
+ }
157
+ const seen = new Set();
158
+ for (const e of entries) {
159
+ if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
160
+ continue;
161
+ if (pending.has(e.id))
162
+ continue;
163
+ const rule = e.kind === "req" ? (covered.has(e.id) ? null : "uncovered") : named.has(e.id) ? null : "scenario-untested";
164
+ if (!rule)
165
+ continue;
166
+ const gap = [e.file, e.line, rule, e.id];
167
+ const key = e.change + JSON.stringify(gap);
168
+ if (seen.has(key))
169
+ continue;
170
+ seen.add(key);
171
+ slot(e.change).gaps.push(gap);
172
+ }
173
+ return out;
174
+ }
175
+ export function deferredOf(entries, data) {
176
+ const out = [];
177
+ for (const [change, g] of deltaGaps(entries, data)) {
178
+ // 052/D1 to include a change that has no task, the condition is `g.all === 0 || g.open > 0`
179
+ if ((g.all === 0 || g.open > 0) && g.gaps.length)
180
+ out.push({ change, untested: g.gaps.length, open: g.open });
181
+ }
182
+ return out.sort((x, y) => cmpTuple([x.change], [y.change]));
183
+ }
125
184
  /** Sorted, unique findings. The scan never reads .ctx/, so evidence adds no index entry. */
126
185
  export function check(root, cfg, data, strict) {
127
186
  const entries = data.entries;
@@ -190,29 +249,10 @@ export function check(root, cfg, data, strict) {
190
249
  if (e.kind === "scn" && !named.has(e.id))
191
250
  findings.push([e.file, e.line, "scenario-untested", e.id]);
192
251
  }
193
- // REQ-CHK-008 a delta scenario needs a test once its change has no open task (038/D1, D2)
194
- const tasksOf = new Map();
195
- for (const e of entries) {
196
- if (e.kind !== "task" || e.change === "main" || e.file.includes("/archive/"))
197
- continue;
198
- const t = tasksOf.get(e.change) ?? { open: 0, all: 0 };
199
- t.all++;
200
- if (e.status !== "done")
201
- t.open++;
202
- tasksOf.set(e.change, t);
203
- }
204
- for (const e of entries) {
205
- if ((e.kind !== "scn" && e.kind !== "req") || e.change === "main" || (e.status !== "active" && e.status !== "modified"))
206
- continue;
207
- const t = tasksOf.get(e.change);
208
- if (!t || t.open > 0 || pending.has(e.id))
209
- continue;
210
- // REQ-CHK-009 a delta requirement needs a Covers tag on the same terms (039/D2)
211
- if (e.kind === "req" && !covered.has(e.id))
212
- findings.push([e.file, e.line, "uncovered", e.id]);
213
- if (e.kind === "scn" && !named.has(e.id))
214
- findings.push([e.file, e.line, "scenario-untested", e.id]);
215
- }
252
+ // REQ-CHK-008 REQ-CHK-009 a delta item needs a test once its change has a task and no open task (038/D1, 038/D2, 039/D2)
253
+ for (const g of deltaGaps(entries, data).values())
254
+ if (g.open === 0 && g.all > 0)
255
+ findings.push(...g.gaps);
216
256
  findings.push(...evidenceFindings(root, entries, listEvidence(root)));
217
257
  }
218
258
  const uniq = new Map();
package/dist/cli/args.js CHANGED
@@ -10,6 +10,7 @@ export const COMMAND_FLAGS = {
10
10
  bundle: { budget: "int", depth: "int", all: "bool", block: "bool" },
11
11
  context: { budget: "int", depth: "int" },
12
12
  check: { strict: "bool" },
13
+ report: {},
13
14
  stats: { change: "string", write: "bool" },
14
15
  gate: { strict: "bool", markdown: "string", rerun: "bool", base: "string" },
15
16
  "id new": { for: "string", count: "int" },