specflow-cc 1.22.1 → 1.23.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/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to SpecFlow will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.23.0] - 2026-05-27
9
+
10
+ ### Added
11
+
12
+ - **Strict TODO frontmatter validation in `cmdTodoReindex` (SPEC-015)** — the indexing chokepoint at [`bin/lib/todo.cjs`](bin/lib/todo.cjs) now enforces a `REQUIRED_TODO_FIELDS = ['id', 'title', 'created']` invariant per TODO file. Files missing any required field (or lacking a frontmatter block entirely) are no longer silently defaulted into the index — they are surfaced as `MALFORMED: <reason>` sentinel rows in `INDEX.md`, sorted after well-formed entries by `fileId` ascending. Each malformed file emits a per-file warning to stderr, the `**Total:**` summary line gains a `… X malformed` component, and `node bin/sf-tools.cjs todo reindex` exits non-zero (`process.exitCode = 1` after `output()` flushes) so callers can gate on it. Well-formed runs remain byte-identical to the legacy format. Validation lives at the call site, not in shared `parseFrontmatter()` — `cmdTodoLoad` / `cmdTodoList` keep their tolerance. 6 new tests in `tests/todo-index.test.cjs` cover the four malformed shapes, malformed-YAML, and a regression guard for legacy parity.
13
+ - **`todo check-stale` exit gate (TODO-029)** — `cmdTodoCheckStale` in [`bin/lib/todo.cjs`](bin/lib/todo.cjs) now sets `process.exitCode = 1` when `INDEX.md` drifts from disk (ghost rows or missing entries). Previously it only reported drift via JSON/text output and always exited 0, so callers could not gate on it.
14
+
15
+ ### Fixed
16
+
17
+ - **Ghost-row drift after `/sf:done` and `/sf:plan` finalize (TODO-029)** — the LLM-orchestrated `rm .specflow/todos/{source}.md` + `node bin/sf-tools.cjs todo reindex` sequence in [`commands/sf/done.md`](commands/sf/done.md) Step 7.5 and [`commands/sf/plan.md`](commands/sf/plan.md) Step 7 (plus its inline fallback) had no enforced atomicity — any inversion, interruption, or skipped step left `INDEX.md` with a row pointing at a deleted file, and the only enforcement was `/sf:status` running `check-stale` reactively, days later. Both command files now invoke `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale` immediately after the `rm` + `reindex` block and treat non-zero exit as a finalization failure (re-run reindex once; if still stale, halt and surface `extra_in_index` / `missing_from_index` to the user). Surfaced in a downstream project (`~/Projects/topgun`) where SPEC-275 finalization left TODO-406 as a ghost in `INDEX.md`, then `/sf:plan TODO-406` operated on it.
18
+
19
+ ## [1.22.2] - 2026-05-22
20
+
21
+ ### Fixed
22
+
23
+ - **Installed `/sf:*` commands broken on user projects** — every `/sf:*` command shipped in `commands/sf/*.md` is now safe to run from a project directory with no local `bin/`. Two bugs fixed:
24
+ 1. **Bare `bin/sf-tools.cjs` paths in 15 command files** (`audit.md`, `autopilot.md`, `discuss.md`, `done.md`, `fix.md`, `health.md`, `help.md`, `pause.md`, `review.md`, `revise.md`, `run.md`, `show.md`, `split.md`, `status.md`, `verify.md`) were rewritten to `~/.claude/specflow-cc/bin/sf-tools.cjs`. The installer rewrites this literal prefix to the actual install location, so every invocation now resolves correctly. Previously these lines errored with `Cannot find module '/path/to/project/bin/sf-tools.cjs'`.
25
+ 2. **Unsafe `state resolve $ARGUMENTS` in 8 commands** (`audit.md`, `autopilot.md`, `done.md`, `fix.md`, `pause.md`, `review.md`, `run.md`, `verify.md`) was replaced with a SPEC-ID parsing guard: `$ARGUMENTS` is split into `FIRST_TOKEN` (matched against `^SPEC-\d{3,}$`) and a per-command scope variable (`FIX_SCOPE`, `DONE_SCOPE`, `AUDIT_SCOPE`, …). Previously `/sf:fix all`, `/sf:done --apply=minor`, etc. produced spurious `SPEC_NOT_ACTIVE` errors because the scope/mode flag was passed to the resolver as if it were a SPEC-ID.
26
+
8
27
  ## [1.22.1] - 2026-05-20
9
28
 
10
29
  ### Fixed
package/bin/lib/todo.cjs CHANGED
@@ -15,6 +15,13 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
  const { output, error, safeReadFile, parseFrontmatter } = require('./core.cjs');
17
17
 
