spec-ctx 0.4.0 → 0.4.1
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 +88 -16
- package/dist/adopt/write.js +6 -2
- package/dist/assets.generated.js +1 -1
- package/dist/check/hints.js +10 -9
- package/dist/cli/args.js +2 -1
- package/dist/cli/commands.js +66 -20
- package/dist/cli/help.js +17 -5
- package/dist/cli/workflow.js +24 -10
- package/dist/config.js +8 -0
- package/dist/graph/bundle.js +140 -26
- package/dist/graph/files.js +31 -0
- package/dist/graph/input.js +37 -0
- package/dist/graph/links.js +96 -0
- package/dist/graph/path.js +54 -0
- package/dist/graph/query.js +3 -0
- package/dist/graph/related.js +58 -11
- package/dist/handoff/handoff.js +6 -4
- package/dist/handoff/questions.js +9 -1
- package/dist/index/build.js +33 -4
- package/dist/index/clock.js +55 -0
- package/dist/index/scan.js +36 -9
- package/dist/index/structure.js +97 -0
- package/dist/init/init.js +15 -4
- package/dist/loop/fails.js +10 -2
- package/dist/loop/next.js +38 -17
- package/dist/runcmd.js +5 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/specctx-run/SKILL.md +3 -3
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Six commands on a small project. Each clip is one command.
|
|
|
37
37
|
|
|
38
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`.
|
|
39
39
|
|
|
40
|
-

|
|
40
|
+

|
|
41
41
|
|
|
42
42
|
You see: the dry run prints each header and the ID it gets. Then `adopt` writes them.
|
|
43
43
|
|
|
@@ -45,7 +45,7 @@ You see: the dry run prints each header and the ID it gets. Then `adopt` writes
|
|
|
45
45
|
|
|
46
46
|
`status` shows the open change, the task counts, and the next command to run.
|
|
47
47
|
|
|
48
|
-

|
|
48
|
+

|
|
49
49
|
|
|
50
50
|
You see: one change, two tasks done, one open, and the hint.
|
|
51
51
|
|
|
@@ -53,15 +53,15 @@ You see: one change, two tasks done, one open, and the hint.
|
|
|
53
53
|
|
|
54
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.
|
|
55
55
|
|
|
56
|
-

|
|
56
|
+

|
|
57
57
|
|
|
58
58
|
You see: task `add-mfa/1.3`, its requirement, and `grep -q totp src/login.ts`.
|
|
59
59
|
|
|
60
60
|
### 4. Only the spec this task needs
|
|
61
61
|
|
|
62
|
-
`task start` prints the
|
|
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.
|
|
63
63
|
|
|
64
|
-

|
|
64
|
+

|
|
65
65
|
|
|
66
66
|
You see: 711 characters, 3.6 percent of the tree. The agent reads this instead of the tree.
|
|
67
67
|
|
|
@@ -69,15 +69,15 @@ You see: 711 characters, 3.6 percent of the tree. The agent reads this instead o
|
|
|
69
69
|
|
|
70
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.
|
|
71
71
|
|
|
72
|
-

|
|
72
|
+

|
|
73
73
|
|
|
74
74
|
You see: a red refusal, the edit, then a green done line.
|
|
75
75
|
|
|
76
76
|
### 6. Hand off to the next session
|
|
77
77
|
|
|
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.
|
|
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.
|
|
79
79
|
|
|
80
|
-

|
|
80
|
+

|
|
81
81
|
|
|
82
82
|
You see: the head of `.ctx/handoff.md`.
|
|
83
83
|
|
|
@@ -107,6 +107,26 @@ graph LR
|
|
|
107
107
|
|
|
108
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.
|
|
109
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.
|
|
129
|
+
|
|
110
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:
|
|
111
131
|
|
|
112
132
|
```text
|
|
@@ -118,6 +138,53 @@ code src/login.ts:1 active src/login.ts:1 implements REQ-UA-004
|
|
|
118
138
|
test tests/login.test.ts:1 active tests/login.test.ts:1 covers REQ-UA-004
|
|
119
139
|
```
|
|
120
140
|
|
|
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`.
|
|
187
|
+
|
|
121
188
|
## The life cycle for agents
|
|
122
189
|
|
|
123
190
|
`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.
|
|
@@ -138,7 +205,7 @@ specctx gate --strict # check, baseline, linters, openspec validate, le
|
|
|
138
205
|
specctx handoff write # .ctx/handoff.md for the next session
|
|
139
206
|
```
|
|
140
207
|
|
|
141
|
-
The rule: the agent never opens `openspec/specs/` by hand. It uses `task start` or `
|
|
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`.
|
|
142
209
|
|
|
143
210
|
## Install
|
|
144
211
|
|
|
@@ -158,7 +225,7 @@ The examples below use `specctx`. With `npx`, write `npx -y spec-ctx` in its pla
|
|
|
158
225
|
|
|
159
226
|
## Two paths
|
|
160
227
|
|
|
161
|
-
[`docs/getting-started.md`](docs/getting-started.md) walks the fresh path on upstream OpenSpec with real output, for a developer who knows OpenSpec.
|
|
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.
|
|
162
229
|
|
|
163
230
|
### Fresh project
|
|
164
231
|
|
|
@@ -180,6 +247,7 @@ specctx baseline write
|
|
|
180
247
|
specctx gate --strict
|
|
181
248
|
```
|
|
182
249
|
|
|
250
|
+
- `adopt --dry-run` exits 1 when headers need IDs. A script with `set -e` must allow that code.
|
|
183
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`.
|
|
184
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.
|
|
185
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`.
|
|
@@ -198,8 +266,9 @@ Both paths have an end to end test on upstream OpenSpec: `tests/paths/fresh.sh`
|
|
|
198
266
|
| `status` | Active changes with task counts, the current task, and open questions. |
|
|
199
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`. |
|
|
200
268
|
| `query <term...>` | Entries that hold every term. Flags: `--kind`, `--change`, `--limit`, `--all`. |
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
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`. |
|
|
203
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`. |
|
|
204
273
|
| `id new`, `id retire` | Allocate and retire IDs. |
|
|
205
274
|
| `task next`, `start`, `verify`, `done`, `reopen` | The task state machine. |
|
|
@@ -249,6 +318,9 @@ base = "origin/main" # the ref whose merge base holds the baseline that gate co
|
|
|
249
318
|
[loop]
|
|
250
319
|
max_attempts = 3 # Verify failures on one task before task done writes a question
|
|
251
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.
|
|
252
324
|
```
|
|
253
325
|
|
|
254
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`.
|
|
@@ -317,8 +389,8 @@ A Verify command is code from the pull request. The boundary:
|
|
|
317
389
|
|
|
318
390
|
## Status
|
|
319
391
|
|
|
320
|
-
- Published: `spec-ctx@0.4.
|
|
321
|
-
- Archived: 001 to 025, 027 to 034, and 036 to
|
|
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.
|
|
322
394
|
- A version bump on `main` publishes the next release. See `docs/CONTRIBUTING.md`.
|
|
323
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.
|
|
324
396
|
|
|
@@ -328,9 +400,9 @@ A Verify command is code from the pull request. The boundary:
|
|
|
328
400
|
|
|
329
401
|
| change | context_ratio | evidence_ratio | stale_catches | cycle_time_median_s | debt |
|
|
330
402
|
| --- | --- | --- | --- | --- | --- |
|
|
331
|
-
| total | 0.
|
|
403
|
+
| total | 0.0189 (n=135) | 0.94 (236/252) | 65 (of 118 reopens) | 60 (n=135) | 0 (0 baselined + 0 new) |
|
|
332
404
|
|
|
333
|
-
- `context_ratio`: the size of one task bundle divided by the size of all spec text. Lower is better.
|
|
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.
|
|
334
406
|
- `evidence_ratio`: done tasks with a passed Verify command, divided by all done tasks. The rest have a manual note. Higher is better.
|
|
335
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.
|
|
336
408
|
- `cycle_time_median_s`: the median number of seconds from `task start` to `task done`. Lower is faster.
|
package/dist/adopt/write.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { UsageExit } from "../cli/errors.js";
|
|
5
5
|
import { CONFIG_FILE } from "../config.js";
|
|
6
6
|
import { loadConfigOrExit } from "../cli/config.js";
|
|
7
|
+
import { runCommand } from "../runcmd.js";
|
|
7
8
|
import { dumps } from "../util/json.js";
|
|
8
9
|
import { configText, withCaps } from "./config-write.js";
|
|
9
10
|
export const REPORT_FILE = "adopt-report.md";
|
|
@@ -57,7 +58,7 @@ export function reportText(plan) {
|
|
|
57
58
|
}
|
|
58
59
|
return out.join("\n") + "\n";
|
|
59
60
|
}
|
|
60
|
-
export async function applyAdopt(root,
|
|
61
|
+
export async function applyAdopt(root, cfg, plan, io, json) {
|
|
61
62
|
const cfgPath = join(root, CONFIG_FILE);
|
|
62
63
|
const cfgBefore = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : null;
|
|
63
64
|
const cfgAfter = withCaps(configText(root), plan.caps);
|
|
@@ -120,8 +121,11 @@ export async function applyAdopt(root, _cfg, plan, io, json) {
|
|
|
120
121
|
// REQ-CLI-007 only the codes in spec-ctx.toml changed: no header, no report
|
|
121
122
|
if (!report)
|
|
122
123
|
io.out(`wrote the codes to ${CONFIG_FILE}\nnothing to adopt\n`);
|
|
123
|
-
else
|
|
124
|
+
else {
|
|
124
125
|
io.out(`adopted ${plan.edits.length} header(s) in ${files} file(s). Report: ${REPORT_FILE}\n`);
|
|
126
|
+
// REQ-ADOPT-002 adopt writes no baseline. The next step is a separate commit (044/D4, ADR-006 item 6).
|
|
127
|
+
io.out(`Commit the baseline with a subject that starts with baseline:.\nnext: ${runCommand(cfg.cli.run, "baseline write")}\n`);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
return 0;
|
|
127
131
|
}
|
package/dist/assets.generated.js
CHANGED
|
@@ -10,5 +10,5 @@ export const ASSETS = {
|
|
|
10
10
|
"skills/specctx-bootstrap/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtYm9vdHN0cmFwCmRlc2NyaXB0aW9uOiBTdGFydCBhIHNlc3Npb24gaW4gYW4gT3BlblNwZWMgcHJvamVjdCB0aGF0IHVzZXMgc3BlYy1jdHguIFVzZSBhdCB0aGUgc3RhcnQgb2YgZXZlcnkgc2Vzc2lvbiwgYW5kIG9uICJzdGFydCIsICJib290c3RyYXAiLCBvciAid2hhdCBzaG91bGQgSSB3b3JrIG9uIi4gTG9hZHMgb25seSB0aGUgc3BlYyB0ZXh0IHRoYXQgdGhlIG5leHQgdGFzayBuZWVkcy4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
|
|
11
11
|
"skills/specctx-done/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtZG9uZQpkZXNjcmlwdGlvbjogQ2xvc2UgYSB0YXNrIGluIGFuIE9wZW5TcGVjIHByb2plY3QgdGhhdCB1c2VzIHNwZWMtY3R4LiBSdW5zIHRoZSBWZXJpZnkgY29tbWFuZCwgd3JpdGVzIGV2aWRlbmNlLCBzZXRzIHRoZSBjaGVja2JveCwgd3JpdGVzIHRoZSBoYW5kb2ZmLCBhbmQgc2hvd3MgdGhlIG1ldHJpY3MuIFVzZSB3aGVuIHRoZSB3b3JrIG9mIGEgdGFzayBpcyBjb21wbGV0ZSwgb3Igb24gImRvbmUiLCAiY2xvc2UgdGhlIHRhc2siLCBhbmQgImVuZCB0aGUgc2Vzc2lvbiIuCi0tLQoKVGhpcyBza2lsbCBpcyBub3cgYHNwZWNjdHgtcnVuYC4gUnVuIGl0Lgo=",
|
|
12
12
|
"skills/specctx-next/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtbmV4dApkZXNjcmlwdGlvbjogUGljayBhbmQgc3RhcnQgdGhlIG5leHQgdGFzayBpbiB0aGUgbWlkZGxlIG9mIGEgc2Vzc2lvbiBpbiBhbiBPcGVuU3BlYyBwcm9qZWN0IHRoYXQgdXNlcyBzcGVjLWN0eC4gVXNlIGFmdGVyIHlvdSBjbG9zZSBhIHRhc2ssIG9yIG9uICJuZXh0IHRhc2siIGFuZCAid2hhdCBpcyBuZXh0Ii4KLS0tCgpUaGlzIHNraWxsIGlzIG5vdyBgc3BlY2N0eC1ydW5gLiBSdW4gaXQuCg==",
|
|
13
|
-
"skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+
|
|
13
|
+
"skills/specctx-run/SKILL.md": "LS0tCm5hbWU6IHNwZWNjdHgtcnVuCmRlc2NyaXB0aW9uOiBSdW4gdGhlIHNwZWMtY3R4IGxvb3AgaW4gYW4gT3BlblNwZWMgcHJvamVjdC4gVXNlIG9uICJydW4iLCAiY29udGludWUiLCAibmV4dCB0YXNrIiwgImJ1aWxkIHRoaXMiLCAiY2Fycnkgb24iLCBhbmQgb24gYSBmaXJzdCBtZXNzYWdlIHRoYXQgZGVzY3JpYmVzIGEgY2hhbmdlIHRvIGJ1aWxkLiBUaGUgZW5naW5lIHBpY2tzIHRoZSBuZXh0IGFjdGlvbi4gVGhlIHNraWxsIGFjdHMgb24gaXQgdW50aWwgdGhlIGNoYW5nZSByZWFjaGVzIHRoZSBhcmNoaXZlIG9yIGEgaHVtYW4gbXVzdCBkZWNpZGUuCi0tLQoKIyBzcGVjY3R4LXJ1bgoKUnVuIGBzcGVjY3R4YCBhcyBgbnB4IHNwZWMtY3R4YCB3aGVuIGl0IGlzIG5vdCBpbnN0YWxsZWQuIGBvcGVuc3BlY2AgaXMgdGhlIGNvbW1hbmQgaW4gYFtnYXRlXSBvcGVuc3BlY2Agb2YgYHNwZWMtY3R4LnRvbWxgLiBXaGVuIHRoYXQga2V5IGlzIGFic2VudCwgYG9wZW5zcGVjYCBpcyBgbnB4IC15IEBmaXNzaW9uLWFpL29wZW5zcGVjQGxhdGVzdGAuCgojIyBUaGUgbG9vcAoKMS4gUnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYC4gUmVhZCBgYWN0aW9uYCwgYGNoYW5nZWAgKGA8Yz5gKSwgYHRhc2tgIChgPHQ+YCksIGBjb21tYW5kYCwgYHJlYXNvbmAsIGFuZCBgaHVtYW5gLgoyLiBBY3Qgb24gYGFjdGlvbmAgYXMgdGhlIHRhYmxlIGJlbG93IHNheXMuCjMuIFJ1biBgc3BlY2N0eCBoYW5kb2ZmIHdyaXRlYC4KNC4gUmVwZWF0IGZyb20gc3RlcCAxIHVudGlsIGBhY3Rpb25gIGlzIGBzdG9wYCBvciBgaHVtYW5gIGlzIHRydWUuCgojIyBBY3Rpb25zCgotIGBwbGFuYDogdGFrZSB0aGUgY2hhbmdlIG5hbWUgZnJvbSB0aGUgcmVxdWVzdCBvZiB0aGUgdXNlciBpbiBrZWJhYiBjYXNlLiBUaGF0IG5hbWUgaXMgYDxjPmAuIEZvbGxvdyB0aGUgdXBzdHJlYW0gYG9wZW5zcGVjLXByb3Bvc2VgIHNraWxsIGZvciBgPGM+YC4gUmVhZCBleGlzdGluZyByZXF1aXJlbWVudHMgd2l0aCBgc3BlY2N0eCBidW5kbGUgPGlkPmAgb3IgYHNwZWNjdHggcXVlcnlgLCBuZXZlciBgb3BlbnNwZWMgc2hvd2AuIFRha2UgZXZlcnkgSUQgZnJvbSBgc3BlY2N0eCBpZCBuZXcgUkVRIDxDQVA+YCBhbmQgYHNwZWNjdHggaWQgbmV3IFNDTiA8Q0FQPiAtLWZvciA8UkVRPmAuIFdoZW4gdGhlIGNoYW5nZSBuZWVkcyBzZXZlcmFsIElEcyBvZiBvbmUga2luZCwgcnVuIGBzcGVjY3R4IGlkIG5ldyAtLWNvdW50IDxuPmAgb25jZS4gR2l2ZSBldmVyeSB0YXNrIGEgYFtSRVEtLi4uXWAgY2l0YXRpb24gYW5kIGEgYFZlcmlmeTpgIGNvZGUgc3Bhbi4gVGhlbiBydW4gYHNwZWNjdHggY2hlY2sgLS1zdHJpY3RgIGFuZCBgc3BlY2N0eCBnYXRlIC0tc3RyaWN0YC4gVGhlIGdhdGUgcnVucyB0aGUgT3BlblNwZWMgdmFsaWRhdGUgYW5kIHRoZSBzcGVjLWxlbmd0aCBjaGVjay4gRml4IGV2ZXJ5IGZpbmRpbmcuIFRoZW4gcnVuIGBzcGVjY3R4IG5leHQgLS1qc29uYCBhbmQgcnVuIHRoZSBjb21tYW5kIGl0IHByaW50cy4gVGhpcyBpcyB0aGUgcGxhbiBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB3aXRoIGBodW1hbjogdHJ1ZWAgdW50aWwgdGhlIGh1bWFuIGFwcHJvdmVzIHRoZSBxdWVzdGlvbi4gWW91IG5ldmVyIHJlc29sdmUgYSBwbGFuIHF1ZXN0aW9uIHlvdXJzZWxmLgotIGBjaXRlYDogcnVuIGBzcGVjY3R4IHF1ZXJ5IC0tY2hhbmdlIDxjPiAtLWtpbmQgcmVxYC4gQWRkIHRoZSBgW1JFUS0uLi5dYCBjaXRhdGlvbiBhbmQgYSBgVmVyaWZ5OmAgY29kZSBzcGFuIHRvIHRoZSB0YXNrIGxpbmUgbmFtZWQgaW4gYHRhc2tgLiBSdW4gYHNwZWNjdHggY2hlY2tgLiBDb250aW51ZS4KLSBgc3RhcnRgOiBydW4gYHNwZWNjdHggdGFzayBzdGFydCA8dD5gLiBSZWFkIHRoZSBidW5kbGUgaXQgcHJpbnRzLiBDb250aW51ZS4KLSBgd29ya2A6IGltcGxlbWVudCB0aGUgdGFzay4gVGFnIGNvZGUgd2l0aCBgLy8gUkVRLS4uLmAuIFRhZyB0ZXN0cyB3aXRoIGAvLyBDb3ZlcnM6IFJFUS0uLi5gLiBUaGVuIHJ1biBgc3BlY2N0eCB0YXNrIGRvbmUgPHQ+YC4KLSBgZml4YDogcmVhZCBgcmVhc29uYC4gSXQgaG9sZHMgdGhlIHRhaWwgb2YgdGhlIGZhaWxlZCBWZXJpZnkgb3V0cHV0LiBGaXggdGhlIGNhdXNlLiBSdW4gYHNwZWNjdHggdGFzayBkb25lIDx0PmAgYWdhaW4uCi0gYHJlcGFpcmA6IHJ1biB0aGUgY29tbWFuZCBpbiB0aGUgYGNvbW1hbmRgIGZpZWxkLiBJdCBydW5zIFZlcmlmeSBhZ2FpbiBmb3IgYSB0aWNrZWQgdGFzayB3aG9zZSBldmlkZW5jZSBpcyBhYnNlbnQgb3Igc3RhbGUuCi0gYGdhdGVgOiBydW4gYHNwZWNjdHggZ2F0ZSAtLXN0cmljdGAuIEZpeCBldmVyeSBmaW5kaW5nLiBGb3IgYSBgc3RhbGUtZXZpZGVuY2VgIGZpbmRpbmcsIGdvIGJhY2sgdG8gc3RlcCAxLiBgbmV4dGAgcmV0dXJucyBgcmVwYWlyYCBhbmQgaXRzIGNvbW1hbmQgcmVwYWlycyBldmVyeSBzdGFsZSB0YXNrLiBSdW4gdGhlIGdhdGUgYWdhaW4gdW50aWwgaXQgZXhpdHMgMC4KLSBgYXJjaGl2ZWA6IHdoZW4gYGh1bWFuYCBpcyB0cnVlLCBydW4gYHNwZWNjdHggaGFuZG9mZiBhc2sgLS1jaGFuZ2UgPGM+ICJhcmNoaXZlIDxjPj8iYCBhbmQgc3RvcC4gVGhpcyBpcyB0aGUgYXJjaGl2ZSBzdG9wLiBgbmV4dGAgcmV0dXJucyBgYXNrYCB1bnRpbCB0aGUgaHVtYW4gYXBwcm92ZXMgdGhlIHF1ZXN0aW9uLiBZb3UgbmV2ZXIgcmVzb2x2ZSBhbiBhcmNoaXZlIHF1ZXN0aW9uIHlvdXJzZWxmLiBBZnRlciBhIHJlamVjdGlvbiwgYG5leHRgIHJldHVybnMgYGFyY2hpdmVgIHdpdGggYGh1bWFuOiB0cnVlYCBhZ2Fpbi4gV2hlbiBgaHVtYW5gIGlzIGZhbHNlLCB0aGUgdXNlciBhcHByb3ZlZCBvciBgW2xvb3BdIGFyY2hpdmVgIGlzIGBhdXRvYC4gVGhlbiBydW4gZWFjaCBzdGVwIHVuZGVyIGBvcGVyYXRpb25zLmFyY2hpdmUuZ3VpZGFuY2VgIGluIGBvcGVuc3BlYy9jb25maWcueWFtbGAuIFRoZW4gcnVuIHRoZSBjb21tYW5kIGluIHRoZSBgY29tbWFuZGAgZmllbGQuIFRoZW4gcnVuIGBvcGVuc3BlYyB2YWxpZGF0ZSAtLWFsbCAtLXN0cmljdGAsIGBzcGVjY3R4IGNoZWNrIC0tc3RyaWN0YCwgYW5kIGBzcGVjY3R4IGhhbmRvZmYgd3JpdGVgLgotIGBhc2tgOiBwcmludCBgcmVhc29uYC4gSXQgaXMgdGhlIG9wZW4gcXVlc3Rpb24uIFN0b3AuIFlvdSBuZXZlciByZXNvbHZlIGEgcGxhbiBvciBhcmNoaXZlIHF1ZXN0aW9uIHlvdXJzZWxmLiBUaGUgaHVtYW4gZG9lcywgd2l0aCBgc3BlY2N0eCBoYW5kb2ZmIHJlc29sdmUgPG4+IC0tYXBwcm92ZSAiPGFuc3dlcj4iYCBvciBgLS1yZWplY3QgIjxhbnN3ZXI+ImAuIEEgcmVqZWN0aW9uIGtlZXBzIHRoZSBzdG9wLiBBbnkgb3RoZXIgcXVlc3Rpb24gdGFrZXMgYHNwZWNjdHggaGFuZG9mZiByZXNvbHZlIDxuPiAiPGFuc3dlcj4iYC4KLSBgc3RvcGA6IHByaW50IGByZWFzb25gLiBTdG9wLgoKIyMgUnVsZXMKCi0gVGhpcyBza2lsbCBvdmVycmlkZXMgdHdvIHN0ZXBzIG9mIHRoZSB1cHN0cmVhbSBgb3BlbnNwZWMtcHJvcG9zZWAgc2tpbGwuIEl0IHJlYWRzIHNwZWNzIHdpdGggYHNwZWNjdHggYnVuZGxlYCBvciBgc3BlY2N0eCBxdWVyeWAsIG5vdCBgb3BlbnNwZWMgc2hvd2AuIEl0IG5ldmVyIHJ1bnMgdGhlIGNsb3NpbmcgYC9vcHN4OmFwcGx5YC4KLSBOZXZlciB0aWNrIGEgY2hlY2tib3ggYnkgaGFuZC4gYHNwZWNjdHggdGFzayBkb25lYCBpcyB0aGUgb25seSB3YXkgYSB0YXNrIGNsb3Nlcy4KLSBOZXZlciB1c2UgYC0tbWFudWFsYCBvbiBhIHRhc2sgd2l0aCBhIFZlcmlmeSBjb21tYW5kLgotIE5ldmVyIGNhbGwgYC9vcHN4OmFwcGx5YC4gVGhpcyBza2lsbCByZXBsYWNlcyBpdC4KLSBUbyB3cml0ZSBhIE1PRElGSUVEIGJsb2NrLCBydW4gYHNwZWNjdHggYnVuZGxlIDxSRVE+IC0tYmxvY2tgLiBJdCBwcmludHMgdGhlIGJsb2NrIGFzIGl0IHNpdHMgaW4gdGhlIHNwZWMgZmlsZS4gQSBNT0RJRklFRCBibG9jayBrZWVwcyBldmVyeSBzY2VuYXJpbyB0aXRsZSB1bmNoYW5nZWQuCi0gYGdhdGVgIGNvdW50cyB0aGUgUmVmcyBsaW5lIGluIHRoZSA1MDAgY2hhcmFjdGVycyBvZiBhIHJlcXVpcmVtZW50LgotIFRoZSBDSEFOR0VMT0cgZW50cnkgZ29lcyB1bmRlciBgIyMgVW5yZWxlYXNlZGAsIG5ldmVyIHVuZGVyIGEgcmVsZWFzZWQgdmVyc2lvbi4KLSBBZnRlciBhbiBlZGl0IHVuZGVyIGBza2lsbHMvYCwgcnVuIGBidW4gcnVuIGJ1aWxkICYmIG5vZGUgZGlzdC9jbGkuanMgaW5pdCAtLXNraWxscyAtLWZvcmNlYCB0byByZWZyZXNoIHRoZSBjb3BpZXMuCi0gTmV2ZXIgcmVhZCBgb3BlbnNwZWMvc3BlY3MvYCBvciBgb3BlbnNwZWMvY2hhbmdlcy8qL3NwZWNzL2AgZGlyZWN0bHkuIFVzZSB0aGUgYnVuZGxlIGZyb20gYHRhc2sgc3RhcnRgIG9yIGBzcGVjY3R4IGJ1bmRsZSA8aWQ+YC4KLSBTdG9wIG9uIGFueSBgYWN0aW9uYCBub3QgaW4gdGhlIGxpc3QgYWJvdmUuIFByaW50IHRoZSBvYmplY3QgYW5kIGFzayB0aGUgdXNlci4KLSBLZWVwIG5vIHN0YXRlIGluIHlvdXIgY29udGV4dC4gQSBuZXcgc2Vzc2lvbiBzdGFydHMgdGhlIHNhbWUgbG9vcCBhZ2FpbiB3aXRoIHN0ZXAgMS4K",
|
|
14
14
|
};
|
package/dist/check/hints.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// REQ-CHK-004 the rules of check and one fix hint per rule. The check summary prints the hint of each rule it found.
|
|
2
2
|
/** `hard` rules set exit 1. `advisory` rules print a warning and keep exit 0. */
|
|
3
3
|
export const RULES = {
|
|
4
|
-
"missing-id": { level: "hard", hint: "Add an ID to the header. Run
|
|
4
|
+
"missing-id": { level: "hard", hint: "Add an ID to the header. Run {run} adopt --dry-run to see the plan." },
|
|
5
5
|
"scenario-id-mismatch": { level: "hard", hint: "Give the scenario the number of its requirement." },
|
|
6
6
|
"bad-ref": { level: "hard", hint: "Write only IDs on the Refs line, separated by commas." },
|
|
7
7
|
"task-without-req": { level: "hard", hint: "Cite a requirement in brackets on the task line, for example [REQ-ABC-001]." },
|
|
8
8
|
"task-without-verify": { level: "hard", hint: "Add Verify: and a command in backticks to the task line." },
|
|
9
|
-
"duplicate-id": { level: "hard", hint: "Give one of the two entries a new ID from
|
|
9
|
+
"duplicate-id": { level: "hard", hint: "Give one of the two entries a new ID from {run} id new." },
|
|
10
10
|
"duplicate-change-key": { level: "hard", hint: "Rename one change folder, so that each key names one change." },
|
|
11
|
-
"reused-id": { level: "hard", hint: "The ID is retired. Get a new ID from
|
|
11
|
+
"reused-id": { level: "hard", hint: "The ID is retired. Get a new ID from {run} id new." },
|
|
12
12
|
// REQ-CHK-007 the hint names the string literal cause and the fix (033/D3)
|
|
13
13
|
"dangling-ref": {
|
|
14
14
|
level: "hard",
|
|
@@ -20,13 +20,13 @@ export const RULES = {
|
|
|
20
20
|
"unknown-cap": { level: "hard", hint: "Add the code to caps under [ids] in spec-ctx.toml." },
|
|
21
21
|
uncovered: { level: "hard", hint: "Add a test with a Covers tag for the requirement." },
|
|
22
22
|
"scenario-untested": { level: "hard", hint: "Name the scenario ID in a test with a // Covers: <SCN> tag." },
|
|
23
|
-
"done-without-evidence": { level: "hard", hint: "Run
|
|
23
|
+
"done-without-evidence": { level: "hard", hint: "Run {run} task done. It runs the Verify command and writes the evidence." },
|
|
24
24
|
// REQ-CHK-006 the hint names the why argument of task reopen (031/D8)
|
|
25
|
-
"stale-evidence": { level: "hard", hint: "Run
|
|
26
|
-
"evidence-without-done": { level: "hard", hint: "Run
|
|
25
|
+
"stale-evidence": { level: "hard", hint: "Run {run} task reopen <task> <why>, then {run} task done <task>." },
|
|
26
|
+
"evidence-without-done": { level: "hard", hint: "Run {run} task done, or remove the evidence file." },
|
|
27
27
|
// REQ-CHK-005 advisory in plain check. check --strict reports it as a hard finding (028/D5).
|
|
28
28
|
"weak-verify": { level: "advisory", hint: "Name a test file or a command whose exit code proves the task." },
|
|
29
|
-
"manual-evidence": { level: "advisory", hint: "Give the task a Verify command and run
|
|
29
|
+
"manual-evidence": { level: "advisory", hint: "Give the task a Verify command and run {run} task done again." },
|
|
30
30
|
// REQ-CHK-001 the hint is the block to paste. The finding line carries the ID and both titles (021/D3).
|
|
31
31
|
// REQ-ADOPT-010 FROM copies the main header and TO copies the delta header, each in its own form (027/D4).
|
|
32
32
|
"retitle-needs-renamed": {
|
|
@@ -34,6 +34,7 @@ export const RULES = {
|
|
|
34
34
|
hint: "Add to the delta before the MODIFIED block. Write each ID as its header writes it, bare or in brackets:\n## RENAMED Requirements\n- FROM: `### Requirement: <ID> <old title>`\n- TO: `### Requirement: <ID> <new title>`",
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** REQ-CLI-010 `{run}` in a hint stands for the run prefix of `[cli] run` (043/D1). */
|
|
38
|
+
export function hintOf(rule, run = "specctx") {
|
|
39
|
+
return (RULES[rule]?.hint ?? "Read the check rules in the check spec.").replaceAll("{run}", run);
|
|
39
40
|
}
|
package/dist/cli/args.js
CHANGED
|
@@ -8,6 +8,7 @@ export const COMMAND_FLAGS = {
|
|
|
8
8
|
query: { kind: "string", change: "string", all: "bool", limit: "int" },
|
|
9
9
|
related: { depth: "int", all: "bool" },
|
|
10
10
|
bundle: { budget: "int", depth: "int", all: "bool", block: "bool" },
|
|
11
|
+
context: { budget: "int", depth: "int" },
|
|
11
12
|
check: { strict: "bool" },
|
|
12
13
|
stats: { change: "string", write: "bool" },
|
|
13
14
|
gate: { strict: "bool", markdown: "string", rerun: "bool", base: "string" },
|
|
@@ -20,7 +21,7 @@ export const COMMAND_FLAGS = {
|
|
|
20
21
|
"task reopen": {},
|
|
21
22
|
"handoff write": {},
|
|
22
23
|
"handoff ask": { change: "string", plan: "bool" },
|
|
23
|
-
"handoff resolve": {},
|
|
24
|
+
"handoff resolve": { approve: "bool", reject: "bool" },
|
|
24
25
|
adopt: { "dry-run": "bool" },
|
|
25
26
|
"baseline write": {},
|
|
26
27
|
"baseline prune": {},
|
package/dist/cli/commands.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { UsageExit } from "./errors.js";
|
|
4
4
|
import { findRoot, requireOpenSpec } from "./root.js";
|
|
5
|
+
import { defaults } from "../config.js";
|
|
5
6
|
import { hintOf } from "../check/hints.js";
|
|
6
7
|
import { paint } from "./color.js";
|
|
8
|
+
import { runCommand } from "../runcmd.js";
|
|
7
9
|
/** check with warnings and the baseline filter. Strict mode adds the strict rules and the warnings. */
|
|
8
10
|
async function runCheck(root, cfg, data, strict) {
|
|
9
11
|
const { check } = await import("../check/check.js");
|
|
@@ -26,7 +28,7 @@ async function runCheck(root, cfg, data, strict) {
|
|
|
26
28
|
* REQ-CHK-004 the findings, then a blank line, the summary line, and one line per rule with its hint.
|
|
27
29
|
* A warning counts as advisory. The `baselined` line stays last (REQ-CHK-002).
|
|
28
30
|
*/
|
|
29
|
-
export function checkText(r, tty) {
|
|
31
|
+
export function checkText(r, tty, run = "specctx") {
|
|
30
32
|
// REQ-CLI-008 every finding of check is hard, so every finding line is red. stale-evidence is one of them.
|
|
31
33
|
// REQ-CHK-001 a finding with a detail, such as a retitle, carries it after the ID (021/D2)
|
|
32
34
|
const body = r.findings.map((x) => paint("fail", `${x.file}:${x.line} ${x.rule} ${x.id}${x.detail ? ` ${x.detail}` : ""}\n`, tty)).join("")
|
|
@@ -62,7 +64,7 @@ export function checkText(r, tty) {
|
|
|
62
64
|
const rules = [...count.keys()].sort();
|
|
63
65
|
return body
|
|
64
66
|
+ "\n" + paint("dim", `${n} finding${n === 1 ? "" : "s"} in ${files.size} file${files.size === 1 ? "" : "s"} (${r.findings.length} hard, ${r.warnings.length} advisory)\n`, tty)
|
|
65
|
-
+ rules.map((rule) => `${rule} ${count.get(rule)} ${own.get(rule)?.join("\n") ?? hintOf(rule)}\n`).join("")
|
|
67
|
+
+ rules.map((rule) => `${rule} ${count.get(rule)} ${own.get(rule)?.join("\n") ?? hintOf(rule, run)}\n`).join("")
|
|
66
68
|
+ base;
|
|
67
69
|
}
|
|
68
70
|
export async function run(p, io) {
|
|
@@ -103,7 +105,7 @@ export async function run(p, io) {
|
|
|
103
105
|
io.out(plan.unmatched.length ? planLines(plan).join("\n") + "\nnothing to adopt\n" : "nothing to adopt\n");
|
|
104
106
|
// REQ-CLI-007 the dry run ends with the next step. REQ-ADOPT-001 the exit code stays 1 and the last line says why (017/D5).
|
|
105
107
|
else
|
|
106
|
-
io.out(planLines(plan).join("\n") + `\nexit 1: ${plan.edits.length} headers need an ID. Next:
|
|
108
|
+
io.out(planLines(plan).join("\n") + `\nexit 1: ${plan.edits.length} headers need an ID. Next: ${runCommand(cfg.cli.run, "adopt")}\n`);
|
|
107
109
|
return plan.edits.length ? 1 : 0;
|
|
108
110
|
}
|
|
109
111
|
if (cmd === "baseline write" || cmd === "baseline prune") {
|
|
@@ -163,7 +165,7 @@ export async function run(p, io) {
|
|
|
163
165
|
void check;
|
|
164
166
|
const data = buildIndex(root, cfg);
|
|
165
167
|
const r = await runCheck(root, cfg, data, strict);
|
|
166
|
-
return { code: r.findings.length ? 1 : 0, out: checkText(r, io.tty) };
|
|
168
|
+
return { code: r.findings.length ? 1 : 0, out: checkText(r, io.tty, cfg.cli.run) };
|
|
167
169
|
},
|
|
168
170
|
}, io);
|
|
169
171
|
}
|
|
@@ -176,12 +178,17 @@ export async function run(p, io) {
|
|
|
176
178
|
if (cmd === "query" && !f.change)
|
|
177
179
|
need(1, "at least one term");
|
|
178
180
|
if (cmd === "related") {
|
|
179
|
-
need(1, "one ID");
|
|
181
|
+
need(1, "one ID, path, or change");
|
|
180
182
|
if (p.positional.length > 1)
|
|
181
|
-
throw new UsageExit("related takes one ID");
|
|
183
|
+
throw new UsageExit("related takes one ID, path, or change");
|
|
182
184
|
}
|
|
183
185
|
if (cmd === "bundle")
|
|
184
|
-
need(1, "at least one ID");
|
|
186
|
+
need(1, "at least one ID, path, or change");
|
|
187
|
+
if (cmd === "context") {
|
|
188
|
+
need(1, "one task, ID, path, or change");
|
|
189
|
+
if (p.positional.length > 1)
|
|
190
|
+
throw new UsageExit("context takes one task, ID, path, or change");
|
|
191
|
+
}
|
|
185
192
|
if ((cmd === "status" || cmd === "check" || cmd === "next") && p.positional.length)
|
|
186
193
|
throw new UsageExit(`${cmd} takes no arguments`);
|
|
187
194
|
const { data, rebuilt } = loadIndex(root, cfg);
|
|
@@ -231,15 +238,35 @@ export async function run(p, io) {
|
|
|
231
238
|
}
|
|
232
239
|
case "related": {
|
|
233
240
|
const { canon } = await import("../index/canonical.js");
|
|
234
|
-
const { related } = await import("../graph/related.js");
|
|
235
|
-
const id = p.positional[0];
|
|
241
|
+
const { related, mainVersion } = await import("../graph/related.js");
|
|
236
242
|
const ids = canon(entries);
|
|
243
|
+
let id = p.positional[0];
|
|
244
|
+
let isPath = false;
|
|
237
245
|
if (!ids.has(id) && !(all && entries.some((e) => e.id === id))) {
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
// REQ-QRY-011 a change key or folder name is a change. REQ-QRY-005 any other input is a path in the repository.
|
|
247
|
+
const { resolveFileInput, changeInput } = await import("../graph/path.js");
|
|
248
|
+
const ch = changeInput(entries, id);
|
|
249
|
+
if (ch)
|
|
250
|
+
id = ch;
|
|
251
|
+
else {
|
|
252
|
+
const r = resolveFileInput(root, io.cwd, id);
|
|
253
|
+
if ("error" in r) {
|
|
254
|
+
io.err(`${r.error}\n`);
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
id = r.rel;
|
|
258
|
+
isPath = true;
|
|
259
|
+
}
|
|
240
260
|
}
|
|
241
261
|
const rows = [];
|
|
242
262
|
for (const [i, reason] of related(entries, id, f.depth ?? 1, all)) {
|
|
263
|
+
// REQ-QRY-010 the pair of a main version stands for the non-canonical copy in the main spec
|
|
264
|
+
if (!all && reason === `main version of ${i}`) {
|
|
265
|
+
const m = mainVersion(entries, i);
|
|
266
|
+
if (m)
|
|
267
|
+
rows.push([m, reason]);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
243
270
|
const copies = all ? entries.filter((e) => e.id === i) : ids.has(i) ? [ids.get(i)] : [];
|
|
244
271
|
// canonical copy first, the rest in index order
|
|
245
272
|
copies.sort((a, b) => Number(!a.canonical) - Number(!b.canonical));
|
|
@@ -248,22 +275,27 @@ export async function run(p, io) {
|
|
|
248
275
|
}
|
|
249
276
|
if (json)
|
|
250
277
|
print(rows.map(([e, reason]) => ({ ...e, reason })));
|
|
278
|
+
else if (isPath && rows.length === 0)
|
|
279
|
+
io.out(`no links: ${id}\n`);
|
|
251
280
|
else
|
|
252
281
|
io.out(rows.map(([e, r]) => `${e.kind} ${e.id} ${e.status} ${e.file}:${e.line} ${r}\n`).join(""));
|
|
253
282
|
return 0;
|
|
254
283
|
}
|
|
284
|
+
case "context":
|
|
255
285
|
case "bundle": {
|
|
256
286
|
const { canon } = await import("../index/canonical.js");
|
|
257
287
|
const { bundle } = await import("../graph/bundle.js");
|
|
288
|
+
const { resolveInputs } = await import("../graph/input.js");
|
|
258
289
|
const ids = canon(entries);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
290
|
+
// REQ-QRY-005 an input that is no known ID is a path in the repository
|
|
291
|
+
const { wanted, paths, errors, taskId } = resolveInputs(root, io.cwd, entries, p.positional);
|
|
292
|
+
if (errors.length) {
|
|
293
|
+
io.err(errors.map((l) => l + "\n").join(""));
|
|
262
294
|
return 2;
|
|
263
295
|
}
|
|
264
296
|
// REQ-QRY-004 --block prints the requirement blocks verbatim and nothing else (034/D2)
|
|
265
297
|
if (f.block) {
|
|
266
|
-
const notReq = p.positional.filter((i) => ids.get(i)
|
|
298
|
+
const notReq = p.positional.filter((i) => ids.get(i)?.kind !== "req");
|
|
267
299
|
if (notReq.length) {
|
|
268
300
|
io.err(`not a requirement: ${notReq.join(", ")}\n`);
|
|
269
301
|
return 2;
|
|
@@ -276,9 +308,13 @@ export async function run(p, io) {
|
|
|
276
308
|
io.out(text);
|
|
277
309
|
return 0;
|
|
278
310
|
}
|
|
279
|
-
|
|
311
|
+
// 047/D5 a task input passes the entry that findTask picks, so an active task wins over an archived one
|
|
312
|
+
const task = taskId === undefined ? undefined : (await import("../tasks/tasks.js")).findTask(root, entries, taskId).entry;
|
|
313
|
+
const b = bundle(root, entries, wanted, f.budget ?? cfg.bundle.budget, f.depth ?? cfg.bundle.depth, { all, task });
|
|
280
314
|
if (json)
|
|
281
315
|
print(b);
|
|
316
|
+
else if (b.text === "" && wanted.every((w) => paths.has(w)))
|
|
317
|
+
io.out(wanted.map((w) => `no links: ${w}\n`).join(""));
|
|
282
318
|
else
|
|
283
319
|
io.out(b.text);
|
|
284
320
|
return 0;
|
|
@@ -310,7 +346,7 @@ export async function run(p, io) {
|
|
|
310
346
|
if (json)
|
|
311
347
|
print({ baselined: r.baselined, count: findings.length, findings, warnings: r.warnings });
|
|
312
348
|
else
|
|
313
|
-
io.out(checkText(r, io.tty));
|
|
349
|
+
io.out(checkText(r, io.tty, cfg.cli.run));
|
|
314
350
|
return r.findings.length ? 1 : 0;
|
|
315
351
|
}
|
|
316
352
|
}
|
|
@@ -323,18 +359,28 @@ async function init(p, io) {
|
|
|
323
359
|
const root = resolve(io.cwd, p.flags.root ?? ".");
|
|
324
360
|
const all = !!p.flags.all;
|
|
325
361
|
const r = runInit(root, { schema: all || !!p.flags.schema, skills: all || !!p.flags.skills, force: !!p.flags.force });
|
|
362
|
+
// REQ-CLI-010 init prints its next step with the run prefix of the config it wrote or kept (043/D1)
|
|
363
|
+
const { loadConfigOrExit } = await import("./config.js");
|
|
364
|
+
const run = (() => {
|
|
365
|
+
try {
|
|
366
|
+
return loadConfigOrExit(root).cli.run;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return defaults().cli.run;
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
326
372
|
if (p.flags.json)
|
|
327
373
|
io.out(JSON.stringify(r, null, 2) + "\n");
|
|
328
374
|
else if (r.nothing)
|
|
329
|
-
io.out("nothing to do\n" + r.notes.map((n) => `${n}\n`).join("") + paint("dim",
|
|
375
|
+
io.out("nothing to do\n" + r.notes.map((n) => `${n}\n`).join("") + paint("dim", `next: ${runCommand(run, "status")}\n`, io.tty));
|
|
330
376
|
else {
|
|
331
377
|
io.out(r.written.map((w) => `wrote ${w}\n`).join(""));
|
|
332
378
|
io.out(r.merged.map((m) => `merged ${m}\n`).join(""));
|
|
333
379
|
io.out(r.kept.map((k) => `kept ${k}\n`).join(""));
|
|
334
380
|
io.out(r.notes.map((n) => `${n}\n`).join(""));
|
|
335
381
|
// REQ-CLI-007 init ends with the next step. REQ-DIST-002 a kept changed file points to --force (014/D2).
|
|
336
|
-
const again = ["
|
|
337
|
-
io.out(paint("dim", `next: ${r.changed.length ? again : "
|
|
382
|
+
const again = [runCommand(run, "init"), all ? "--all" : [p.flags.schema ? "--schema" : "", p.flags.skills ? "--skills" : ""].filter(Boolean).join(" "), "--force"].filter(Boolean).join(" ");
|
|
383
|
+
io.out(paint("dim", `next: ${r.changed.length ? again : runCommand(run, "status")}\n`, io.tty));
|
|
338
384
|
}
|
|
339
385
|
return 0;
|
|
340
386
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -38,19 +38,27 @@ export const TABLE = [
|
|
|
38
38
|
exit: PLAIN, example: "specctx query lockfile --kind req",
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
|
-
name: "
|
|
41
|
+
name: "context", group: "Read", args: "<task|ID|path|change>", summary: "Print what an agent needs before it edits a task, an ID, a file, or a change.",
|
|
42
|
+
flags: [
|
|
43
|
+
["--budget <n>", "Stop at n characters. The default is 8000."],
|
|
44
|
+
["--depth <n>", "Follow links n steps. The default is 1."],
|
|
45
|
+
],
|
|
46
|
+
exit: [OK, ["2", "A usage error, an unknown ID, or a bad path. It takes one input. Use `bundle` for several inputs or for `--block`."], INTERNAL], example: "specctx context 045/2.1",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "related", group: "Read", args: "<ID|path|change>", summary: "Show the entries that link to an ID, a file, or a change.",
|
|
42
50
|
flags: [["--depth <n>", "Follow links n steps. The default is 1."], ["--all", "Show all copies of an ID."]],
|
|
43
|
-
exit: [OK, ["2", "A usage error
|
|
51
|
+
exit: [OK, ["2", "A usage error, an unknown ID, or a path that is not a file in the repository."], INTERNAL], example: "specctx related src/graph/bundle.ts",
|
|
44
52
|
},
|
|
45
53
|
{
|
|
46
|
-
name: "bundle", group: "Read", args: "<ID...>", summary: "Print the spec text for the IDs and their links.",
|
|
54
|
+
name: "bundle", group: "Read", args: "<ID|path|change...>", summary: "Print the spec text for the IDs, files, or changes and their links.",
|
|
47
55
|
flags: [
|
|
48
56
|
["--budget <n>", "Stop at n characters. The default is 8000."],
|
|
49
57
|
["--depth <n>", "Follow links n steps. The default is 1."],
|
|
50
58
|
["--all", "Show all copies of an ID."],
|
|
51
59
|
["--block", "Print only the requirement block, as it sits in the spec file. Each ID must be a requirement."],
|
|
52
60
|
],
|
|
53
|
-
exit: [OK, ["2", "A usage error, an unknown ID, or an ID that is not a requirement."], INTERNAL], example: "specctx bundle REQ-IDX-001 --budget 3000",
|
|
61
|
+
exit: [OK, ["2", "A usage error, an unknown ID, a bad path, or an ID that is not a requirement."], INTERNAL], example: "specctx bundle REQ-IDX-001 --budget 3000",
|
|
54
62
|
},
|
|
55
63
|
{
|
|
56
64
|
name: "check", group: "Read", args: "", summary: "Find ID and link errors.",
|
|
@@ -125,7 +133,11 @@ export const TABLE = [
|
|
|
125
133
|
},
|
|
126
134
|
{
|
|
127
135
|
name: "handoff resolve", group: "Write", args: "<n> <answer>", summary: "Move open question n to the resolved list.",
|
|
128
|
-
flags: [
|
|
136
|
+
flags: [
|
|
137
|
+
["--approve", "Approve a plan or archive question. A plan or archive question needs this or --reject."],
|
|
138
|
+
["--reject", "Reject a plan or archive question. The stop stays."],
|
|
139
|
+
],
|
|
140
|
+
exit: PLAIN, example: "specctx handoff resolve 1 --approve \"yes, for one release\"",
|
|
129
141
|
},
|
|
130
142
|
// Setup
|
|
131
143
|
{
|