spec-ctx 0.4.1 → 0.6.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 +124 -264
- package/dist/adopt/plan.js +40 -7
- package/dist/adopt/write.js +27 -4
- package/dist/assets.generated.js +1 -1
- package/dist/check/check.js +63 -23
- package/dist/cli/args.js +1 -0
- package/dist/cli/commands.js +45 -8
- package/dist/cli/gate.js +2 -1
- package/dist/cli/help.js +12 -5
- package/dist/cli/workflow.js +8 -3
- package/dist/cli.js +45 -6
- package/dist/graph/bundle.js +145 -18
- package/dist/graph/files.js +2 -1
- package/dist/graph/related.js +1 -1
- package/dist/graph/trust.js +114 -0
- package/dist/index/scan.js +2 -2
- package/dist/init/init.js +21 -11
- package/dist/init/next.js +50 -0
- package/dist/journal/journal.js +65 -0
- package/dist/journal/note.js +57 -0
- package/dist/journal/report.js +115 -0
- package/dist/journal/signals.js +26 -0
- package/dist/loop/fails.js +61 -10
- package/dist/loop/openspec.js +2 -1
- package/dist/main.js +16 -1
- package/dist/tasks/evidence.js +3 -3
- package/dist/util/env.js +10 -0
- package/dist/util/redact.js +57 -0
- package/dist/verify/run.js +2 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/specctx-run/SKILL.md +2 -2
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 works.
|
|
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
|
|
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 it checked.
|
|
71
136
|
|
|
72
137
|

