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 +114 -264
- package/dist/check/check.js +63 -23
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands.js +32 -5
- package/dist/cli/help.js +7 -2
- package/dist/cli/workflow.js +8 -3
- package/dist/cli.js +45 -6
- package/dist/graph/bundle.js +20 -6
- package/dist/journal/journal.js +65 -0
- package/dist/journal/note.js +54 -0
- package/dist/journal/report.js +115 -0
- package/dist/journal/signals.js +26 -0
- package/dist/loop/fails.js +61 -10
- package/dist/main.js +16 -1
- package/dist/tasks/evidence.js +3 -3
- package/dist/util/redact.js +57 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|

|
|
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
|
|
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
|

|
|
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
|
|
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
|

|
|
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.
|
|
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
|

|
|
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.
|
|
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
|

|
|
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`.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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.
|
|
393
|
-
- Archived: 001 to 025, 027 to 034, and
|
|
394
|
-
-
|
|
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-
|
|
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.
|
|
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.
|
|
406
|
-
- `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks.
|
|
407
|
-
- `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks.
|
|
408
|
-
- `cycle_time_median_s`: the median
|
|
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.
|
|
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
|
|
package/dist/check/check.js
CHANGED
|
@@ -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
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
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" },
|