specflow-cc 1.18.2 → 1.19.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,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.19.0] - 2026-04-12
9
+
10
+ ### Added
11
+
12
+ - **Deferred work detection in auditor** — the auditor now detects deferred work items (TODOs, FIXMEs, placeholder implementations) in the spec and flags them as findings (step 3.9.5)
13
+
14
+ ### Fixed
15
+
16
+ - **TODO INDEX.md auto-sync** — `INDEX.md` in `.specflow/todos/` was getting out of sync because `/sf:todo` and `/sf:done` modified TODO files without regenerating the index. Added `todo reindex` command to `sf-tools.cjs` and wired it into both `/sf:todo` (Step 6.5) and `/sf:done` (Step 7.5, after TODO file deletion)
17
+
18
+ ## [1.18.3] - 2026-04-11
19
+
20
+ ### Fixed
21
+
22
+ - **Auto-heal for users upgrading from 1.18.0 / 1.18.1** — the installer now scans `hooks.PostToolUse` during every install and removes any flat `{ type, command }` entry that references `context-monitor`. These entries were written by the buggy `1.18.0`/`1.18.1` installer and would otherwise linger in `settings.json` next to the newly written correct matcher group, still tripping Claude Code's `"Expected array, but received undefined"` parse error. Foreign flat entries (belonging to other tools) are left untouched — only entries unambiguously written by prior SpecFlow versions are removed.
23
+ - Three new smoke tests cover clean auto-heal, preservation of foreign flat entries, and the mixed state (pre-existing correct hook + flat broken duplicate).
24
+
8
25
  ## [1.18.2] - 2026-04-11
9
26
 
10
27
  ### Fixed
@@ -457,6 +457,55 @@ Check if spec includes items marked as "Out of Scope" or "Deferred" in PROJECT.m
457
457
  **If compliant:**
458
458
  - Add to audit output: "Project compliance: ✓ Honors PROJECT.md decisions"
459
459
 
460
+ ## Step 3.9.5: Deferred Work Detection
461
+
462
+ Scan the specification's prose sections for mentions of work that is explicitly deferred, out of scope, or planned for a follow-up. This prevents deferred work from being silently lost when the spec is archived.
463
+
464
+ ### 3.9.5.1 Scan Sections
465
+
466
+ Search the following sections of the spec for deferred work signals:
467
+ - Context
468
+ - Goal Analysis
469
+ - Requirements
470
+ - Constraints
471
+ - Assumptions
472
+ - Acceptance Criteria
473
+
474
+ **Signal patterns to detect** (case-insensitive):
475
+ - "defer", "deferred"
476
+ - "follow-up", "follow up", "followup"
477
+ - "future work", "future spec", "future phase"
478
+ - "separate spec", "separate task"
479
+ - "out of scope", "out-of-scope"
480
+ - "not in this spec", "not included in this spec"
481
+ - "will be added later", "added in a later"
482
+ - "TBD", "to be determined"
483
+ - "planned for", "tracked as", "tracked in"
484
+
485
+ ### 3.9.5.2 Cross-Reference with TODOs
486
+
487
+ For each deferred item found:
488
+
489
+ 1. Extract a short description of the deferred work from the surrounding sentence
490
+ 2. Check if a corresponding TODO already exists:
491
+ - Search `.specflow/todos/` files for keywords from the deferred item description
492
+ - If a matching TODO exists: note as covered
493
+ - If no matching TODO found: flag as untracked
494
+
495
+ ### 3.9.5.3 Deferred Work Verdict
496
+
497
+ **If untracked deferred items found:**
498
+ - Add **Recommendation** with prefix `[Deferred]` for each:
499
+ - `"Deferred work mentioned but no TODO found: '{quoted phrase from spec}'. Create via /sf:todo to prevent scope loss."`
500
+
501
+ **If all deferred items have matching TODOs:**
502
+ - Add to audit output: `"Deferred work: ✓ All deferred items tracked"`
503
+
504
+ **If no deferred work mentions found:**
505
+ - Omit from audit output (no line needed)
506
+
507
+ **Note:** This is always a Recommendation, never Critical — the spec itself may be correct, and the TODO may have been created outside SpecFlow or may be intentionally deferred to a later planning session.
508
+
460
509
  ## Step 3.10: Language Profile Check
461
510
 
462
511
  **Detection:** Check if PROJECT.md contains a `## Language Profile` section.
@@ -840,6 +889,7 @@ Tip: `/clear` recommended before `/sf:run` — executor needs fresh context
840
889
  - [ ] PROJECT.md context loaded
841
890
  - [ ] All 10 dimensions evaluated (clarity, completeness, testability, scope, feasibility, architecture, duplication, cognitive load, strategic fit, project compliance)
842
891
  - [ ] Language profile checked (if present in PROJECT.md)
892
+ - [ ] Deferred work scanned and cross-referenced with TODOs
843
893
  - [ ] Assumptions extracted and impact assessed
