spec-ctx 0.1.1 → 0.2.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.
@@ -0,0 +1,173 @@
1
+ // REQ-LOOP-001 specctx next: the next action from the files on disk, with a closed enum (024/D1, 024/D2, ADR-006 item 2).
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { applyBaseline } from "../baseline/baseline.js";
5
+ import { check } from "../check/check.js";
6
+ import { readQuestions } from "../handoff/questions.js";
7
+ import { changeKey, matchesChange } from "../index/scan.js";
8
+ import { listEvidence } from "../tasks/evidence.js";
9
+ import { hasEvidence, nextTask, tasksOf } from "../tasks/tasks.js";
10
+ import { cmpStr, cmpTuple } from "../util/pystr.js";
11
+ import { capQuestion, failsInWindow, openQuestionFor } from "./fails.js";
12
+ import { openspecCommand, openspecStatus } from "./openspec.js";
13
+ /** The closed action list. A skill stops on any value outside it. */
14
+ export const ACTIONS = ["plan", "cite", "start", "work", "fix", "repair", "gate", "archive", "ask", "stop"];
15
+ /** The keys of the object, sorted. Every state prints all of them. */
16
+ export const KEYS = ["action", "attempt", "change", "command", "human", "max_attempts", "reason", "state", "task"];
17
+ /** The rules of check whose finding means a task or a header lacks its citation (024/D2 step 4). */
18
+ const CITE_RULES = new Set(["missing-id", "task-without-req", "task-without-verify", "unknown-task-req"]);
19
+ const TASK_RULES = new Set(["task-without-req", "task-without-verify", "unknown-task-req"]);
20
+ /** The active change folders under openspec/changes/, sorted. The file system decides, so a change with no tasks.md counts. */
21
+ export function activeFolders(root) {
22
+ const dir = join(root, "openspec", "changes");
23
+ if (!existsSync(dir))
24
+ return [];
25
+ const out = [];
26
+ for (const n of readdirSync(dir)) {
27
+ if (n === "archive" || n.startsWith("."))
28
+ continue;
29
+ try {
30
+ if (statSync(join(dir, n)).isDirectory())
31
+ out.push(n);
32
+ }
33
+ catch {
34
+ // a vanished entry is not a change
35
+ }
36
+ }
37
+ return out.sort(cmpStr);
38
+ }
39
+ function questions(root) {
40
+ try {
41
+ return readQuestions(root);
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ /** The last line of the gate log, or null. */
48
+ function lastGate(root) {
49
+ const p = join(root, ".ctx/gate-log.jsonl");
50
+ if (!existsSync(p))
51
+ return null;
52
+ const lines = readFileSync(p, "utf8").split("\n").filter((l) => l.trim());
53
+ const last = lines.at(-1);
54
+ if (!last)
55
+ return null;
56
+ try {
57
+ const o = JSON.parse(last);
58
+ return { at: typeof o.at === "string" ? o.at : "", ok: o.ok === true };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** The `at` of the newest live evidence file of a change key, or "" with none. */
65
+ function newestEvidenceAt(root, key) {
66
+ let newest = "";
67
+ for (const rel of listEvidence(root)) {
68
+ if (rel.split("/")[2] !== key)
69
+ continue;
70
+ try {
71
+ const ev = JSON.parse(readFileSync(join(root, rel), "utf8"));
72
+ if (typeof ev.at === "string" && cmpStr(ev.at, newest) > 0)
73
+ newest = ev.at;
74
+ }
75
+ catch {
76
+ // a damaged evidence file is a check finding
77
+ }
78
+ }
79
+ return newest;
80
+ }
81
+ function readCurrent(root) {
82
+ try {
83
+ return JSON.parse(readFileSync(join(root, ".ctx/current.json"), "utf8"));
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /** REQ-LOOP-001 the decision of 024/D2. A pure function of the files. It writes nothing. */
90
+ export function decideNext(root, cfg, data, want) {
91
+ const openspec = openspecCommand(cfg.gate.openspec);
92
+ const d = { action: "stop", attempt: 0, change: null, command: null, human: false, max_attempts: cfg.loop.max_attempts, reason: null, state: "unknown", task: null };
93
+ const done = (o) => ({ ...d, ...o });
94
+ // step 0 and step 1: the change under consideration, or the plan stop with no change
95
+ const folders = activeFolders(root);
96
+ const folder = want === undefined ? folders[0] : folders.find((f) => matchesChange(f, want));
97
+ if (folder === undefined) {
98
+ const name = want ?? "<name>";
99
+ return done({ action: "plan", change: want ?? null, command: `${openspec} new change ${name}`, human: true, reason: want === undefined ? "no active change" : `no change ${want}`, state: "no_change" });
100
+ }
101
+ d.change = folder;
102
+ const key = changeKey(folder);
103
+ const entries = data.entries;
104
+ const ofChange = (t) => t.change === folder && !t.file.includes("/archive/");
105
+ const qs = questions(root);
106
+ // step 2: an open question for the change blocks it
107
+ const blocking = qs.find((q) => q.status === "open" && (q.task === folder || q.task === key || q.task.startsWith(`${key}/`)));
108
+ if (blocking) {
109
+ const task = blocking.task.includes("/") ? blocking.task : null;
110
+ const attempt = task ? failsInWindow(root, task, qs).length : 0;
111
+ return done({ action: "ask", attempt, command: `specctx handoff resolve ${blocking.n} "<answer>"`, human: true, reason: blocking.text, state: "question_open", task });
112
+ }
113
+ // step 3: planning state from OpenSpec
114
+ const st = openspecStatus(root, openspec, folder);
115
+ if (!st.ok)
116
+ return done({ action: "stop", reason: st.reason, state: "openspec_failed" });
117
+ if (!st.state.isPlanningComplete) {
118
+ return done({ action: "plan", command: st.state.nextStep ?? `${openspec} status --change ${folder}`, human: true, reason: "planning is not complete", state: "planning" });
119
+ }
120
+ // step 4: a citation gap in the files of the change
121
+ const prefix = `openspec/changes/${folder}/`;
122
+ const found = applyBaseline(root, check(root, cfg, data, true), []).findings
123
+ .filter((f) => CITE_RULES.has(f.rule) && f.file.startsWith(prefix))
124
+ .sort((a, b) => cmpTuple([a.file, a.line, a.rule], [b.file, b.line, b.rule]));
125
+ if (found.length) {
126
+ const first = found[0];
127
+ const task = found.find((f) => TASK_RULES.has(f.rule))?.id ?? null;
128
+ return done({ action: "cite", command: `specctx query --change ${folder} --kind req`, reason: `${first.rule} ${first.id} at ${first.file}:${first.line}`, state: "citation_missing", task });
129
+ }
130
+ // step 5: a hand tick without evidence gets the repair run
131
+ const tasks = tasksOf(entries).filter(ofChange).sort((a, b) => cmpTuple([a.file, a.line], [b.file, b.line]));
132
+ const ticked = tasks.find((t) => t.status === "done" && !hasEvidence(root, t));
133
+ if (ticked)
134
+ return done({ action: "repair", command: `specctx task done ${ticked.id}`, state: "done_without_evidence", task: ticked.id });
135
+ // step 6: the current task of this change
136
+ const cur = readCurrent(root);
137
+ const current = cur?.task && cur.task.split("/")[0] === key ? tasks.find((t) => t.id === cur.task && t.status === "open") : undefined;
138
+ if (current) {
139
+ const fails = failsInWindow(root, current.id, qs);
140
+ const last = fails.at(-1);
141
+ const reason = last ? `Verify failed (exit ${last.file.verify.exit ?? "timeout"}): ${last.file.stderr_tail.split("\n")[0]?.trim() || "no output"}` : null;
142
+ if (fails.length >= cfg.loop.max_attempts && !openQuestionFor(root, current.id, qs)) {
143
+ const q = capQuestion(current.id, fails.length, last?.file.stderr_tail ?? "");
144
+ return done({ action: "ask", attempt: fails.length, command: `specctx handoff ask "${q}"`, human: true, reason, state: "attempts_exhausted", task: current.id });
145
+ }
146
+ if (fails.length)
147
+ return done({ action: "fix", attempt: fails.length, command: `specctx task done ${current.id}`, reason, state: "task_failed", task: current.id });
148
+ return done({ action: "work", command: `specctx task done ${current.id}`, state: "task_started", task: current.id });
149
+ }
150
+ // step 7: the next open task
151
+ const open = nextTask(root, entries, folder);
152
+ if (open)
153
+ return done({ action: "start", command: `specctx task start ${open.id}`, state: "task_open", task: open.id });
154
+ // step 8: the gate must pass after the newest evidence
155
+ const gate = lastGate(root);
156
+ if (!gate || !gate.ok || cmpStr(gate.at, newestEvidenceAt(root, key)) < 0) {
157
+ return done({ action: "gate", command: "specctx gate --strict", state: "gate_due" });
158
+ }
159
+ // step 9: archive, with the human stop from [loop] archive
160
+ return done({ action: "archive", command: `${openspec} archive ${folder} -y`, human: cfg.loop.archive === "ask", state: "gate_passed" });
161
+ }
162
+ /** REQ-LOOP-001 REQ-CLI-007 the human form: action first, one line per key, next last (024/D1). Strict mode. */
163
+ export function renderNext(d) {
164
+ const show = (v) => (v === null ? "none" : String(v));
165
+ const lines = [`action: ${d.action}`];
166
+ for (const k of KEYS) {
167
+ if (k === "action" || k === "command")
168
+ continue;
169
+ lines.push(`${k}: ${show(d[k])}`);
170
+ }
171
+ lines.push(`next: ${show(d.command)}`);
172
+ return lines.join("\n") + "\n";
173
+ }
@@ -0,0 +1,41 @@
1
+ // REQ-LOOP-001 the one OpenSpec call of next: status --change <c> --json, two keys read (024/D3, ADR-006 item 2).
2
+ import { spawnSync } from "node:child_process";
3
+ const TIMEOUT_MS = 60_000;
4
+ /** REQ-LOOP-001 the command that gate and next run for OpenSpec. The env var wins over [gate] openspec (018/D2). */
5
+ export function openspecCommand(configured) {
6
+ return process.env.SPECCTX_OPENSPEC ?? configured;
7
+ }
8
+ const firstLine = (text) => text.split("\n").map((l) => l.trim()).find(Boolean) ?? "";
9
+ /** OpenSpec reports an error as JSON `{ status: [{ message }] }` on stdout. The first message is the reason. */
10
+ function errorMessage(stdout) {
11
+ try {
12
+ const o = JSON.parse(stdout);
13
+ const m = o.status?.find((s) => typeof s.message === "string")?.message;
14
+ return typeof m === "string" ? firstLine(m) : "";
15
+ }
16
+ catch {
17
+ return "";
18
+ }
19
+ }
20
+ /** Run `<openspec> status --change <folder> --json` in the root. A failure of any kind is a reason, never a throw. */
21
+ export function openspecStatus(root, openspec, folder) {
22
+ const r = spawnSync("/bin/sh", ["-c", `${openspec} status --change ${folder} --json`], { cwd: root, encoding: "utf8", timeout: TIMEOUT_MS });
23
+ if (r.error && r.error.code === "ETIMEDOUT") {
24
+ return { ok: false, reason: `openspec status timed out after ${TIMEOUT_MS / 1000}s` };
25
+ }
26
+ const head = errorMessage(r.stdout ?? "") || firstLine(r.stderr ?? "") || firstLine(r.stdout ?? "") || "no output";
27
+ if (r.status !== 0)
28
+ return { ok: false, reason: `openspec status failed (exit ${r.status ?? "none"}): ${head}` };
29
+ let raw;
30
+ try {
31
+ raw = JSON.parse(r.stdout);
32
+ }
33
+ catch {
34
+ return { ok: false, reason: `openspec status printed no JSON: ${head}` };
35
+ }
36
+ if (raw === null || typeof raw !== "object")
37
+ return { ok: false, reason: "openspec status printed no JSON object" };
38
+ const o = raw;
39
+ const steps = Array.isArray(o.nextSteps) ? o.nextSteps.filter((s) => typeof s === "string") : [];
40
+ return { ok: true, state: { isPlanningComplete: o.isPlanningComplete === true, nextStep: steps[0] ?? null } };
41
+ }
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { readBaseline } from "../baseline/baseline.js";
5
5
  import { changeKey, matchesChange } from "../index/scan.js";
6
+ import { isFailFile } from "../tasks/evidence.js";
6
7
  import { cmpStr, cpLen } from "../util/pystr.js";
7
8
  export const DEBT_RULES = ["uncovered", "scenario-untested", "done-without-evidence", "manual-evidence"];
8
9
  const NA = (n = 0) => ({ value: null, n, text: "n/a" });
@@ -21,7 +22,8 @@ function readEvidence(root) {
21
22
  continue;
22
23
  }
23
24
  for (const n of names) {
24
- if (!n.endsWith(".json"))
25
+ // REQ-LOOP-002 a fail file is the retry record, not evidence (024/D4)
26
+ if (!n.endsWith(".json") || isFailFile(n))
25
27
  continue;
26
28
  try {
27
29
  const ev = JSON.parse(readFileSync(join(dir, c, n), "utf8"));
@@ -79,7 +79,10 @@ export function retireEvidence(root, t, reason) {
79
79
  }
80
80
  return target;
81
81
  }
82
- /** Every live evidence file, as a path relative to the root. Reopened copies are history and stay out. */
82
+ /** REQ-LOOP-002 the file name pattern of a fail file. Every evidence reader skips it (024/D4). */
83
+ export const FAIL_RE = /\.fail-\d+\.json$/;
84
+ export const isFailFile = (name) => FAIL_RE.test(name);
85
+ /** Every live evidence file, as a path relative to the root. Reopened copies and fail files are history and stay out. */
83
86
  export function listEvidence(root) {
84
87
  const dir = join(root, ".ctx/evidence");
85
88
  if (!existsSync(dir))
@@ -94,7 +97,7 @@ export function listEvidence(root) {
94
97
  continue;
95
98
  }
96
99
  for (const n of names.sort()) {
97
- if (n.endsWith(".json") && !n.includes(".reopened-"))
100
+ if (n.endsWith(".json") && !n.includes(".reopened-") && !isFailFile(n))
98
101
  out.push(`.ctx/evidence/${c}/${n}`);
99
102
  }
100
103
  }
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // REQ-CLI-001 version string. tests/cli/version.test.ts keeps it equal to package.json.
2
- export const VERSION = "0.1.1";
2
+ export const VERSION = "0.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-ctx",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Deterministic context engine and task workflow for OpenSpec projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,19 +1,19 @@
1
1
  # spec-driven-ids
2
2
 
3
- A fork of the OpenSpec `spec-driven` schema. It adds stable IDs from spec-ctx to every artifact.
3
+ Templates and config rules that add stable IDs from spec-ctx to the OpenSpec `spec-driven` workflow. The project keeps `schema: spec-driven` in `openspec/config.yaml`. Upstream OpenSpec injects the `rules` and `operations` below into every artifact instruction, so the agent learns the ID commands from there.
4
4
 
5
- - The spec template starts each requirement with `REQ-<CAP>-NNN` and each scenario with `SCN-<CAP>-NNN-<n>`.
6
- - The instructions tell the agent to get IDs from `specctx id new` and to link IDs with `Refs:` lines.
7
- - The tasks template cites REQ IDs and puts the `Verify:` command in a code span, so `specctx task done` can run it.
8
- - `config-fragment.yaml` holds the `rules` and `operations` for `openspec/config.yaml`.
5
+ - `templates/spec.md` starts each requirement with `REQ-<CAP>-NNN` and each scenario with `SCN-<CAP>-NNN-<n>`.
6
+ - `templates/tasks.md` cites REQ IDs and puts the `Verify:` command in a code span, so `specctx task done` can run it.
7
+ - `templates/design.md` ends each decision with a `Refs:` line.
8
+ - `config-fragment.yaml` holds the `rules` and `operations` for `openspec/config.yaml`. The rules tell the agent to get IDs from `specctx id new` and name the templates path. The `apply` guidance says that a task closes only with `specctx task done <id>` and that the agent runs the `specctx-run` skill, never `/opsx:apply`.
9
9
 
10
10
  ## Install by copy
11
11
 
12
12
  - Clone the repository: `git clone https://github.com/sonqh/spec-ctx`.
13
- - Copy `schemas/spec-driven-ids` to `openspec/schemas/spec-driven-ids` in your project.
14
- - Set `schema: spec-driven-ids` in `openspec/config.yaml`.
13
+ - Copy `schemas/spec-driven-ids/templates` to `openspec/templates/spec-driven-ids` in your project.
14
+ - Keep `schema: spec-driven` in `openspec/config.yaml`.
15
15
  - Merge `config-fragment.yaml` into `openspec/config.yaml` by hand. Add its `rules` and `operations` keys. Keep every item quoted.
16
- - Run `openspec schema validate spec-driven-ids`.
16
+ - Run `openspec validate --all`.
17
17
 
18
18
  ## Install with init --schema
19
19
 
@@ -21,11 +21,11 @@ A fork of the OpenSpec `spec-driven` schema. It adds stable IDs from spec-ctx to
21
21
  npx -y spec-ctx init --schema
22
22
  ```
23
23
 
24
- - The command copies this folder to `openspec/schemas/spec-driven-ids/`.
25
- - It sets `schema: spec-driven-ids` in `openspec/config.yaml`.
26
- - It adds the `rules` and `operations` keys that the config does not have yet. It leaves other keys as they are.
24
+ - The command copies the four templates to `openspec/templates/spec-driven-ids/`.
25
+ - It adds the `rules` and `operations` keys that the config does not have yet. A key that exists gets the items it lacks. Other keys stay as they are.
27
26
  - It quotes a list item that YAML would read as a mapping.
27
+ - It does not touch the `schema` key. A project that still says `schema: spec-driven-ids` gets a note. Set `schema: spec-driven` by hand and delete `openspec/schemas/spec-driven-ids/`.
28
28
 
29
29
  ## Version
30
30
 
31
- Forked from OpenSpec 1.14.3 with `openspec schema fork spec-driven spec-driven-ids`. CI validates it against that version.
31
+ The templates follow the `spec-driven` templates of OpenSpec 1.14.3 with the ID placeholders added. `tests/schema/validate.sh` checks them on upstream OpenSpec in CI.
@@ -1,11 +1,12 @@
1
1
  # spec-ctx fragment for openspec/config.yaml. specctx init --schema adds each top-level key
2
- # that the project config does not have yet. It never changes a key that exists.
2
+ # that the project config does not have yet. A key that exists gets the items it lacks.
3
3
  rules:
4
4
  specs:
5
5
  - 'Every requirement header starts with its ID, e.g. "### Requirement: REQ-ABC-001 Name". Get the ID from `specctx id new REQ ABC`.'
6
6
  - 'Every scenario header starts with its ID, e.g. "#### Scenario: SCN-ABC-001-1 Name".'
7
7
  - 'A requirement may end with a line "Refs: REQ-ABC-002, ADR-001" to link related IDs.'
8
8
  - 'Requirement text stays under 500 characters. Move detail into scenarios.'
9
+ - 'The templates with the ID placeholders are in openspec/templates/spec-driven-ids/. Start a spec from spec.md and a task list from tasks.md there.'
9
10
  design:
10
11
  - 'Each decision is one bullet under "## Decisions". Add new decisions at the end.'
11
12
  tasks:
@@ -17,7 +18,7 @@ operations:
17
18
  guidance:
18
19
  - 'Start each task with `specctx task start <id>` and read the bundle it prints before you read specs.'
19
20
  - 'Tag code with the REQ ID it implements (a comment `// REQ-ABC-001`). Tag tests with `// Covers: REQ-ABC-001`.'
20
- - 'Close each task with `specctx task done <id>`. Never tick a checkbox by hand.'
21
+ - 'Close each task only with `specctx task done <id>`. Never edit a checkbox. Never run `/opsx:apply`; run the `specctx-run` skill.'
21
22
  - 'Run `specctx check --strict` after each task group.'
22
23
  archive:
23
24
  guidance:
@@ -3,25 +3,4 @@ name: specctx-bootstrap
3
3
  description: Start a session in an OpenSpec project that uses spec-ctx. Use at the start of every session, and on "start", "bootstrap", or "what should I work on". Loads only the spec text that the next task needs.
4
4
  ---
5
5
 
6
- # specctx-bootstrap
7
-
8
- Load the minimum context for one task. Run `specctx` as `npx spec-ctx` when it is not installed.
9
-
10
- ## Steps
11
-
12
- 1. Run `specctx status`. It rebuilds a stale index first. Note the active change, the current task, and the count of open questions. The last line `next: <step>` names the next step. Exit 2 with `not an OpenSpec project` means the folder has no `openspec/`. Tell the user to run `npx -y @fission-ai/openspec@latest init` and stop.
13
- 2. Read `.ctx/handoff.md` if it exists. Note `current`, `reopened`, and `open_questions`.
14
- 3. If `status` prints `no active change`, show the open questions to the user. Ask which change to start. Stop here.
15
- 4. Pick the task. Use `current` from the handoff, then the user's message, then the output of `specctx task next`.
16
- 5. Run `specctx task start <task>`. It prints the bundle for the REQ IDs of the task and sets the current task.
17
- 6. Read the bundle. Each block starts with `-- <id> | <file>:<line> | <status> | <reason>`. A `modified` block is the pending text of an active change. Trust it over the main spec.
18
- 7. If the bundle ends with `truncated: N entries omitted: <ids>`, run `specctx bundle <id>` for each ID that the task needs.
19
- 8. Say in one line which task you are on and which IDs you loaded. Then start the work.
20
-
21
- ## Rules
22
-
23
- - Do not open `openspec/specs/` or `openspec/changes/*/specs/` by hand. Use `specctx bundle` or `specctx task start`.
24
- - Read at most 10,000 characters of spec text before the first code edit.
25
- - Tag code with `// REQ-ABC-001` and tests with `// Covers: REQ-ABC-001`.
26
- - Get a new ID from `specctx id new REQ <CAP>`. Never count numbers by hand.
27
- - Close the task with the `specctx-done` skill.
6
+ This skill is now `specctx-run`. Run it.
@@ -3,23 +3,4 @@ name: specctx-done
3
3
  description: Close a task in an OpenSpec project that uses spec-ctx. Runs the Verify command, writes evidence, sets the checkbox, writes the handoff, and shows the metrics. Use when the work of a task is complete, or on "done", "close the task", and "end the session".
4
4
  ---
5
5
 
6
- # specctx-done
7
-
8
- Close the current task with evidence. Run `specctx` as `npx spec-ctx` when it is not installed.
9
-
10
- ## Steps
11
-
12
- 1. Run `specctx task done <task>`. It runs the `Verify:` command of the task in the project root.
13
- 2. On exit 0 it writes `.ctx/evidence/<change>/<task>.json` and sets `[x]` on the task line. Nothing else in `tasks.md` changes.
14
- 3. On a failure it prints the last 20 lines of output and then the line `Verify failed (exit <n>). The task stays open.` It exits 1. Fix the cause and run step 1 again.
15
- 4. If the task has prose after `Verify:` and no command, run `specctx task done <task> --manual "<what you checked>"`.
16
- 5. After the last task of a group, run `specctx check --strict`. Fix every finding before the next group.
17
- 6. Run `specctx handoff write`. It writes `.ctx/handoff.md` from the index, the evidence, and the open questions.
18
- 7. Run `specctx stats --change <change>`. Report the five metrics of the change in one line each.
19
- 8. Commit the work together with `.ctx/`. Leave `.ctx/index.json` out of git.
20
-
21
- ## Rules
22
-
23
- - Never tick a checkbox by hand. `check --strict` reports a ticked task with no evidence as `done-without-evidence`.
24
- - If a spec changed after you closed a task, run `specctx task reopen <task> "<reason>"`.
25
- - To leave a question for the next session, run `specctx handoff ask "<question>"` before step 6.
6
+ This skill is now `specctx-run`. Run it.
@@ -3,21 +3,4 @@ name: specctx-next
3
3
  description: Pick and start the next task in the middle of a session in an OpenSpec project that uses spec-ctx. Use after you close a task, or on "next task" and "what is next".
4
4
  ---
5
5
 
6
- # specctx-next
7
-
8
- Move to the next open task. Run `specctx` as `npx spec-ctx` when it is not installed.
9
-
10
- ## Steps
11
-
12
- 1. Run `specctx task next`. It prints the task ID and text, then `reqs:` and `verify:`.
13
- 2. If it prints `no open task. Every task in the active changes is done. Next: archive <change>.`, every task of the active changes has its checkbox set. Run `specctx status`. Its last line is `next: archive <change>`. Tell the user that the change is ready for `specctx check --strict` and archive. Stop here.
14
- 3. Run `specctx task start <task>`. It prints the bundle for the REQ IDs of the task and sets the current task in `.ctx/current.json`.
15
- 4. Read the bundle. Request an omitted ID with `specctx bundle <id>` only when the task needs it.
16
- 5. Run `specctx task verify <task>` once before the work. It runs the `Verify:` command and writes nothing. A failure now shows what the work must fix.
17
- 6. Say in one line which task you are on. Then start the work.
18
-
19
- ## Rules
20
-
21
- - Take the tasks in the order that `specctx task next` gives. Earlier tasks in a group come first.
22
- - A question that blocks you goes to `specctx handoff ask "<question>"`. Then continue with what you can do.
23
- - Close the task with the `specctx-done` skill.
6
+ This skill is now `specctx-run`. Run it.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: specctx-run
3
+ description: Run the spec-ctx loop in an OpenSpec project. Use on "run", "continue", "next task", "build this", "carry on", and on a first message that describes a change to build. The engine picks the next action. The skill acts on it until the change reaches the archive or a human must decide.
4
+ ---
5
+
6
+ # specctx-run
7
+
8
+ Run `specctx` as `npx spec-ctx` when it is not installed. `openspec` is the command in `[gate] openspec` of `spec-ctx.toml`.
9
+
10
+ ## The loop
11
+
12
+ 1. Run `specctx next --json`. Read `action`, `change` (`<c>`), `task` (`<t>`), `command`, `reason`, and `human`.
13
+ 2. Act on `action` as the table below says.
14
+ 3. Run `specctx handoff write`.
15
+ 4. Repeat from step 1 until `action` is `stop` or `human` is true.
16
+
17
+ ## Actions
18
+
19
+ - `plan`: follow the upstream `openspec-propose` skill for the change in `command`. Take every ID from `specctx id new REQ <CAP>` and `specctx id new SCN <CAP> --for <REQ>`. Give every task a `[REQ-...]` citation and a `Verify:` code span. Then run `specctx check --strict` and `openspec validate <c> --strict`. Fix every finding. Stop and show the plan to the user. This is the plan stop.
20
+ - `cite`: run `specctx query --change <c> --kind req`. Add the `[REQ-...]` citation and a `Verify:` code span to the task line named in `task`. Run `specctx check`. Continue.
21
+ - `start`: run `specctx task start <t>`. Read the bundle it prints. Continue.
22
+ - `work`: implement the task. Tag code with `// REQ-...`. Tag tests with `// Covers: REQ-...`. Then run `specctx task done <t>`.
23
+ - `fix`: read `reason`. It holds the tail of the failed Verify output. Fix the cause. Run `specctx task done <t>` again.
24
+ - `repair`: run `specctx task done <t>`. It runs Verify for a ticked task that has no evidence.
25
+ - `gate`: run `specctx gate --strict`. Fix every finding. Run it again until it exits 0.
26
+ - `archive`: when `human` is true, stop and ask the user in one line whether to archive `<c>`. When `human` is false, follow the upstream `openspec-archive-change` skill. Then run `openspec validate --all --strict`, `specctx check --strict`, and `specctx handoff write`.
27
+ - `ask`: print `reason`. It is the open question. Stop. The user answers with `specctx handoff resolve <n> "<answer>"`.
28
+ - `stop`: print `reason`. Stop.
29
+
30
+ ## Rules
31
+
32
+ - Never tick a checkbox by hand. `specctx task done` is the only way a task closes.
33
+ - Never use `--manual` on a task with a Verify command.
34
+ - Never call `/opsx:apply`. This skill replaces it.
35
+ - Never read `openspec/specs/` or `openspec/changes/*/specs/` directly. Use the bundle from `task start` or `specctx bundle <id>`.
36
+ - Stop on any `action` not in the list above. Print the object and ask the user.
37
+ - Keep no state in your context. A new session starts the same loop again with step 1.