18
+ /**
19
+ * Required YAML frontmatter fields for each TODO file.
20
+ * When any of these is absent or blank, the reindex records the file as MALFORMED
21
+ * rather than silently defaulting to empty values (which would hide drift).
22
+ */
23
+ const REQUIRED_TODO_FIELDS = ['id', 'title', 'created'];
24
+
18
25
  /**
19
26
  * Priority sort order (lower number = higher priority in sort).
20
27
  */
@@ -284,23 +291,55 @@ function cmdTodoReindex(cwd, raw) {
284
291
  const parsed = parseFrontmatter(content);
285
292
  const fm = parsed.frontmatter;
286
293
 
294
+ // Determine which required fields are absent or blank.
295
+ const missing = REQUIRED_TODO_FIELDS.filter(k => !fm[k] || String(fm[k]).trim() === '');
296
+
297
+ if (missing.length > 0) {
298
+ // Distinguish "no frontmatter block at all" (fm has no keys) from "some
299
+ // fields present but specific ones missing".
300
+ const hasAnyKey = Object.keys(fm).length > 0;
301
+ const reason = hasAnyKey
302
+ ? 'missing fields: ' + missing.join(', ')
303
+ : 'no frontmatter block';
304
+
305
+ process.stderr.write('warn: ' + file + ' — ' + reason + '\n');
306
+
307
+ todos.push({
308
+ malformed: true,
309
+ fileId: file.replace('.md', ''),
310
+ reason,
311
+ });
312
+ continue;
313
+ }
314
+
287
315
  // Strip surrounding quotes from title (YAML may preserve them)
288
- let title = fm.title || '';
316
+ let title = String(fm.title);
289
317
  if ((title.startsWith('"') && title.endsWith('"')) || (title.startsWith("'") && title.endsWith("'"))) {
290
318
  title = title.slice(1, -1);
291
319
  }
292
320
 
293
321
  todos.push({
294
- id: fm.id || file.replace('.md', ''),
322
+ id: fm.id,
295
323
  title,
296
324
  priority: fm.priority || '—',
297
325
  status: fm.status || 'open',
298
- created: fm.created || '',
326
+ created: fm.created,
299
327
  });
300
328
  }
301
329
 
