specflow-cc 1.22.2 → 1.23.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/CHANGELOG.md +17 -0
- package/agents/impl-reviewer.md +2 -1
- package/agents/spec-auditor.md +3 -3
- package/agents/spec-reviser.md +3 -3
- package/bin/lib/todo.cjs +91 -14
- package/commands/sf/done.md +9 -0
- package/commands/sf/plan.md +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.1] - 2026-05-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **spec-auditor could not emit its Recommendation line** (agent lacked the Bash tool, and both audit/review agents referenced a non-existent relative `bin/sf-tools.cjs` path). Granted Bash to spec-auditor and corrected both paths to the canonical `~/.claude/specflow-cc/bin/sf-tools.cjs`. Also prefixed the same latent bare-path bug in [`agents/spec-reviser.md`](agents/spec-reviser.md) (`todo next-id` / `todo reindex`) so no bare relative `bin/sf-tools.cjs` remains in any agent. Both audit/review Step 7.5 blocks now fall back to the deterministic action mapping if the shell-out fails.
|
|
13
|
+
|
|
14
|
+
## [1.23.0] - 2026-05-27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **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.
|
|
19
|
+
- **`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.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
8
25
|
## [1.22.2] - 2026-05-22
|
|
9
26
|
|
|
10
27
|
### Fixed
|
package/agents/impl-reviewer.md
CHANGED
|
@@ -289,8 +289,9 @@ Using the Critical, Major, and Minor counts determined in Step 5:
|
|
|
289
289
|
|
|
290
290
|
1. Shell out to obtain the recommendation:
|
|
291
291
|
```
|
|
292
|
-
node bin/sf-tools.cjs recommend --source review --critical <N> --major <M> --minor <K>
|
|
292
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source review --critical <N> --major <M> --minor <K>
|
|
293
293
|
```
|
|
294
|
+
If the shell-out fails for any reason, derive the recommendation deterministically from the documented (critical/major/minor)→action mapping rather than omitting the line.
|
|
294
295
|
|
|
295
296
|
2. Parse the JSON response: `{ "action": "...", "reason": "..." }`
|
|
296
297
|
|
package/agents/spec-auditor.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sf-spec-auditor
|
|
3
3
|
description: Audits specifications for quality, completeness, and clarity in a fresh context
|
|
4
|
-
tools: Read, Write, Glob, Grep
|
|
4
|
+
tools: Read, Write, Glob, Grep, Bash
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<role>
|
|
@@ -776,9 +776,9 @@ Using the Critical count and Recommendations count determined in Step 5:
|
|
|
776
776
|
|
|
777
777
|
1. Shell out to obtain the recommendation:
|
|
778
778
|
```
|
|
779
|
-
node bin/sf-tools.cjs recommend --source audit --critical <N> --minor <M>
|
|
779
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs recommend --source audit --critical <N> --minor <M>
|
|
780
780
|
```
|
|
781
|
-
Note: The CLI flag is `--minor` even though the auditor uses the label "Recommendations" — this is intentional for parser symmetry across audit/review sources.
|
|
781
|
+
Note: The CLI flag is `--minor` even though the auditor uses the label "Recommendations" — this is intentional for parser symmetry across audit/review sources. If the shell-out fails for any reason, derive the recommendation deterministically from the documented (critical/minor)→action mapping rather than omitting the line.
|
|
782
782
|
|
|
783
783
|
2. Parse the JSON response: `{ "action": "...", "reason": "..." }`
|
|
784
784
|
|
package/agents/spec-reviser.md
CHANGED
|
@@ -147,7 +147,7 @@ For each deferred item:
|
|
|
147
147
|
|
|
148
148
|
1. Generate next TODO ID:
|
|
149
149
|
```bash
|
|
150
|
-
node bin/sf-tools.cjs todo next-id --raw
|
|
150
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo next-id --raw
|
|
151
151
|
```
|
|
152
152
|
2. Create `.specflow/todos/TODO-{XXX}.md` using the Write tool:
|
|
153
153
|
```markdown
|
|
@@ -181,7 +181,7 @@ For each deferred item:
|
|
|
181
181
|
4. After the loop completes (at least one TODO created), refresh the INDEX.md cache so it reflects the newly-created files:
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
-
node bin/sf-tools.cjs todo reindex
|
|
184
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
**Important:** Both substeps are mandatory. Every deferred item MUST produce a TODO, and if any TODO is created the reindex helper MUST run before reporting completion. Skipping the reindex leaves `.specflow/todos/INDEX.md` missing the just-created entries, which the `/sf:status` freshness check will then flag. If TODO creation fails, report the failure — do not silently skip.
|
|
@@ -253,7 +253,7 @@ Tip: `/clear` recommended — auditor needs fresh context
|
|
|
253
253
|
- [ ] Revision Response recorded in Audit History
|
|
254
254
|
- [ ] Deferred items (if any) created as individual `.specflow/todos/TODO-XXX.md` files
|
|
255
255
|
- [ ] TODOs Created subsection appended to Response (if deferred items exist)
|
|
256
|
-
- [ ] INDEX.md refreshed via `node bin/sf-tools.cjs todo reindex` (if any TODO was created)
|
|
256
|
+
- [ ] INDEX.md refreshed via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex` (if any TODO was created)
|
|
257
257
|
- [ ] Frontmatter status updated
|
|
258
258
|
- [ ] STATE.md updated
|
|
259
259
|
- [ ] Clear summary of changes provided
|
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
|
|
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
|
|
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.
|
|
316
|
-
|
|
317
|
-
else if (t.priority === '
|
|
318
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/commands/sf/done.md
CHANGED
|
@@ -400,6 +400,15 @@ rm .specflow/todos/{source}.md
|
|
|
400
400
|
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
401
401
|
```
|
|
402
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
|
+
|
|
403
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.
|
|
404
413
|
|
|
405
414
|
No "Last updated" lines to update in per-file format.
|
package/commands/sf/plan.md
CHANGED
|
@@ -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>
|