844
894
  - [ ] Project alignment verified
845
895
  - [ ] Project compliance verified (decisions, constraints, out-of-scope)
package/bin/install.js CHANGED
@@ -289,6 +289,32 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
289
289
  if (!settings.hooks) settings.hooks = {};
290
290
  if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
291
291
 
292
+ // Auto-heal: remove flat, broken context-monitor entries written by
293
+ // installer versions 1.18.0 and 1.18.1. Those versions pushed
294
+ // { type: "command", command: "..." } directly into PostToolUse, but
295
+ // Claude Code requires every PostToolUse entry to be a matcher group
296
+ // of shape { matcher?, hooks: [{ type, command }, ...] }. A flat entry
297
+ // makes Claude Code fail to parse settings.json entirely. Only entries
298
+ // that are unambiguously ours (flat + command references context-monitor)
299
+ // are removed; everything else is left untouched.
300
+ {
301
+ const before = settings.hooks.PostToolUse.length;
302
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(entry => {
303
+ const isFlatBrokenMonitor =
304
+ entry &&
305
+ !Array.isArray(entry.hooks) &&
306
+ entry.type === 'command' &&
307
+ typeof entry.command === 'string' &&
308
+ entry.command.includes('context-monitor');
309
+ return !isFlatBrokenMonitor;
310
+ });
311
+ const removed = before - settings.hooks.PostToolUse.length;
312
+ if (removed > 0) {
313
+ const plural = removed === 1 ? 'y' : 'ies';
314
+ console.log(` ${green}✓${reset} Repaired settings.json (removed ${removed} broken hook entr${plural} from prior install)`);
315
+ }
316
+ }
317
+
292
318
  // Claude Code expects each PostToolUse entry to be a matcher group:
293
319
  // { matcher?: string, hooks: [{ type, command }, ...] }
294
320
  // Detect an existing context-monitor hook inside any matcher group.
package/bin/lib/todo.cjs CHANGED
@@ -231,8 +231,102 @@ function cmdTodoNextId(cwd, raw) {
231
231
  }, raw, nextId);
232
232
  }
233
233
 
234
+ /**
235
+ * Reindex: scan TODO-*.md files, regenerate INDEX.md.
236
+ *
237
+ * @param {string} cwd - Working directory
238
+ * @param {boolean} raw - Output raw string
239
+ */
240
+ function cmdTodoReindex(cwd, raw) {
241
+ const todosDir = path.join(cwd, '.specflow', 'todos');
242
+
243
+ // Collect per-file TODOs
244
+ let perFiles;
245
+ try {
246
+ perFiles = fs.readdirSync(todosDir).filter(f => /^TODO-\d+\.md$/.test(f)).sort();
247
+ } catch (e) {
248
+ perFiles = [];
249
+ }
250
+
251
+ const todos = [];
252
+
253
+ for (const file of perFiles) {
254
+ const content = safeReadFile(path.join(todosDir, file));
255
+ if (!content) continue;
256
+
257
+ const parsed = parseFrontmatter(content);
258
+ const fm = parsed.frontmatter;
259
+
260
+ // Strip surrounding quotes from title (YAML may preserve them)
261
+ let title = fm.title || '';
262
+ if ((title.startsWith('"') && title.endsWith('"')) || (title.startsWith("'") && title.endsWith("'"))) {
263
+ title = title.slice(1, -1);
264
+ }
265
+
266
+ todos.push({
267
+ id: fm.id || file.replace('.md', ''),
268
+ title,
269
+ priority: fm.priority || '—',
270
+ status: fm.status || 'open',
271
+ created: fm.created || '',
272
+ });
273
+ }
274
+
275
+ // Sort by priority (high > medium > low > unset), then by created date
276
+ todos.sort((a, b) => {
277
+ const pa = priorityKey(a.priority);
278
+ const pb = priorityKey(b.priority);
279
+ if (pa !== pb) return pa - pb;
280
+ if (a.created < b.created) return -1;
281
+ if (a.created > b.created) return 1;
282
+ return 0;
283
+ });
284
+
285
+ // Count by priority
286
+ const counts = { high: 0, medium: 0, low: 0, unset: 0 };
287
+ for (const t of todos) {
288
+ if (t.priority === 'high') counts.high++;
289
+ else if (t.priority === 'medium') counts.medium++;
290
+ else if (t.priority === 'low') counts.low++;
291
+ else counts.unset++;
292
+ }
293
+
294
+ // Build INDEX.md
295
+ const lines = [
296
+ '# To-Do Index',
297
+ '',
298
+ '> Auto-generated from individual TODO files. Do not edit manually.',
299
+ '> Regenerate with `/sf:todos`.',
300
+ '',
301
+ '| # | ID | Title | Priority | Status | Created |',
302
+ '|---|-----|-------|----------|--------|---------|',
303
+ ];
304
+
305
+ for (let i = 0; i < todos.length; i++) {
306
+ const t = todos[i];
307
+ let title = t.title;
308
+ if (title.length > 50) title = title.slice(0, 50) + '...';
309
+ lines.push(`| ${i + 1} | ${t.id} | ${title} | ${t.priority} | ${t.status} | ${t.created} |`);
310
+ }
311
+
312
+ lines.push('');
313
+ lines.push(`**Total:** ${todos.length} items (${counts.high} high, ${counts.medium} medium, ${counts.low} low, ${counts.unset} unset)`);
314
+ lines.push('');
315
+ lines.push('---');
316
+ const now = new Date();
317
+ const timestamp = now.toISOString().replace('T', ' ').slice(0, 16);
318
+ lines.push(`*Last regenerated: ${timestamp}*`);
319
+ lines.push('');
320
+
321
+ const indexPath = path.join(todosDir, 'INDEX.md');
322
+ fs.writeFileSync(indexPath, lines.join('\n'), 'utf8');
323
+
324
+ output({ reindexed: todos.length, path: indexPath }, raw, `Reindexed ${todos.length} TODOs → INDEX.md`);
325
+ }
326
+
234
327
  module.exports = {
235
328
  cmdTodoLoad,
236
329
  cmdTodoList,
237
330
  cmdTodoNextId,
331
+ cmdTodoReindex,
238
332
  };