302
- // Sort by priority (high > medium > low > unset), then by created date
330
+ // Sort: well-formed records by priority then created date; malformed records
331
+ // always come after all well-formed ones, ordered by fileId ascending.
303
332
  todos.sort((a, b) => {
333
+ const am = a.malformed ? 1 : 0;
334
+ const bm = b.malformed ? 1 : 0;
335
+ if (am !== bm) return am - bm; // well-formed before malformed
336
+ if (a.malformed && b.malformed) {
337
+ // Both malformed: sort by fileId ascending
338
+ if (a.fileId < b.fileId) return -1;
339
+ if (a.fileId > b.fileId) return 1;
340
+ return 0;
341
+ }
342
+ // Both well-formed: priority then date
304
343
  const pa = priorityKey(a.priority);
305
344
  const pb = priorityKey(b.priority);
306
345
  if (pa !== pb) return pa - pb;
@@ -309,13 +348,21 @@ function cmdTodoReindex(cwd, raw) {
309
348
  return 0;
310
349
  });
311
350
 
312
- // Count by priority
351
+ // Count by priority — malformed records are excluded from priority breakdown.
313
352
  const counts = { high: 0, medium: 0, low: 0, unset: 0 };
353
+ let malformedCount = 0;
314
354
  for (const t of todos) {
315
- if (t.priority === 'high') counts.high++;
316
- else if (t.priority === 'medium') counts.medium++;
317
- else if (t.priority === 'low') counts.low++;
318
- else counts.unset++;
355
+ if (t.malformed) {
356
+ malformedCount++;
357
+ } else if (t.priority === 'high') {
358
+ counts.high++;
359
+ } else if (t.priority === 'medium') {
360
+ counts.medium++;
361
+ } else if (t.priority === 'low') {
362
+ counts.low++;
363
+ } else {
364
+ counts.unset++;
365
+ }
319
366
  }
320
367
 
321
368
  // Build INDEX.md
@@ -333,13 +380,28 @@ function cmdTodoReindex(cwd, raw) {
333
380
 
334
381
  for (let i = 0; i < todos.length; i++) {
335
382
  const t = todos[i];
336
- let title = t.title;
337
- if (title.length > 50) title = title.slice(0, 50) + '...';
338
- lines.push(`| ${i + 1} | ${t.id} | ${title} | ${t.priority} | ${t.status} | ${t.created} |`);
383
+ if (t.malformed) {
384
+ // Render the MALFORMED sentinel row; truncate the reason if very long.
385
+ let marker = 'MALFORMED: ' + t.reason;
386
+ if (marker.length > 50) marker = marker.slice(0, 50) + '...';
387
+ lines.push(`| ${i + 1} | ${t.fileId} | ${marker} | — | — | — |`);
388
+ } else {
389
+ let title = t.title;
390
+ if (title.length > 50) title = title.slice(0, 50) + '...';
391
+ lines.push(`| ${i + 1} | ${t.id} | ${title} | ${t.priority} | ${t.status} | ${t.created} |`);
392
+ }
339
393
  }
340
394
 
341
395
  lines.push('');
342
- lines.push(`**Total:** ${todos.length} items (${counts.high} high, ${counts.medium} medium, ${counts.low} low, ${counts.unset} unset)`);
396
+ // Malformed records count toward N items total; they are excluded only from
397
+ // the priority breakdown (H/M/L/unset). When all TODOs are well-formed the
398
+ // summary line is byte-identical to the legacy format so downstream parsers
399
+ // of well-formed runs stay compatible.
400
+ if (malformedCount > 0) {
401
+ lines.push(`**Total:** ${todos.length} items (${counts.high} high, ${counts.medium} medium, ${counts.low} low, ${counts.unset} unset, ${malformedCount} malformed)`);
402
+ } else {
403
+ lines.push(`**Total:** ${todos.length} items (${counts.high} high, ${counts.medium} medium, ${counts.low} low, ${counts.unset} unset)`);
404
+ }
343
405
  lines.push('');
344
406
  lines.push('---');
345
407
  const now = new Date();
@@ -350,7 +412,15 @@ function cmdTodoReindex(cwd, raw) {
350
412
  const indexPath = path.join(todosDir, 'INDEX.md');
351
413
  fs.writeFileSync(indexPath, lines.join('\n'), 'utf8');
352
414
 
353
- output({ reindexed: todos.length, path: indexPath }, raw, `Reindexed ${todos.length} TODOs INDEX.md`);
415
+ // Signal callers that drift was found set exit code non-zero AFTER writing
416
+ // INDEX.md (so the file is on disk) and BEFORE the output() call (so JSON
417
+ // still flushes). Do NOT call process.exit() here; Node will use exitCode
418
+ // when the event loop drains.
419
+ if (malformedCount > 0) {
420
+ process.exitCode = 1;
421
+ }
422
+
423
+ output({ reindexed: todos.length, malformed: malformedCount, path: indexPath }, raw, `Reindexed ${todos.length} TODOs → INDEX.md`);
354
424
  }
355
425
 
356
426
  /**
@@ -408,6 +478,13 @@ function cmdTodoCheckStale(cwd, raw) {
408
478
  extraInIndex.length > 0 ||
409
479
  (!indexExists && diskIds.size > 0);
410
480
 
481
+ // Exit non-zero when stale so callers can use this as a gate after
482
+ // delete-and-reindex sequences (per SPEC TODO-029). Set exitCode before
483
+ // output() so JSON still flushes; do not call process.exit() directly.
484
+ if (stale) {
485
+ process.exitCode = 1;
486
+ }
487
+
411
488
  output(
412
489
  {
413
490
  stale,
@@ -43,7 +43,22 @@ Exit.
43
43
 
44
44
  ## Step 2: Resolve Active Specification
45
45
 
46
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
46
+ Parse `$ARGUMENTS`:
47
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
48
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
49
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
50
+ - Set `AUDIT_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
51
+ - Else:
52
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
53
+ - Set `AUDIT_SCOPE="$ARGUMENTS"`
54
+
55
+ Call:
56
+
57
+ ```bash
58
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
59
+ ```
60
+
61
+ Use `AUDIT_SCOPE` in the subsequent argument-parsing step (Step 3.5).
47
62
 
48
63
  Parse the JSON response:
49
64
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -77,7 +92,7 @@ Exit.
77
92
 
78
93
  ## Step 3.5: Check for --import Flag
79
94
 
80
- Parse arguments for `--import "feedback text"` pattern.
95
+ Parse `AUDIT_SCOPE` for `--import "feedback text"` pattern.
81
96
 
82
97
  **If --import flag present:** Go to Step 4-IMPORT
83
98
  **Otherwise:** Continue to Step 4 (internal audit)
@@ -156,7 +171,7 @@ In spec frontmatter, set: `status: revision_requested`
156
171
 
157
172
  Update STATE.md via CLI:
158
173
  ```bash
159
- node bin/sf-tools.cjs state add-active SPEC-XXX external_review /sf:revise
174
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX external_review /sf:revise
160
175
  ```
161
176
  Add decision: "Imported external feedback for SPEC-XXX"
162
177
 
@@ -299,7 +314,7 @@ Tip: `/clear` recommended — executor needs fresh context for implementation
299
314
 
300
315
  ### If APPROVED (with optional recommendations):
301
316
 
302
- The `Recommendation:` line is emitted by the auditor agent (Step 7.5 in `agents/spec-auditor.md`) using `node bin/sf-tools.cjs recommend --source audit --critical 0 --minor N`. The STATE.md Next Step remains `/sf:run` (without the `--apply=minor` suffix) — the suffix is advisory here only.
317
+ The `Recommendation:` line is emitted by the auditor agent (Step 7.5 in `agents/spec-auditor.md`) using `node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source audit --critical 0 --minor N`. The STATE.md Next Step remains `/sf:run` (without the `--apply=minor` suffix) — the suffix is advisory here only.
303
318
 
304
319
  ```
305
320
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -50,7 +50,22 @@ Parse the command argument to determine execution mode:
50
50
 
51
51
  **CRITICAL — N>1 guard (autopilot must be unambiguous):**
52
52
 
53
- Call `node bin/sf-tools.cjs state resolve $SPEC_ID_ARG` (pass SPEC-ID arg if provided; omit if not).
53
+ Parse `$ARGUMENTS`:
54
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
55
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
56
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
57
+ - Set `AUTOPILOT_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
58
+ - Else:
59
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
60
+ - Set `AUTOPILOT_SCOPE="$ARGUMENTS"`
61
+
62
+ Call:
63
+
64
+ ```bash
65
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
66
+ ```
67
+
68
+ Use `AUTOPILOT_SCOPE` in the subsequent mode-determination step (e.g. `--all` flag).
54
69
 
55
70
  Parse the JSON response:
56
71
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -274,7 +289,7 @@ mv .specflow/specs/SPEC-XXX.md .specflow/archive/
274
289
  5. **Update STATE.md:**
275
290
  Remove SPEC-XXX from Active Specifications table:
276
291
  ```bash
277
- node bin/sf-tools.cjs state remove-active SPEC-XXX
292
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state remove-active SPEC-XXX
278
293
  ```
279
294
  Remove SPEC-XXX row from Queue table (using Read+Write).
280
295
 
@@ -62,7 +62,7 @@ Determine discussion mode from arguments:
62
62
  - Mode: `requirements-gathering`
63
63
 
64
64
  **Case E: No arguments**
65
- - Call `node bin/sf-tools.cjs state resolve` to resolve active spec
65
+ - Call `node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve` to resolve active spec
66
66
  - `{"action":"use","id":"SPEC-XXX"}` → discuss that spec
67
67
  - `{"action":"error","code":"NO_ACTIVE_SPEC"}` → ask what to discuss
68
68
  - `{"action":"ask","options":[...]}` → use AskUserQuestion to pick which spec to discuss
@@ -39,7 +39,22 @@ Exit.
39
39
 
40
40
  ## Step 2: Resolve Active Specification
41
41
 
42
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
42
+ Parse `$ARGUMENTS`:
43
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
44
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
45
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
46
+ - Set `DONE_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
47
+ - Else:
48
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
49
+ - Set `DONE_SCOPE="$ARGUMENTS"`
50
+
51
+ Call:
52
+
53
+ ```bash
54
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
55
+ ```
56
+
57
+ Use `DONE_SCOPE` in the subsequent argument-parsing step (Step 2.5).
43
58
 
44
59
  Parse the JSON response:
45
60
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -61,7 +76,7 @@ Parse the JSON response:
61
76
 
62
77
  ## Step 2.5: Handle `--apply=minor` Flag
63
78
 
64
- **Check if `--apply=minor` was passed in the invocation arguments.**
79
+ **Check if `DONE_SCOPE` contains `--apply=minor`.**
65
80
 
66
81
  **If `--apply=minor` is NOT present:** Continue to Step 3 (existing behavior unchanged).
67
82
 
@@ -85,7 +100,7 @@ Extract Critical, Major, and Minor counts from that entry.
85
100
 
86
101
  Run:
87
102
  ```bash
88
- node bin/sf-tools.cjs recommend --source review --critical N --major M --minor K
103
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source review --critical N --major M --minor K
89
104
  ```
90
105
 
91
106
  Parse the JSON response.
@@ -385,6 +400,15 @@ rm .specflow/todos/{source}.md
385
400
  node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
386
401
  ```
387
402
 
403
+ Then run the stale-check exit gate. The `rm` + `reindex` sequence is LLM-orchestrated and has no machine-enforced atomicity — any inversion, interruption, or skipped step leaves INDEX.md with a ghost row. Treat non-zero exit as a **finalization failure**: re-run `todo reindex`, and if still stale, surface the drift to the user before continuing.
404
+
405
+ ```bash
406
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale
407
+ ```
408
+
409
+ - **Exit 0 (FRESH):** proceed to Step 8.
410
+ - **Exit 1 (STALE):** re-run `todo reindex` once; if `check-stale` still exits non-zero, halt finalization and report `extra_in_index` / `missing_from_index` from the JSON output so the user can repair the drift manually.
411
+
388
412
  3. **If NOT_FOUND (backward compatibility):** Also check legacy format — look in `.specflow/todos/TODO.md` for the referenced ID. If found there, remove the block using the Edit tool.
389
413
 
390
414
  No "Last updated" lines to update in per-file format.
@@ -414,7 +438,7 @@ If the command fails (parser cannot extract required fields), log a warning to t
414
438
  ### Remove from Active Specifications Table
415
439
 
416
440
  ```bash
417
- node bin/sf-tools.cjs state remove-active SPEC-XXX
441
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state remove-active SPEC-XXX
418
442
  ```
419
443
 
420
444
  ### Remove from Queue
@@ -43,7 +43,22 @@ Exit.
43
43
 
44
44
  ## Step 2: Resolve Active Specification
45
45
 
46
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
46
+ Parse `$ARGUMENTS`:
47
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
48
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
49
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
50
+ - Set `FIX_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
51
+ - Else:
52
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
53
+ - Set `FIX_SCOPE="$ARGUMENTS"`
54
+
55
+ Call:
56
+
57
+ ```bash
58
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
59
+ ```
60
+
61
+ Use `FIX_SCOPE` in the subsequent argument-parsing step (Step 5).
47
62
 
48
63
  Parse the JSON response:
49
64
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -93,14 +108,16 @@ Exit.
93
108
 
94
109
  ## Step 5: Parse Arguments
95
110
 
96
- | Argument | Action |
97
- |----------|--------|
98
- | (none) | Interactive mode show issues, ask what to fix |
111
+ Parse `FIX_SCOPE` (set in Step 2):
112
+
113
+ | Value of `FIX_SCOPE` | Action |
114
+ |----------------------|--------|
115
+ | (empty) | Interactive mode — show issues, ask what to fix |
99
116
  | "all" | Apply all critical AND major AND minor fixes |
100
117
  | "1,2,3" | Apply only numbered items |
101
118
  | "..." | Treat as custom fix instructions |
102
119
 
103
- ### If Interactive Mode (no arguments):
120
+ ### If Interactive Mode (FIX_SCOPE is empty):
104
121
 
105
122
  Display review findings:
106
123
 
@@ -182,7 +199,7 @@ Append to Review History:
182
199
  **If `--internal` is NOT set (normal invocation):**
183
200
 
184
201
  ```bash
185
- node bin/sf-tools.cjs state add-active SPEC-XXX review /sf:review
202
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX review /sf:review
186
203
  ```
187
204
 
188
205
  ## Step 9: Display Result
@@ -38,7 +38,7 @@ Exit.
38
38
  Run migration on entry — idempotent, no-op when already migrated:
39
39
 
40
40
  ```bash
41
- node bin/sf-tools.cjs state migrate
41
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state migrate
42
42
  ```
43
43
 
44
44
  Parse the response:
@@ -84,7 +84,7 @@ For each check:
84
84
  Read STATE.md and validate:
85
85
 
86
86
  **E003: Active spec references non-existent file**
87
- - List all active specs via `node bin/sf-tools.cjs state list-active`
87
+ - List all active specs via `node ~/.claude/specflow-cc/bin/sf-tools.cjs state list-active`
88
88
  - For each SPEC-ID, check `.specflow/specs/{ID}.md` exists
89
89
  - If missing: error (repairable — remove that row via `state remove-active`)
90
90
 
@@ -206,7 +206,7 @@ Exit.
206
206
 
207
207
  Show active specs count (uses list-active for multi-spec awareness):
208
208
  ```bash
209
- node bin/sf-tools.cjs state list-active --raw
209
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state list-active --raw
210
210
  ```
211
211
 
212
212
  Display full command reference:
@@ -36,7 +36,22 @@ Exit.
36
36
 
37
37
  ## Step 2: Resolve Active Specification
38
38
 
39
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
39
+ Parse `$ARGUMENTS`:
40
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
41
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
42
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
43
+ - Set `PAUSE_NOTE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
44
+ - Else:
45
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
46
+ - Set `PAUSE_NOTE="$ARGUMENTS"`
47
+
48
+ Call:
49
+
50
+ ```bash
51
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
52
+ ```
53
+
54
+ Use `PAUSE_NOTE` in Step 6.5 (notes prompt — pre-populate if non-empty).
40
55
 
41
56
  Parse the JSON response:
42
57
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -53,7 +68,7 @@ Parse the JSON response:
53
68
  Options: {id — title (status)} for each entry
54
69
  ```
55
70
 
56
- Also call `node bin/sf-tools.cjs state list-active` to capture all active specs in the pause file.
71
+ Also call `node ~/.claude/specflow-cc/bin/sf-tools.cjs state list-active` to capture all active specs in the pause file.
57
72
 
58
73
  ## Step 3: Load Active Specification Details
59
74
 
@@ -179,6 +179,15 @@ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
179
179
 
180
180
  This is mandatory — skipping it leaves INDEX.md listing a TODO that no longer exists on disk, which trips the `/sf:status` freshness check and breaks downstream consumers.
181
181
 
182
+ 4. **Run the stale-check exit gate.** The `rm` + `reindex` sequence is LLM-orchestrated; this surfaces drift immediately instead of deferring it to the next `/sf:status`.
183
+
184
+ ```bash
185
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale
186
+ ```
187
+
188
+ - **Exit 0 (FRESH):** proceed to Step 8.
189
+ - **Exit 1 (STALE):** re-run `todo reindex` once; if `check-stale` still exits non-zero, halt the conversion and report `extra_in_index` / `missing_from_index` from the JSON output so the user can repair the drift manually.
190
+
182
191
  ## Step 8: Display Result
183
192
 
184
193
  **IMPORTANT:** Output the following directly as formatted text, NOT wrapped in a markdown code block:
@@ -240,13 +249,16 @@ Use `/sf:new "{todo description}"` logic:
240
249
 
241
250
  ### Remove Todo
242
251
 
243
- Delete the file `.specflow/todos/TODO-{XXX}.md`, then refresh INDEX.md:
252
+ Delete the file `.specflow/todos/TODO-{XXX}.md`, then refresh INDEX.md and run the stale-check exit gate:
244
253
 
245
254
  ```bash
246
255
  rm .specflow/todos/TODO-{XXX}.md
247
256
  node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
257
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale
248
258
  ```
249
259
 
260
+ Treat non-zero exit from `check-stale` as a failure: re-run `todo reindex`, and if still stale, surface the drift to the user before continuing.
261
+
250
262
  </fallback>
251
263
 
252
264
  <success_criteria>
@@ -39,7 +39,22 @@ Exit.
39
39
 
40
40
  ## Step 2: Resolve Active Specification
41
41
 
42
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
42
+ Parse `$ARGUMENTS`:
43
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
44
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
45
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
46
+ - Set `REVIEW_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
47
+ - Else:
48
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
49
+ - Set `REVIEW_SCOPE="$ARGUMENTS"`
50
+
51
+ Call:
52
+
53
+ ```bash
54
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
55
+ ```
56
+
57
+ Use `REVIEW_SCOPE` in subsequent steps for any review flags.
43
58
 
44
59
  Parse the JSON response:
45
60
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -158,7 +173,7 @@ After the agent updates STATE.md, check if rotation is needed:
158
173
 
159
174
  ### If APPROVED (no minor issues):
160
175
 
161
- The `Recommendation:` line is emitted by the reviewer agent (Step 7.5 in `agents/impl-reviewer.md`) using `node bin/sf-tools.cjs recommend --source review --critical 0 --major 0 --minor 0`. The STATE.md Next Step remains `/sf:done` (canonical).
176
+ The `Recommendation:` line is emitted by the reviewer agent (Step 7.5 in `agents/impl-reviewer.md`) using `node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source review --critical 0 --major 0 --minor 0`. The STATE.md Next Step remains `/sf:done` (canonical).
162
177
 
163
178
  ```
164
179
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -345,9 +360,9 @@ Append Review History to spec.
345
360
 
346
361
  ```bash
347
362
  # If APPROVED:
348
- node bin/sf-tools.cjs state add-active SPEC-XXX done /sf:done
363
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX done /sf:done
349
364
  # If CHANGES_REQUESTED:
350
- node bin/sf-tools.cjs state add-active SPEC-XXX review /sf:fix
365
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX review /sf:fix
351
366
  ```
352
367
 
353
368
  </fallback>
@@ -42,7 +42,22 @@ Exit.
42
42
 
43
43
  ## Step 2: Resolve Active Specification
44
44
 
45
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided; strip non-SPEC-ID args first).
45
+ Parse `$ARGUMENTS`:
46
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
47
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
48
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
49
+ - Set `REVISE_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
50
+ - Else:
51
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
52
+ - Set `REVISE_SCOPE="$ARGUMENTS"`
53
+
54
+ Call:
55
+
56
+ ```bash
57
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
58
+ ```
59
+
60
+ Use `REVISE_SCOPE` in the subsequent argument-parsing step (Step 5).
46
61
 
47
62
  Parse the JSON response:
48
63
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -160,9 +175,11 @@ Continue to Step 5 with analysis context available.
160
175
 
161
176
  ## Step 5: Parse Arguments
162
177
 
163
- | Argument | Action |
164
- |----------|--------|
165
- | (none) | Interactive mode show comments, ask what to fix |
178
+ Parse `REVISE_SCOPE` (set in Step 2):
179
+
180
+ | Value of `REVISE_SCOPE` | Action |
181
+ |-------------------------|--------|
182
+ | (empty) | Interactive mode — show comments, ask what to fix |
166
183
  | "all" | Apply all critical issues AND recommendations |
167
184
  | "1,2,3" | Apply only numbered items |
168
185
  | "--no-analysis" | Skip pre-analysis, go directly to review mode |
@@ -170,9 +187,9 @@ Continue to Step 5 with analysis context available.
170
187
 
171
188
  **Check for `--no-analysis` flag:**
172
189
 
173
- If the arguments string contains `--no-analysis`:
190
+ If `REVISE_SCOPE` contains `--no-analysis`:
174
191
  - Set SKIP_ANALYSIS to true
175
- - Remove the `--no-analysis` flag from the arguments string for further processing
192
+ - Remove the `--no-analysis` flag from `REVISE_SCOPE` for further processing
176
193
 
177
194
  ### If Interactive Mode (no arguments):
178
195
 
@@ -532,7 +549,7 @@ In spec frontmatter: `status: auditing`
532
549
 
533
550
  In STATE.md:
534
551
  ```bash
535
- node bin/sf-tools.cjs state add-active SPEC-XXX auditing /sf:audit
552
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX auditing /sf:audit
536
553
  ```
537
554
 
538
555
  </fallback>
@@ -42,7 +42,22 @@ Exit.
42
42
 
43
43
  ## Step 2: Resolve Active Specification
44
44
 
45
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
45
+ Parse `$ARGUMENTS`:
46
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
47
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
48
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
49
+ - Set `RUN_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
50
+ - Else:
51
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
52
+ - Set `RUN_SCOPE="$ARGUMENTS"`
53
+
54
+ Call:
55
+
56
+ ```bash
57
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
58
+ ```
59
+
60
+ Use `RUN_SCOPE` in the subsequent argument-parsing step (Step 3.5).
46
61
 
47
62
  Parse the JSON response:
48
63
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
@@ -68,7 +83,7 @@ Read the active spec file: `.specflow/specs/SPEC-XXX.md`
68
83
 
69
84
  ## Step 3.5: Handle `--apply=minor` Flag
70
85
 
71
- **Check if `--apply=minor` was passed in the invocation arguments.**
86
+ **Check if `RUN_SCOPE` contains `--apply=minor`.**
72
87
 
73
88
  **If `--apply=minor` is NOT present:** Continue to Step 4 (existing behavior unchanged).
74
89
 
@@ -92,7 +107,7 @@ Extract Critical count and Recommendations count from that entry. Map Recommenda
92
107
 
93
108
  Run:
94
109
  ```bash
95
- node bin/sf-tools.cjs recommend --source audit --critical N --minor M
110
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source audit --critical N --minor M
96
111
  ```
97
112
 
98
113
  Parse the JSON response.
@@ -118,7 +133,7 @@ This reuses `/sf:revise`'s existing per-item commit behavior. Do NOT duplicate r
118
133
 
119
134
  Run spec structural validation:
120
135
  ```bash
121
- node bin/sf-tools.cjs spec validate SPEC-XXX
136
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs spec validate SPEC-XXX
122
137
  ```
123
138
 
124
139
  This is the exact gate specified in R2.5: verifies frontmatter parses, required fields present (`id`, `type`, `status`, `priority`), and `## Requirements` heading present. No fallback path.
@@ -265,7 +280,7 @@ Use model for `spec-executor` or `spec-executor-orchestrator` from selected prof
265
280
  ## Step 7: Update Status
266
281
 
267
282
  ```bash
268
- node bin/sf-tools.cjs state add-active SPEC-XXX running "(in progress)"
283
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXX running "(in progress)"
269
284
  ```
270
285
 
271
286
  Update spec frontmatter:
@@ -42,7 +42,7 @@ Exit.
42
42
  Use provided ID (e.g., SPEC-003).
43
43
 
44
44
  **If no argument:**
45
- Call `node bin/sf-tools.cjs state resolve` to get active spec.
45
+ Call `node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve` to get active spec.
46
46
 
47
47
  Parse the JSON response:
48
48
  - `{"action":"use","id":"SPEC-XXX"}` → use SPEC-XXX
@@ -58,7 +58,7 @@ Use `/sf:list` to see available specifications.
58
58
  Exit.
59
59
 
60
60
  **If no ID provided:**
61
- Call `node bin/sf-tools.cjs state resolve` to get active spec.
61
+ Call `node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve` to get active spec.
62
62
 
63
63
  Parse the JSON response:
64
64
  - `{"action":"use","id":"SPEC-XXX"}` → use SPEC-XXX
@@ -365,8 +365,8 @@ Add split reference to archived parent.
365
365
  - Add children to Queue (using Read+Write)
366
366
  - Register first child in Active Specifications table:
367
367
  ```bash
368
- node bin/sf-tools.cjs state remove-active SPEC-PARENT
369
- node bin/sf-tools.cjs state add-active SPEC-XXXa draft /sf:audit
368
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state remove-active SPEC-PARENT
369
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state add-active SPEC-XXXa draft /sf:audit
370
370
  ```
371
371
 
372
372
  </fallback>
@@ -39,12 +39,12 @@ Read `.specflow/STATE.md` and extract Queue, Recent Decisions, Warnings.
39
39
 
40
40
  Get active specs:
41
41
  ```bash
42
- node bin/sf-tools.cjs state list-active
42
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state list-active
43
43
  ```
44
44
 
45
45
  For single-spec display, also call:
46
46
  ```bash
47
- node bin/sf-tools.cjs state resolve
47
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve
48
48
  ```
49
49
 
50
50
  Parse the resolve response:
@@ -108,13 +108,13 @@ Fix: Rename the spec in specs/ to next available ID.
108
108
  Compare the set of TODO files on disk to the IDs listed in `.specflow/todos/INDEX.md`:
109
109
 
110
110
  ```bash
111
- node bin/sf-tools.cjs todo check-stale
111
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale
112
112
  ```
113
113
 
114
114
  Parse the JSON response. If `stale: true`, add a warning to the Warnings section:
115
115
 
116
116
  ```
117
- INDEX.md stale — run /sf:todos (or `node bin/sf-tools.cjs todo reindex`).
117
+ INDEX.md stale — run /sf:todos (or `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex`).
118
118
  {If missing_from_index is non-empty:}
119
119
  Missing from INDEX.md (TODO file exists on disk but not listed):
120
120
  {comma-separated list}
@@ -232,7 +232,7 @@ Based on state, provide additional guidance:
232
232
  - [ ] STATE.md loaded
233
233
  - [ ] PROJECT.md info extracted
234
234
  - [ ] Statistics calculated
235
- - [ ] TODO index freshness checked via `node bin/sf-tools.cjs todo check-stale` (warning surfaced if stale)
235
+ - [ ] TODO index freshness checked via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo check-stale` (warning surfaced if stale)
236
236
  - [ ] Current position displayed
237
237
  - [ ] Queue shown
238
238
  - [ ] Recommended next step clear
@@ -36,7 +36,22 @@ Exit.
36
36
 
37
37
  ## Step 2: Resolve Active Specification
38
38
 
39
- Call `node bin/sf-tools.cjs state resolve $ARGUMENTS` (pass the optional SPEC-XXX arg if provided).
39
+ Parse `$ARGUMENTS`:
40
+ - Let `FIRST_TOKEN` = first whitespace-separated token of `$ARGUMENTS`.
41
+ - If `FIRST_TOKEN` matches `^SPEC-\d{3,}$`:
42
+ - Set `SPEC_ARG="$FIRST_TOKEN"`
43
+ - Set `VERIFY_SCOPE` = remainder of `$ARGUMENTS` after `FIRST_TOKEN` (trimmed)
44
+ - Else:
45
+ - Set `SPEC_ARG=""` (resolver uses Active Specifications table)
46
+ - Set `VERIFY_SCOPE="$ARGUMENTS"`
47
+
48
+ Call:
49
+
50
+ ```bash
51
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs state resolve $SPEC_ARG
52
+ ```
53
+
54
+ Use `VERIFY_SCOPE` in subsequent steps for any verify flags.
40
55
 
41
56
  Parse the JSON response:
42
57
  - `{"action":"use","id":"SPEC-XXX"}` → proceed with SPEC-XXX
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specflow-cc",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "description": "Spec-driven development system for Claude Code — quality-first workflow with explicit audit cycles",
5
5
  "bin": {
6
6
  "specflow-cc": "bin/install.js"