|
|
73
138
|
|
|
@@ -75,24 +140,40 @@ 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.
|
|
167
|
+
|
|
168
|
+
## Try it on your own OpenSpec repo
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npx -y spec-ctx init --all # config, ID templates, rules, and the agent skill
|
|
172
|
+
npx -y spec-ctx adopt # gives an ID to every header without one
|
|
173
|
+
npx -y spec-ctx context <task> # the specs, code, and tests that this task needs
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`init` prints the next command, from the state of your specs. To gate the old debt later, run `npx -y spec-ctx baseline write`, commit it with a subject that starts with `baseline:`, and run `npx -y spec-ctx gate --strict`.
|
|
96
177
|
|
|
97
178
|
## How it finds the context
|
|
98
179
|
|
|
@@ -105,29 +186,9 @@ graph LR
|
|
|
105
186
|
R -->|"Covers:"| X["tests/login.test.ts"]
|
|
106
187
|
```
|
|
107
188
|
|
|
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.
|
|
109
|
-
|
|
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.
|
|
189
|
+
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.
|
|
129
190
|
|
|
130
|
-
`specctx
|
|
191
|
+
`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. Each entry prints its card first. A card carries its trust labels: the provenance of the link (`declared`, `tagged`, `structural`, or `verified`) and, for a task or a requirement, the evidence state (`fresh`, `stale since <path>`, `no evidence`, `verified`, `unverified`). A card ends with `| body omitted` when its body does not fit, and `bundle <ID>` gets the body. A `truncated:` line names an entry that has no card. A task that cites no requirement gets a `note:` line. A task also gets one card for each file that its line names, with the reason `named by <task>`. `related` prints each neighbor and the reason for the link:
|
|
131
192
|
|
|
132
193
|
```text
|
|
133
194
|
$ specctx related REQ-UA-004
|
|
@@ -138,212 +199,17 @@ code src/login.ts:1 active src/login.ts:1 implements REQ-UA-004
|
|
|
138
199
|
test tests/login.test.ts:1 active tests/login.test.ts:1 covers REQ-UA-004
|
|
139
200
|
```
|
|
140
201
|
|
|
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`.
|
|
202
|
+
The reference has the budget rules and the lookups by file, by change, and by task.
|
|
187
203
|
|
|
188
204
|
## The life cycle for agents
|
|
189
205
|
|
|
190
206
|
`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
207
|
|
|
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.
|
|
221
|
-
|
|
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. |
|
|
208
|
+
1. It runs `specctx next --json`. The engine reads the files on disk and prints one action, such as `start`, `fix`, or `archive`.
|
|
209
|
+
2. It runs the exact commands of that action.
|
|
210
|
+
3. It runs `specctx handoff write` and repeats.
|
|
334
211
|
|
|
335
|
-
|
|
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
|
-
```
|
|
212
|
+
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
213
|
|
|
348
214
|
## GitHub Action
|
|
349
215
|
|
|
@@ -354,29 +220,7 @@ npx spec-ctx lint ste --baseline 0 --disable synonym-rotation README.md
|
|
|
354
220
|
strict: true
|
|
355
221
|
```
|
|
356
222
|
|
|
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.
|
|
223
|
+
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
224
|
|
|
381
225
|
A Verify command is code from the pull request. The boundary:
|
|
382
226
|
|
|
@@ -387,28 +231,44 @@ A Verify command is code from the pull request. The boundary:
|
|
|
387
231
|
- For fork pull requests, turn on the repository setting that asks for approval before a workflow runs.
|
|
388
232
|
- Check out with `fetch-depth: 0`. The merge base needs full history.
|
|
389
233
|
|
|
234
|
+
Tests and offline machines set `SPECCTX_OPENSPEC` to the command that runs OpenSpec.
|
|
235
|
+
|
|
390
236
|
## Status
|
|
391
237
|
|
|
392
|
-
- Published: `spec-ctx@0.
|
|
393
|
-
- Archived: 001 to 025, 027 to 034, and 036 to
|
|
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.
|
|
238
|
+
- Published: `spec-ctx@0.6.0` on npm, with binaries on the GitHub release. See `CHANGELOG.md` for each release.
|
|
239
|
+
- Archived: 001 to 025, 027 to 034, and 036 to 063. No change is open.
|
|
240
|
+
- Upstream: the ID scheme has a proposal 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
241
|
|
|
397
242
|
## Measured
|
|
398
243
|
|
|
399
|
-
`specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-
|
|
244
|
+
`specctx stats --write` measures this repository after each change. The newest file is `docs/metrics/2026-11-07.md`. Its total row:
|
|
400
245
|
|
|
401
246
|
| change | context_ratio | evidence_ratio | stale_catches | cycle_time_median_s | debt |
|
|
402
247
|
| --- | --- | --- | --- | --- | --- |
|
|
403
|
-
| total | 0.
|
|
248
|
+
| total | 0.0146 (n=168) | 0.95 (286/302) | 82 (of 151 reopens) | 54 (n=168) | 0 (0 baselined + 0 new) |
|
|
404
249
|
|
|
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
|
|
250
|
+
- `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.
|
|
251
|
+
- `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. Higher is better.
|
|
252
|
+
- `stale_catches`: reopened tasks that `check --strict` flagged as stale, out of all reopened tasks.
|
|
253
|
+
- `cycle_time_median_s`: the median seconds from `task start` to `task done`.
|
|
409
254
|
- `debt`: findings that the baseline hides plus new findings. Zero is the goal.
|
|
410
255
|
|
|
411
|
-
The value in brackets is the count of inputs.
|
|
256
|
+
The value in brackets is the count of inputs.
|
|
257
|
+
|
|
258
|
+
## Report a bug
|
|
259
|
+
|
|
260
|
+
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.
|
|
261
|
+
|
|
262
|
+
- 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`.
|
|
263
|
+
- What it never holds: flag values, paths, IDs, change names, spec text, or error messages.
|
|
264
|
+
- 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.
|
|
265
|
+
- How to turn it off: set `SPECCTX_JOURNAL=0`.
|
|
266
|
+
- How to file a bug:
|
|
267
|
+
1. Run `specctx report > report.md`.
|
|
268
|
+
2. Read `report.md`. Remove what you do not want to share.
|
|
269
|
+
3. Run `gh issue create -R sonqh/spec-ctx --body-file report.md`.
|
|
270
|
+
|
|
271
|
+
`specctx report` only prints. It never runs `gh` and never uses the network.
|
|
412
272
|
|
|
413
273
|
## Contribute
|
|
414
274
|
|
package/dist/adopt/plan.js
CHANGED
|
@@ -84,20 +84,40 @@ function parse(root, rel, cap) {
|
|
|
84
84
|
});
|
|
85
85
|
return { rel, cap, lines, blocks, renames };
|
|
86
86
|
}
|
|
87
|
-
/** REQ-ADOPT-
|
|
88
|
-
export function
|
|
87
|
+
/** REQ-ADOPT-012 the family code of a path with two or more segments: three letters of the first, three of the rest. */
|
|
88
|
+
export function familyCode(path) {
|
|
89
|
+
const segs = path.split(/[/-]/).map((x) => x.toUpperCase().replace(/[^A-Z]/g, "")).filter(Boolean);
|
|
90
|
+
if (segs.length < 2)
|
|
91
|
+
return null;
|
|
92
|
+
const code = segs[0].slice(0, 3) + segs.slice(1).join("").slice(0, 3);
|
|
93
|
+
return /^[A-Z]{2,6}$/.test(code) ? code : null;
|
|
94
|
+
}
|
|
95
|
+
/** REQ-ADOPT-012 the first segment of a path, the key of its family */
|
|
96
|
+
export function familySegment(path) {
|
|
97
|
+
return path.split(/[/-]/)[0] ?? path;
|
|
98
|
+
}
|
|
99
|
+
/** REQ-ADOPT-012 the segments shared by three or more paths */
|
|
100
|
+
export function familiesOf(paths) {
|
|
101
|
+
const count = new Map();
|
|
102
|
+
for (const p of paths)
|
|
103
|
+
count.set(familySegment(p), (count.get(familySegment(p)) ?? 0) + 1);
|
|
104
|
+
return new Set([...count].filter(([, n]) => n >= 3).map(([s]) => s));
|
|
105
|
+
}
|
|
106
|
+
/** REQ-ADOPT-004 candidate codes in order: the base code, then two and three letters of the first segment, then a suffix letter. REQ-ADOPT-012 a family member gets its family code first. */
|
|
107
|
+
export function codeCandidates(path, family = false) {
|
|
89
108
|
const segs = path.split(/[/-]/).map((x) => x.toUpperCase().replace(/[^A-Z]/g, "")).filter(Boolean);
|
|
90
109
|
if (!segs.length)
|
|
91
110
|
return [];
|
|
92
111
|
const rest = segs.slice(1).map((x) => x[0]).join("");
|
|
93
112
|
const base = segs.length === 1 ? segs[0].slice(0, 3) : segs.map((x) => x[0]).join("").slice(0, 6);
|
|
94
|
-
const
|
|
113
|
+
const fam = family ? familyCode(path) : null;
|
|
114
|
+
const out = [...(fam ? [fam] : []), base, (segs[0].slice(0, 2) + rest).slice(0, 6), (segs[0].slice(0, 3) + rest).slice(0, 6)];
|
|
95
115
|
for (let c = 66; c <= 90; c++)
|
|
96
116
|
out.push((base.slice(0, 5) + String.fromCharCode(c)));
|
|
97
117
|
return [...new Set(out.filter((x) => /^[A-Z]{2,6}$/.test(x)))];
|
|
98
118
|
}
|
|
99
|
-
export function deriveCode(path, taken = new Set()) {
|
|
100
|
-
const code = codeCandidates(path).find((c) => !taken.has(c));
|
|
119
|
+
export function deriveCode(path, taken = new Set(), family = false) {
|
|
120
|
+
const code = codeCandidates(path, family).find((c) => !taken.has(c));
|
|
101
121
|
if (!code)
|
|
102
122
|
throw new UsageExit(`no free capability code for ${path}. Add one under [ids.paths] in spec-ctx.toml`);
|
|
103
123
|
return code;
|
|
@@ -125,6 +145,8 @@ export function planAdopt(root, cfg) {
|
|
|
125
145
|
// on a collision the shorter path keeps the base code: user-auth keeps UA, upload-api takes UPA
|
|
126
146
|
const capList = [...new Set(files.map((f) => f.cap))].sort((a, b) => a.length - b.length || cmpStr(a, b));
|
|
127
147
|
const taken = new Set([...cfg.ids.caps, ...codeOfPath.values()]);
|
|
148
|
+
// REQ-ADOPT-012 a family is three or more paths with one first segment (063/D1)
|
|
149
|
+
const families = familiesOf(capList);
|
|
128
150
|
const caps = [];
|
|
129
151
|
for (const cap of capList) {
|
|
130
152
|
let code = codeOfPath.get(cap);
|
|
@@ -135,7 +157,7 @@ export function planAdopt(root, cfg) {
|
|
|
135
157
|
code = HAS_REQ.exec(existing.id)[1];
|
|
136
158
|
}
|
|
137
159
|
if (!code) {
|
|
138
|
-
code = deriveCode(cap, taken);
|
|
160
|
+
code = deriveCode(cap, taken, families.has(familySegment(cap)));
|
|
139
161
|
derived = true;
|
|
140
162
|
taken.add(code);
|
|
141
163
|
}
|
|
@@ -275,13 +297,24 @@ export function planAdopt(root, cfg) {
|
|
|
275
297
|
}
|
|
276
298
|
}
|
|
277
299
|
edits.sort((a, b) => cmpStr(a.file, b.file) || a.line - b.line);
|
|
278
|
-
|
|
300
|
+
// a family whose segment has no letters gives no code, so it is not listed
|
|
301
|
+
const familyList = [...families]
|
|
302
|
+
.sort(cmpStr)
|
|
303
|
+
.map((segment) => ({
|
|
304
|
+
segment,
|
|
305
|
+
prefix: segment.toUpperCase().replace(/[^A-Z]/g, "").slice(0, 3),
|
|
306
|
+
count: capList.filter((c) => familySegment(c) === segment).length,
|
|
307
|
+
}))
|
|
308
|
+
.filter((f) => f.prefix.length >= 2);
|
|
309
|
+
return { edits, caps, families: familyList, unmatched };
|
|
279
310
|
}
|
|
280
311
|
export function planLines(p) {
|
|
281
312
|
const out = p.edits.map((e) => `${e.file}:${e.line} ${e.old} -> ${e.new}`);
|
|
282
313
|
const derived = p.caps.filter((c) => c.derived);
|
|
283
314
|
if (derived.length)
|
|
284
315
|
out.push(...derived.map((c) => `cap: ${c.code} ${c.path}`));
|
|
316
|
+
// REQ-ADOPT-012 one line per family, after the cap lines (063/D5)
|
|
317
|
+
out.push(...p.families.map((f) => `family: ${f.segment} prefix ${f.prefix} ${f.count} codes`));
|
|
285
318
|
for (const u of p.unmatched)
|
|
286
319
|
out.push(`unmatched: ${u.file}:${u.line} ${u.header}`);
|
|
287
320
|
return out;
|