package/bin/sf-tools.cjs CHANGED
@@ -12,6 +12,7 @@
12
12
  * todo load <id> Parse TODO file, return frontmatter + body
13
13
  * todo list [--all] List all TODOs sorted by priority
14
14
  * todo next-id Next available TODO-XXX number
15
+ * todo reindex Regenerate INDEX.md from TODO files
15
16
  * queue next First actionable spec from queue
16
17
  * state get Current active spec, status, next step
17
18
  * state set-active <id> <status> [next] Update active spec in STATE.md
@@ -25,7 +26,7 @@
25
26
  const { output, error, generateSlug } = require('./lib/core.cjs');
26
27
  const { cmdStateGet, cmdStateSetActive, cmdQueueNext } = require('./lib/state.cjs');
27
28
  const { cmdSpecLoad, cmdSpecList, cmdSpecNextId } = require('./lib/spec.cjs');
28
- const { cmdTodoLoad, cmdTodoList, cmdTodoNextId } = require('./lib/todo.cjs');
29
+ const { cmdTodoLoad, cmdTodoList, cmdTodoNextId, cmdTodoReindex } = require('./lib/todo.cjs');
29
30
  const { cmdResolveModel } = require('./lib/config.cjs');
30
31
  const { cmdVerifyStructure } = require('./lib/verify.cjs');
31
32
 
@@ -59,6 +60,7 @@ const COMMANDS = {
59
60
  },
60
61
  'todo list': () => cmdTodoList(cwd, raw, { showAll: flags.all ?? false }),
61
62
  'todo next-id': () => cmdTodoNextId(cwd, raw),
63
+ 'todo reindex': () => cmdTodoReindex(cwd, raw),
62
64
  'queue next': () => cmdQueueNext(cwd, raw),
63
65
  'state get': () => cmdStateGet(cwd, raw),
64
66
  'state set-active': () => {
@@ -90,6 +92,7 @@ Commands:
90
92
  todo load <id> Parse TODO file, return frontmatter + body
91
93
  todo list [--all] List TODOs sorted by priority (--all includes eliminated)
92
94
  todo next-id Next available TODO-XXX number
95
+ todo reindex Regenerate INDEX.md from TODO files
93
96
  queue next First actionable spec from queue table
94
97
  state get Current active spec, status, next step
95
98
  state set-active <id> <status> [next] Update active spec, status, next step
@@ -283,12 +283,16 @@ Check if the spec frontmatter contains a `source:` field (e.g., `source: TODO-00
283
283
  [ -f .specflow/todos/{source}.md ] && echo "FOUND" || echo "NOT_FOUND"
284
284
  ```
285
285
 
286
- 2. **If FOUND:** Delete the file:
286
+ 2. **If FOUND:** Delete the file and reindex:
287
287
 
288
288
  ```bash
289
289
  rm .specflow/todos/{source}.md
290
290
  ```
291
291
 
292
+ ```bash
293
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
294
+ ```
295
+
292
296
  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.
293
297
 
294
298
  No "Last updated" lines to update in per-file format.
@@ -96,6 +96,12 @@ created: {YYYY-MM-DD}
96
96
 
97
97
  Do NOT create or modify TODO.md. Do NOT update any "Last updated" lines.
98
98
 
99
+ ## Step 6.5: Reindex
100
+
101
+ ```bash
102
+ node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
103
+ ```
104
+
99
105
  ## Step 7: Display Confirmation
100
106
 
101
107
  **IMPORTANT:** Output the following directly as formatted text, NOT wrapped in a markdown code block:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specflow-cc",
3
- "version": "1.18.2",
3
+ "version": "1.19.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"