specflow-cc 1.20.0 → 1.21.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 +29 -0
- package/agents/researcher.md +2 -0
- package/agents/spec-auditor.md +2 -0
- package/agents/spec-creator.md +2 -0
- package/agents/spec-reviser.md +10 -1
- package/bin/lib/archive-summary.cjs +508 -0
- package/bin/lib/todo.cjs +74 -2
- package/bin/sf-tools.cjs +17 -1
- package/commands/sf/done.md +11 -0
- package/commands/sf/migrate-todos.md +7 -17
- package/commands/sf/plan.md +15 -1
- package/commands/sf/priority.md +7 -1
- package/commands/sf/revise.md +6 -1
- package/commands/sf/status.md +25 -0
- package/commands/sf/todos.md +9 -21
- package/commands/sf/triage.md +11 -0
- package/package.json +1 -1
- package/templates/archive-summary.md +25 -0
- package/templates/todo-index.md +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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.21.0] - 2026-05-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **L1 archive summary layer** — every archived spec now has a sibling `.specflow/archive/<SPEC-ID>.summary.md` file (~24 lines: goal, key decisions, key files, tests, completion date, link to full spec). Modelled on TencentDB Agent Memory's atomic-facts tier: agents read the summary first and drill down to the full archived spec only when the summary is insufficient. Measured against the existing 22-spec archive: ~94% token reduction when consulting completed-spec history (435-line average full spec → 24-line average summary; ~5.2k → ~0.3k tokens per spec).
|
|
13
|
+
- **`archive summarize <SPEC-ID>` CLI subcommand** in `bin/sf-tools.cjs` — parses an archived spec's frontmatter and `## Goal Analysis` / `## Completion` / `## Delta` sections and writes the `.summary.md` sibling via atomic temp-rename. Falls back to first paragraph of `## Context` for older specs lacking `## Goal Analysis`.
|
|
14
|
+
- **`archive backfill [--force]` CLI subcommand** — iterates `.specflow/archive/SPEC-*.md` and generates missing summaries. Idempotent by default (existing summaries are skipped, zero-diff on second run); `--force` regenerates everything.
|
|
15
|
+
- **`/sf:done` Step 8.5** — automatically generates the L1 summary for every newly archived spec. Non-fatal: summary failure logs a warning but does not abort archival (the full spec is already on disk and `archive backfill` can regenerate later).
|
|
16
|
+
- **Prefer-summary guidance in four agent prompts** — `sf-spec-auditor`, `sf-researcher`, `sf-spec-creator`, and `sf-spec-reviser` now read `<SPEC-ID>.summary.md` first when consulting completed-spec history. Graceful fallback: if no `.summary.md` exists (transitional state during rollout), the agent silently reads the full spec — no error, no warning.
|
|
17
|
+
- `bin/lib/archive-summary.cjs` — pure-Node parser/renderer/generator module (`parseArchivedSpec`, `renderSummary`, `generateSummary`); zero npm dependencies (only `fs`/`path` from stdlib); atomic temp-rename writes consistent with `bin/lib/core.cjs`.
|
|
18
|
+
- `templates/archive-summary.md` — canonical L1 template defining the summary structure; reviewed and stable.
|
|
19
|
+
- `scripts/measure-archive-tokens.cjs` — re-runnable measurement script that scans `.specflow/archive/`, computes average line counts and approximate tokens (lines × ~12 tokens/line), and prints a markdown-formatted ratio report so future contributors can detect regression in the L1 layer's compactness.
|
|
20
|
+
- 11 new tests in `test/archive-summary.test.cjs` covering parser correctness (extracts goal/decisions/keyFiles from fixture specs), older-style spec fallback (no `## Goal Analysis` → goal derived from `## Context`), renderer truncation caps (top 5 decisions, top 6 key files), generator atomic-write behaviour, backfill idempotency, `--force` regeneration, and `archive summarize` error paths.
|
|
21
|
+
|
|
22
|
+
## [1.20.1] - 2026-05-14
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **INDEX.md staleness across all TODO-mutating paths** — `1.19.0` wired the `todo reindex` helper into `/sf:todo` and `/sf:done`, but every other command that mutates `.specflow/todos/` (`/sf:plan` `rm`, `/sf:triage` create, `/sf:revise` deferred-TODO creation, `/sf:priority` priority edits, `/sf:migrate-todos`, and the `sf-spec-reviser` agent) still left INDEX.md silently out of sync. All of these now invoke `node bin/sf-tools.cjs todo reindex` after the mutation. `/sf:todos` no longer writes INDEX.md inline — it delegates to the same helper, making the reindex routine the single source of truth for INDEX layout.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`todo check-stale` CLI subcommand** in `bin/sf-tools.cjs` — compares the set of `TODO-*.md` files on disk to the set of IDs in `INDEX.md` and returns `{stale, index_exists, todo_count, index_count, missing_from_index, extra_in_index}`. Used by `/sf:status` as a safety-net freshness check: if any drift is detected (external edits, manual `rm`, a missed helper call), `/sf:status` surfaces an "INDEX.md stale" warning naming the specific divergences. No auto-fix — the user re-runs `/sf:todos` or the helper.
|
|
31
|
+
- 9 new tests in `tests/todo-index.test.cjs` covering reindex idempotency, header content, drop-after-delete, and all `check-stale` scenarios (fresh, extra-in-index, missing-from-index, no-INDEX-with-files, empty-both, raw output).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **INDEX.md header text** rewritten to describe actual behaviour. Old wording ("Auto-generated from individual TODO files. Do not edit manually. Regenerate with `/sf:todos`.") implied a self-maintaining file. New wording: "Cache of individual TODO files. Refreshed when `/sf:todos` runs OR when an INDEX-mutating command explicitly invokes the regen helper (`node bin/sf-tools.cjs todo reindex`). Do not edit manually — changes will be overwritten on the next regen." Applied in both `bin/lib/todo.cjs` (the source of truth) and `templates/todo-index.md`.
|
|
36
|
+
|
|
8
37
|
## [1.20.0] - 2026-05-02
|
|
9
38
|
|
|
10
39
|
### Added
|
package/agents/researcher.md
CHANGED
|
@@ -66,6 +66,8 @@ Use Glob and Grep to find:
|
|
|
66
66
|
- Similar implementations
|
|
67
67
|
- Configuration patterns
|
|
68
68
|
|
|
69
|
+
**Reading archived specs:** When the research touches completed specs in `.specflow/archive/`, prefer `<SPEC-ID>.summary.md` over `<SPEC-ID>.md`. The summary is 10–15 lines and surfaces the goal, key decisions, and touched files — sufficient for most research queries. Read the full spec only if the summary lacks the specific detail you need. If `.summary.md` does not exist (transitional state during rollout), fall back gracefully to the full spec.
|
|
70
|
+
|
|
69
71
|
## Step 4: External Research (if needed)
|
|
70
72
|
|
|
71
73
|
For topics requiring external knowledge:
|
package/agents/spec-auditor.md
CHANGED
|
@@ -136,6 +136,8 @@ Read `.specflow/PROJECT.md` for:
|
|
|
136
136
|
- Patterns (to check alignment)
|
|
137
137
|
- Constraints (to verify compliance)
|
|
138
138
|
|
|
139
|
+
**Reading archived specs:** When you need to consult completed specs (e.g., to check pattern compliance or prior decisions), read `.specflow/archive/<SPEC-ID>.summary.md` first. The summary is 10–15 lines and surfaces the goal, key decisions, and touched files. Open the full `<SPEC-ID>.md` only when the summary does not contain the detail you need. If `.summary.md` does not exist (transitional state during rollout), fall back gracefully to the full spec.
|
|
140
|
+
|
|
139
141
|
## Step 3: Audit Dimensions
|
|
140
142
|
|
|
141
143
|
Evaluate each dimension:
|
package/agents/spec-creator.md
CHANGED
|
@@ -79,6 +79,8 @@ Read the discussion file (PRE-XXX.md or DISC-XXX.md) to understand:
|
|
|
79
79
|
- Questions already answered
|
|
80
80
|
- User preferences and constraints
|
|
81
81
|
|
|
82
|
+
**Reading archived specs:** When making assumptions informed by prior decisions, read `.specflow/archive/<SPEC-ID>.summary.md` rather than the full archived spec. The summary is 10–15 lines and surfaces the goal, key decisions, and touched files. Open the full `<SPEC-ID>.md` only when the summary does not contain the specific detail you need. If `.summary.md` does not exist (transitional state during rollout), fall back gracefully to the full spec.
|
|
83
|
+
|
|
82
84
|
## Step 2: Analyze Task
|
|
83
85
|
|
|
84
86
|
Parse the user's task description:
|
package/agents/spec-reviser.md
CHANGED
|
@@ -58,6 +58,8 @@ Read `.specflow/STATE.md` to get:
|
|
|
58
58
|
|
|
59
59
|
Read the full specification file.
|
|
60
60
|
|
|
61
|
+
**Reading archived specs:** When making assumptions informed by prior decisions, read `.specflow/archive/<SPEC-ID>.summary.md` rather than the full archived spec. The summary is 10–15 lines and surfaces the goal, key decisions, and touched files. Open the full `<SPEC-ID>.md` only when the summary does not contain the specific detail you need. If `.summary.md` does not exist (transitional state during rollout), fall back gracefully to the full spec.
|
|
62
|
+
|
|
61
63
|
## Step 2: Parse Latest Audit
|
|
62
64
|
|
|
63
65
|
Find the most recent "Audit v[N]" section in Audit History.
|
|
@@ -176,7 +178,13 @@ For each deferred item:
|
|
|
176
178
|
- TODO-{XXX} — {item description}
|
|
177
179
|
```
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
4. After the loop completes (at least one TODO created), refresh the INDEX.md cache so it reflects the newly-created files:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
node bin/sf-tools.cjs todo reindex
|
|
185
|
+
```
|
|
186
|
+
|
|
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.
|
|
180
188
|
|
|
181
189
|
## Step 6: Update Frontmatter
|
|
182
190
|
|
|
@@ -245,6 +253,7 @@ Tip: `/clear` recommended — auditor needs fresh context
|
|
|
245
253
|
- [ ] Revision Response recorded in Audit History
|
|
246
254
|
- [ ] Deferred items (if any) created as individual `.specflow/todos/TODO-XXX.md` files
|
|
247
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)
|
|
248
257
|
- [ ] Frontmatter status updated
|
|
249
258
|
- [ ] STATE.md updated
|
|
250
259
|
- [ ] Clear summary of changes provided
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/archive-summary.cjs — L1 archive summary generator
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* parseArchivedSpec(specPath) → structured summary object
|
|
6
|
+
* renderSummary(parsed, templatePath) → markdown string
|
|
7
|
+
* generateSummary(specPath, templatePath, outputPath) → { written, reason? }
|
|
8
|
+
*
|
|
9
|
+
* No npm dependencies — only fs, path from Node standard library.
|
|
10
|
+
* Caller is responsible for providing correct paths.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Internal helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse YAML-style frontmatter from a markdown file.
|
|
25
|
+
* Matches the simple key: value parsing in bin/lib/core.cjs.
|
|
26
|
+
* @param {string} content
|
|
27
|
+
* @returns {{ frontmatter: Object, body: string }}
|
|
28
|
+
*/
|
|
29
|
+
function _parseFrontmatter(content) {
|
|
30
|
+
if (!content || typeof content !== 'string') {
|
|
31
|
+
return { frontmatter: {}, body: content || '' };
|
|
32
|
+
}
|
|
33
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
34
|
+
if (!fmMatch) {
|
|
35
|
+
return { frontmatter: {}, body: content };
|
|
36
|
+
}
|
|
37
|
+
const fm = {};
|
|
38
|
+
fmMatch[1].split('\n').forEach(line => {
|
|
39
|
+
const kv = line.match(/^([^:]+):\s*(.*)$/);
|
|
40
|
+
if (kv) {
|
|
41
|
+
fm[kv[1].trim()] = kv[2].trim();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return { frontmatter: fm, body: fmMatch[2] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract a named section's content (between the heading and the next same/higher-level heading).
|
|
49
|
+
* @param {string} body - Full body text
|
|
50
|
+
* @param {string} headingText - Exact heading text (without # prefix)
|
|
51
|
+
* @param {number} level - Heading level (2 = ##, 3 = ###)
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
function _extractSection(body, headingText, level) {
|
|
55
|
+
const hashes = '#'.repeat(level);
|
|
56
|
+
// Match the heading line; content follows until the next heading of same or higher level
|
|
57
|
+
const headingRe = new RegExp(
|
|
58
|
+
`^${hashes}\\s+${headingText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`,
|
|
59
|
+
'm'
|
|
60
|
+
);
|
|
61
|
+
const match = body.match(headingRe);
|
|
62
|
+
if (!match) return null;
|
|
63
|
+
const start = match.index + match[0].length;
|
|
64
|
+
// Next heading at same level or higher (fewer or equal hashes)
|
|
65
|
+
const stopRe = new RegExp(`^#{1,${level}}\\s`, 'm');
|
|
66
|
+
const rest = body.slice(start);
|
|
67
|
+
const stopMatch = rest.match(stopRe);
|
|
68
|
+
return stopMatch ? rest.slice(0, stopMatch.index).trim() : rest.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract bullet list items from a markdown section string.
|
|
73
|
+
* Captures lines that start with `- ` (with optional leading spaces).
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @returns {string[]}
|
|
76
|
+
*/
|
|
77
|
+
function _extractBullets(text) {
|
|
78
|
+
if (!text) return [];
|
|
79
|
+
return text
|
|
80
|
+
.split('\n')
|
|
81
|
+
.filter(l => /^\s*-\s+/.test(l))
|
|
82
|
+
.map(l => l.replace(/^\s*-\s+/, '').trim())
|
|
83
|
+
.filter(l => l.length > 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the first non-empty, non-heading paragraph from a text block.
|
|
88
|
+
* @param {string} text
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function _firstParagraph(text) {
|
|
92
|
+
if (!text) return '';
|
|
93
|
+
const lines = text.split('\n');
|
|
94
|
+
const paragraphLines = [];
|
|
95
|
+
let inParagraph = false;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (trimmed.startsWith('#')) continue; // skip headings
|
|
99
|
+
if (trimmed === '') {
|
|
100
|
+
if (inParagraph) break; // end of first paragraph
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
inParagraph = true;
|
|
104
|
+
paragraphLines.push(trimmed);
|
|
105
|
+
}
|
|
106
|
+
return paragraphLines.join(' ').trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract the title from the first # heading in the body.
|
|
111
|
+
* @param {string} body
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function _extractTitle(body) {
|
|
115
|
+
const m = body.match(/^#\s+(.+)$/m);
|
|
116
|
+
return m ? m[1].trim() : '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract test file references from text.
|
|
121
|
+
* Matches patterns like test/foo.test.cjs
|
|
122
|
+
* @param {string} text
|
|
123
|
+
* @returns {string[]}
|
|
124
|
+
*/
|
|
125
|
+
function _extractTestRefs(text) {
|
|
126
|
+
if (!text) return [];
|
|
127
|
+
const matches = text.match(/test\/[^\s,)]+\.test\.cjs/g);
|
|
128
|
+
if (!matches) return [];
|
|
129
|
+
return [...new Set(matches)];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract the completion date from a Completion section.
|
|
134
|
+
* Looks for lines like: **Completed:** YYYY-MM-DD
|
|
135
|
+
* @param {string} completionSection
|
|
136
|
+
* @returns {string}
|
|
137
|
+
*/
|
|
138
|
+
function _extractCompletedDate(completionSection) {
|
|
139
|
+
if (!completionSection) return '';
|
|
140
|
+
const m = completionSection.match(/\*\*Completed:\*\*\s*(\d{4}-\d{2}-\d{2})/);
|
|
141
|
+
return m ? m[1] : '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Public API
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse an archived spec file and return a structured summary object.
|
|
150
|
+
*
|
|
151
|
+
* Field extraction strategy:
|
|
152
|
+
* - goal: from ## Goal Analysis > ### Goal Statement; fallback to first paragraph of ## Context
|
|
153
|
+
* - decisions: from ## Completion > ### Patterns Established bullets;
|
|
154
|
+
* fallback to ## Delta ADDED/MODIFIED bullets (top 5)
|
|
155
|
+
* - keyFiles: from ## Completion > ### Key Files bullets; fallback to ## Delta ADDED bullets (top 6)
|
|
156
|
+
* - tests: test/*.test.cjs refs found in ### Key Files or elsewhere in ## Completion
|
|
157
|
+
* - completed: from ## Completion > **Completed:** date line
|
|
158
|
+
* - title: from first # heading
|
|
159
|
+
*
|
|
160
|
+
* @param {string} specPath - Absolute path to the archived spec file
|
|
161
|
+
* @returns {{
|
|
162
|
+
* specId: string,
|
|
163
|
+
* title: string,
|
|
164
|
+
* type: string,
|
|
165
|
+
* completed: string,
|
|
166
|
+
* goal: string,
|
|
167
|
+
* decisions: string[],
|
|
168
|
+
* keyFiles: Array<{path: string, purpose: string}>,
|
|
169
|
+
* tests: string[]
|
|
170
|
+
* }}
|
|
171
|
+
*/
|
|
172
|
+
function parseArchivedSpec(specPath) {
|
|
173
|
+
const content = fs.readFileSync(specPath, 'utf8');
|
|
174
|
+
const { frontmatter, body } = _parseFrontmatter(content);
|
|
175
|
+
|
|
176
|
+
const specId = frontmatter.id || path.basename(specPath, '.md');
|
|
177
|
+
const type = frontmatter.type || 'feature';
|
|
178
|
+
|
|
179
|
+
// Title
|
|
180
|
+
const title = _extractTitle(body);
|
|
181
|
+
|
|
182
|
+
// --- Goal ---
|
|
183
|
+
let goal = '';
|
|
184
|
+
const goalAnalysisSection = _extractSection(body, 'Goal Analysis', 2);
|
|
185
|
+
if (goalAnalysisSection) {
|
|
186
|
+
const goalStatementSection = _extractSection(goalAnalysisSection, 'Goal Statement', 3);
|
|
187
|
+
if (goalStatementSection) {
|
|
188
|
+
goal = _firstParagraph(goalStatementSection);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!goal) {
|
|
192
|
+
// Fallback: first paragraph of ## Context
|
|
193
|
+
const contextSection = _extractSection(body, 'Context', 2);
|
|
194
|
+
goal = _firstParagraph(contextSection || body);
|
|
195
|
+
}
|
|
196
|
+
// Trim goal to a single sentence if possible (stop at first period followed by space or end)
|
|
197
|
+
const sentenceEnd = goal.match(/^([^.!?]+[.!?])/);
|
|
198
|
+
if (sentenceEnd && sentenceEnd[1].length >= 20) {
|
|
199
|
+
goal = sentenceEnd[1].trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Key Decisions ---
|
|
203
|
+
let decisions = [];
|
|
204
|
+
const completionSection = _extractSection(body, 'Completion', 2);
|
|
205
|
+
if (completionSection) {
|
|
206
|
+
const patternsSection = _extractSection(completionSection, 'Patterns Established', 3);
|
|
207
|
+
if (patternsSection) {
|
|
208
|
+
decisions = _extractBullets(patternsSection);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (decisions.length === 0) {
|
|
212
|
+
// Fallback: ADDED/MODIFIED bullets from ## Delta
|
|
213
|
+
const deltaSection = _extractSection(body, 'Delta', 2);
|
|
214
|
+
if (deltaSection) {
|
|
215
|
+
const addedSection = _extractSection(deltaSection, 'ADDED', 3);
|
|
216
|
+
const modifiedSection = _extractSection(deltaSection, 'MODIFIED', 3);
|
|
217
|
+
const addedBullets = _extractBullets(addedSection || '');
|
|
218
|
+
const modifiedBullets = _extractBullets(modifiedSection || '');
|
|
219
|
+
decisions = [...addedBullets, ...modifiedBullets];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Cap at 5
|
|
223
|
+
decisions = decisions.slice(0, 5);
|
|
224
|
+
|
|
225
|
+
// --- Key Files ---
|
|
226
|
+
let keyFiles = [];
|
|
227
|
+
if (completionSection) {
|
|
228
|
+
const keyFilesSection = _extractSection(completionSection, 'Key Files', 3);
|
|
229
|
+
if (keyFilesSection) {
|
|
230
|
+
const bullets = _extractBullets(keyFilesSection);
|
|
231
|
+
keyFiles = bullets.map(b => {
|
|
232
|
+
// Format: `path/to/file.cjs` — purpose OR `path/to/file.cjs` purpose
|
|
233
|
+
const dashSplit = b.match(/^`?([^\s`]+)`?\s+[—–-]+\s+(.+)$/);
|
|
234
|
+
if (dashSplit) {
|
|
235
|
+
return { path: dashSplit[1], purpose: dashSplit[2].trim() };
|
|
236
|
+
}
|
|
237
|
+
// Fallback: first token is path, rest is purpose
|
|
238
|
+
const spaceIdx = b.indexOf(' ');
|
|
239
|
+
if (spaceIdx > 0) {
|
|
240
|
+
return { path: b.slice(0, spaceIdx).replace(/`/g, ''), purpose: b.slice(spaceIdx + 1).trim() };
|
|
241
|
+
}
|
|
242
|
+
return { path: b.replace(/`/g, ''), purpose: '' };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (keyFiles.length === 0) {
|
|
247
|
+
// Fallback: ADDED bullets from Delta
|
|
248
|
+
const deltaSection = _extractSection(body, 'Delta', 2);
|
|
249
|
+
if (deltaSection) {
|
|
250
|
+
const addedSection = _extractSection(deltaSection, 'ADDED', 3);
|
|
251
|
+
const bullets = _extractBullets(addedSection || '');
|
|
252
|
+
keyFiles = bullets.map(b => {
|
|
253
|
+
const dashSplit = b.match(/^`?([^\s`]+)`?\s+[—–-]+\s+(.+)$/);
|
|
254
|
+
if (dashSplit) {
|
|
255
|
+
return { path: dashSplit[1], purpose: dashSplit[2].trim() };
|
|
256
|
+
}
|
|
257
|
+
const spaceIdx = b.indexOf(' ');
|
|
258
|
+
if (spaceIdx > 0) {
|
|
259
|
+
return { path: b.slice(0, spaceIdx).replace(/`/g, ''), purpose: b.slice(spaceIdx + 1).trim() };
|
|
260
|
+
}
|
|
261
|
+
return { path: b.replace(/`/g, ''), purpose: '' };
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Cap at 6
|
|
266
|
+
keyFiles = keyFiles.slice(0, 6);
|
|
267
|
+
|
|
268
|
+
// --- Tests ---
|
|
269
|
+
let tests = [];
|
|
270
|
+
if (completionSection) {
|
|
271
|
+
tests = _extractTestRefs(completionSection);
|
|
272
|
+
}
|
|
273
|
+
if (tests.length === 0) {
|
|
274
|
+
tests = _extractTestRefs(body);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- Completed date ---
|
|
278
|
+
const completed = _extractCompletedDate(completionSection || '') ||
|
|
279
|
+
frontmatter.completed || '';
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
specId,
|
|
283
|
+
title,
|
|
284
|
+
type,
|
|
285
|
+
completed,
|
|
286
|
+
goal,
|
|
287
|
+
decisions,
|
|
288
|
+
keyFiles,
|
|
289
|
+
tests,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Render a summary markdown string from a parsed spec object and a template file.
|
|
295
|
+
* Substitutes placeholders in the template with actual values.
|
|
296
|
+
* Truncates decisions to top 5 and keyFiles to top 6 (already enforced by parser,
|
|
297
|
+
* but re-enforced here for callers who bypass parseArchivedSpec).
|
|
298
|
+
*
|
|
299
|
+
* @param {{ specId, title, type, completed, goal, decisions, keyFiles, tests }} parsed
|
|
300
|
+
* @param {string} templatePath - Absolute path to templates/archive-summary.md
|
|
301
|
+
* @returns {string} Rendered markdown string
|
|
302
|
+
*/
|
|
303
|
+
function renderSummary(parsed, templatePath) {
|
|
304
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
305
|
+
|
|
306
|
+
const {
|
|
307
|
+
specId,
|
|
308
|
+
title,
|
|
309
|
+
type,
|
|
310
|
+
completed,
|
|
311
|
+
goal,
|
|
312
|
+
decisions,
|
|
313
|
+
keyFiles,
|
|
314
|
+
tests,
|
|
315
|
+
} = parsed;
|
|
316
|
+
|
|
317
|
+
// Cap arrays
|
|
318
|
+
const cappedDecisions = (decisions || []).slice(0, 5);
|
|
319
|
+
const cappedKeyFiles = (keyFiles || []).slice(0, 6);
|
|
320
|
+
|
|
321
|
+
// Build decisions bullet list
|
|
322
|
+
const decisionsBlock = cappedDecisions.length > 0
|
|
323
|
+
? cappedDecisions.map(d => `- ${d}`).join('\n')
|
|
324
|
+
: '- (none recorded)';
|
|
325
|
+
|
|
326
|
+
// Build key files bullet list
|
|
327
|
+
const keyFilesBlock = cappedKeyFiles.length > 0
|
|
328
|
+
? cappedKeyFiles.map(kf => {
|
|
329
|
+
const p = typeof kf === 'string' ? kf : kf.path;
|
|
330
|
+
const pu = typeof kf === 'string' ? '' : kf.purpose;
|
|
331
|
+
return pu ? `- ${p} — ${pu}` : `- ${p}`;
|
|
332
|
+
}).join('\n')
|
|
333
|
+
: '- (none recorded)';
|
|
334
|
+
|
|
335
|
+
// Tests field
|
|
336
|
+
const testsField = (tests && tests.length > 0) ? tests.join(', ') : 'none';
|
|
337
|
+
|
|
338
|
+
// Substitute all placeholders
|
|
339
|
+
let output = template
|
|
340
|
+
// Frontmatter fields (replace globally after first-pass)
|
|
341
|
+
.replace(/\{SPEC-ID\}/g, specId)
|
|
342
|
+
.replace('{full title}', title || specId)
|
|
343
|
+
.replace('{feature|refactor|bugfix}', type || 'feature')
|
|
344
|
+
.replace('{YYYY-MM-DD}', completed || 'unknown')
|
|
345
|
+
// Goal
|
|
346
|
+
.replace('{one-sentence goal extracted from Goal Statement or Context}', goal || '(not extracted)')
|
|
347
|
+
// Decisions block — replace the 3-line placeholder block
|
|
348
|
+
.replace(
|
|
349
|
+
/^- \{decision 1\}\n- \{decision 2\}\n- \{decision 3\}$/m,
|
|
350
|
+
decisionsBlock
|
|
351
|
+
)
|
|
352
|
+
// Key files block — replace the 2-line placeholder block
|
|
353
|
+
.replace(
|
|
354
|
+
/^- \{path 1\} — \{one-line purpose\}\n- \{path 2\} — \{one-line purpose\}$/m,
|
|
355
|
+
keyFilesBlock
|
|
356
|
+
)
|
|
357
|
+
// Tests
|
|
358
|
+
.replace('{test/foo.test.cjs, ...} or "none"', testsField)
|
|
359
|
+
// Related future specs
|
|
360
|
+
.replace('{list of SPEC-IDs that reference this, or "none yet"}', 'none yet');
|
|
361
|
+
|
|
362
|
+
return output;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Orchestrate parse → render → write for a single spec.
|
|
367
|
+
* Uses atomic temp-rename pattern consistent with bin/lib/core.cjs.
|
|
368
|
+
*
|
|
369
|
+
* @param {string} specPath - Path to source archived spec (.md)
|
|
370
|
+
* @param {string} templatePath - Path to templates/archive-summary.md
|
|
371
|
+
* @param {string} outputPath - Destination path for the summary (.summary.md)
|
|
372
|
+
* @returns {{ written: boolean, reason?: string }}
|
|
373
|
+
*/
|
|
374
|
+
function generateSummary(specPath, templatePath, outputPath) {
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = parseArchivedSpec(specPath);
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return { written: false, reason: 'parse failed: ' + e.message };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let rendered;
|
|
383
|
+
try {
|
|
384
|
+
rendered = renderSummary(parsed, templatePath);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
return { written: false, reason: 'render failed: ' + e.message };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Atomic temp-rename write
|
|
390
|
+
const tmpPath = outputPath + '.tmp.' + process.pid;
|
|
391
|
+
try {
|
|
392
|
+
fs.writeFileSync(tmpPath, rendered, 'utf8');
|
|
393
|
+
fs.renameSync(tmpPath, outputPath);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
// Clean up temp file if it exists
|
|
396
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
397
|
+
return { written: false, reason: 'write failed: ' + e.message };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { written: true };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// CLI command implementations
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* CLI handler for `archive summarize <SPEC-ID>`.
|
|
409
|
+
* Generates (or regenerates with --force) a .summary.md for one archived spec.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} cwd - Working directory
|
|
412
|
+
* @param {string} specId - Spec ID (e.g. "SPEC-011")
|
|
413
|
+
* @param {{ force?: boolean }} [opts]
|
|
414
|
+
*/
|
|
415
|
+
function cmdArchiveSummarize(cwd, specId, opts) {
|
|
416
|
+
const { force = false } = opts || {};
|
|
417
|
+
const archiveDir = path.join(cwd, '.specflow', 'archive');
|
|
418
|
+
const specPath = path.join(archiveDir, specId + '.md');
|
|
419
|
+
const outputPath = path.join(archiveDir, specId + '.summary.md');
|
|
420
|
+
const templatePath = path.join(cwd, 'templates', 'archive-summary.md');
|
|
421
|
+
|
|
422
|
+
// Validate spec exists in archive
|
|
423
|
+
if (!fs.existsSync(specPath)) {
|
|
424
|
+
process.stderr.write('Error: Spec not found in archive: ' + specPath + '\n');
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Skip if summary already exists and not --force
|
|
429
|
+
if (!force && fs.existsSync(outputPath)) {
|
|
430
|
+
process.stdout.write(JSON.stringify({ written: false, reason: 'already exists (use --force to overwrite)', path: outputPath }, null, 2) + '\n');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const result = generateSummary(specPath, templatePath, outputPath);
|
|
435
|
+
if (!result.written) {
|
|
436
|
+
process.stderr.write('Error: Failed to generate summary: ' + result.reason + '\n');
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
process.stdout.write(JSON.stringify({ written: true, path: outputPath }, null, 2) + '\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* CLI handler for `archive backfill [--force]`.
|
|
444
|
+
* Generates summary files for all archived specs that lack them.
|
|
445
|
+
* With --force, regenerates all summaries.
|
|
446
|
+
*
|
|
447
|
+
* @param {string} cwd - Working directory
|
|
448
|
+
* @param {{ force?: boolean }} [opts]
|
|
449
|
+
*/
|
|
450
|
+
function cmdArchiveBackfill(cwd, opts) {
|
|
451
|
+
const { force = false } = opts || {};
|
|
452
|
+
const archiveDir = path.join(cwd, '.specflow', 'archive');
|
|
453
|
+
const templatePath = path.join(cwd, 'templates', 'archive-summary.md');
|
|
454
|
+
|
|
455
|
+
let files;
|
|
456
|
+
try {
|
|
457
|
+
files = fs.readdirSync(archiveDir);
|
|
458
|
+
} catch (e) {
|
|
459
|
+
process.stderr.write('Error: Cannot read archive directory: ' + archiveDir + '\n');
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Identify spec files (SPEC-*.md, not *.summary.md)
|
|
464
|
+
const specFiles = files
|
|
465
|
+
.filter(f => f.match(/^SPEC-[A-Z0-9-]+\.md$/) && !f.endsWith('.summary.md'))
|
|
466
|
+
.sort();
|
|
467
|
+
|
|
468
|
+
const results = [];
|
|
469
|
+
let written = 0;
|
|
470
|
+
let skipped = 0;
|
|
471
|
+
let failed = 0;
|
|
472
|
+
|
|
473
|
+
for (const file of specFiles) {
|
|
474
|
+
const specId = file.replace(/\.md$/, '');
|
|
475
|
+
const specPath = path.join(archiveDir, file);
|
|
476
|
+
const outputPath = path.join(archiveDir, specId + '.summary.md');
|
|
477
|
+
|
|
478
|
+
// Skip if summary exists and not --force
|
|
479
|
+
if (!force && fs.existsSync(outputPath)) {
|
|
480
|
+
skipped++;
|
|
481
|
+
results.push({ specId, written: false, reason: 'already exists' });
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const result = generateSummary(specPath, templatePath, outputPath);
|
|
486
|
+
if (result.written) {
|
|
487
|
+
written++;
|
|
488
|
+
results.push({ specId, written: true, path: outputPath });
|
|
489
|
+
} else {
|
|
490
|
+
failed++;
|
|
491
|
+
results.push({ specId, written: false, reason: result.reason });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
process.stdout.write(JSON.stringify({
|
|
496
|
+
total: specFiles.length,
|
|
497
|
+
written,
|
|
498
|
+
skipped,
|
|
499
|
+
failed,
|
|
500
|
+
results,
|
|
501
|
+
}, null, 2) + '\n');
|
|
502
|
+
|
|
503
|
+
if (failed > 0) {
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
module.exports = { parseArchivedSpec, renderSummary, generateSummary, cmdArchiveSummarize, cmdArchiveBackfill };
|
package/bin/lib/todo.cjs
CHANGED
|
@@ -295,8 +295,10 @@ function cmdTodoReindex(cwd, raw) {
|
|
|
295
295
|
const lines = [
|
|
296
296
|
'# To-Do Index',
|
|
297
297
|
'',
|
|
298
|
-
'>
|
|
299
|
-
'>
|
|
298
|
+
'> Cache of individual TODO files. Refreshed when `/sf:todos` runs OR when an',
|
|
299
|
+
'> INDEX-mutating command explicitly invokes the regen helper',
|
|
300
|
+
'> (`node bin/sf-tools.cjs todo reindex`). Do not edit manually — changes will',
|
|
301
|
+
'> be overwritten on the next regen.',
|
|
300
302
|
'',
|
|
301
303
|
'| # | ID | Title | Priority | Status | Created |',
|
|
302
304
|
'|---|-----|-------|----------|--------|---------|',
|
|
@@ -324,9 +326,79 @@ function cmdTodoReindex(cwd, raw) {
|
|
|
324
326
|
output({ reindexed: todos.length, path: indexPath }, raw, `Reindexed ${todos.length} TODOs → INDEX.md`);
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Check whether INDEX.md is stale relative to TODO-*.md files.
|
|
331
|
+
*
|
|
332
|
+
* Stale = the set of TODO-XXX IDs on disk diverges from the set of TODO-XXX IDs
|
|
333
|
+
* listed in INDEX.md (file deleted but still in INDEX, or file present but missing).
|
|
334
|
+
*
|
|
335
|
+
* NOTE: Eliminated TODOs (`status: eliminated`) still appear in `/sf:todos --all`
|
|
336
|
+
* regenerated INDEX.md output, so they are NOT filtered here — both sides see them.
|
|
337
|
+
*
|
|
338
|
+
* Output JSON: { stale, missing_from_index, extra_in_index, index_exists }
|
|
339
|
+
* - missing_from_index: file exists on disk but not in INDEX.md
|
|
340
|
+
* - extra_in_index: ID listed in INDEX.md but no file on disk
|
|
341
|
+
*
|
|
342
|
+
* @param {string} cwd - Working directory
|
|
343
|
+
* @param {boolean} raw - Output mode
|
|
344
|
+
*/
|
|
345
|
+
function cmdTodoCheckStale(cwd, raw) {
|
|
346
|
+
const todosDir = path.join(cwd, '.specflow', 'todos');
|
|
347
|
+
const indexPath = path.join(todosDir, 'INDEX.md');
|
|
348
|
+
|
|
349
|
+
// Collect IDs from disk
|
|
350
|
+
const diskIds = new Set();
|
|
351
|
+
try {
|
|
352
|
+
for (const f of fs.readdirSync(todosDir)) {
|
|
353
|
+
const m = f.match(/^(TODO-\d+)\.md$/);
|
|
354
|
+
if (m) diskIds.add(m[1]);
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
// todos dir missing — treat as empty
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Collect IDs referenced in INDEX.md (parse only the table rows)
|
|
361
|
+
const indexIds = new Set();
|
|
362
|
+
const indexContent = safeReadFile(indexPath);
|
|
363
|
+
const indexExists = indexContent !== null;
|
|
364
|
+
|
|
365
|
+
if (indexContent) {
|
|
366
|
+
// Match TODO-XXX in pipe-table cells: "| N | TODO-001 | ..."
|
|
367
|
+
const regex = /\|\s*\d+\s*\|\s*(TODO-\d+)\s*\|/g;
|
|
368
|
+
let m;
|
|
369
|
+
while ((m = regex.exec(indexContent)) !== null) {
|
|
370
|
+
indexIds.add(m[1]);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const missingFromIndex = [...diskIds].filter(id => !indexIds.has(id)).sort();
|
|
375
|
+
const extraInIndex = [...indexIds].filter(id => !diskIds.has(id)).sort();
|
|
376
|
+
|
|
377
|
+
// If INDEX.md does not exist but there are TODO files, INDEX is stale.
|
|
378
|
+
// If INDEX.md does not exist and no TODO files, not stale (nothing to track).
|
|
379
|
+
const stale =
|
|
380
|
+
missingFromIndex.length > 0 ||
|
|
381
|
+
extraInIndex.length > 0 ||
|
|
382
|
+
(!indexExists && diskIds.size > 0);
|
|
383
|
+
|
|
384
|
+
output(
|
|
385
|
+
{
|
|
386
|
+
stale,
|
|
387
|
+
index_exists: indexExists,
|
|
388
|
+
todo_count: diskIds.size,
|
|
389
|
+
index_count: indexIds.size,
|
|
390
|
+
missing_from_index: missingFromIndex,
|
|
391
|
+
extra_in_index: extraInIndex,
|
|
392
|
+
},
|
|
393
|
+
raw,
|
|
394
|
+
stale ? 'STALE' : 'FRESH'
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
327
398
|
module.exports = {
|
|
328
399
|
cmdTodoLoad,
|
|
329
400
|
cmdTodoList,
|
|
330
401
|
cmdTodoNextId,
|
|
331
402
|
cmdTodoReindex,
|
|
403
|
+
cmdTodoCheckStale,
|
|
332
404
|
};
|
package/bin/sf-tools.cjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* todo list [--all] List all TODOs sorted by priority
|
|
14
14
|
* todo next-id Next available TODO-XXX number
|
|
15
15
|
* todo reindex Regenerate INDEX.md from TODO files
|
|
16
|
+
* todo check-stale Report drift between TODO-*.md and INDEX.md
|
|
16
17
|
* queue next First actionable spec from queue
|
|
17
18
|
* state get Current active spec, status, next step (legacy shim)
|
|
18
19
|
* state set-active <id> <status> [next] Update active spec in STATE.md (legacy shim)
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
* state remove-active <id> Remove one row from Active Specifications table
|
|
22
23
|
* state resolve [id] Resolve active spec; emit JSON contract
|
|
23
24
|
* state migrate One-shot idempotent migration to new schema
|
|
25
|
+
* archive summarize <SPEC-ID> Generate L1 summary for one archived spec
|
|
26
|
+
* archive backfill [--force] Generate missing summaries for all archived specs
|
|
24
27
|
* resolve-model <agent-type> Model for agent by current profile
|
|
25
28
|
* verify-structure Check .specflow/ integrity
|
|
26
29
|
* generate-slug <text> Text to URL-safe slug
|
|
@@ -40,9 +43,10 @@ const {
|
|
|
40
43
|
cmdQueueNext,
|
|
41
44
|
} = require('./lib/state.cjs');
|
|
42
45
|
const { cmdSpecLoad, cmdSpecList, cmdSpecNextId } = require('./lib/spec.cjs');
|
|
43
|
-
const { cmdTodoLoad, cmdTodoList, cmdTodoNextId, cmdTodoReindex } = require('./lib/todo.cjs');
|
|
46
|
+
const { cmdTodoLoad, cmdTodoList, cmdTodoNextId, cmdTodoReindex, cmdTodoCheckStale } = require('./lib/todo.cjs');
|
|
44
47
|
const { cmdResolveModel } = require('./lib/config.cjs');
|
|
45
48
|
const { cmdVerifyStructure } = require('./lib/verify.cjs');
|
|
49
|
+
const { cmdArchiveSummarize, cmdArchiveBackfill } = require('./lib/archive-summary.cjs');
|
|
46
50
|
|
|
47
51
|
const cwd = process.cwd();
|
|
48
52
|
const args = process.argv.slice(2);
|
|
@@ -75,6 +79,7 @@ const COMMANDS = {
|
|
|
75
79
|
'todo list': () => cmdTodoList(cwd, raw, { showAll: flags.all ?? false }),
|
|
76
80
|
'todo next-id': () => cmdTodoNextId(cwd, raw),
|
|
77
81
|
'todo reindex': () => cmdTodoReindex(cwd, raw),
|
|
82
|
+
'todo check-stale': () => cmdTodoCheckStale(cwd, raw),
|
|
78
83
|
'queue next': () => cmdQueueNext(cwd, raw),
|
|
79
84
|
|
|
80
85
|
// Legacy shims (backwards compatible)
|
|
@@ -112,6 +117,14 @@ const COMMANDS = {
|
|
|
112
117
|
.catch(e => error(e.message));
|
|
113
118
|
},
|
|
114
119
|
|
|
120
|
+
'archive summarize': () => {
|
|
121
|
+
if (!filteredArgs[2]) error('Missing SPEC-ID. Usage: archive summarize <SPEC-ID>');
|
|
122
|
+
cmdArchiveSummarize(cwd, filteredArgs[2], { force: flags.force });
|
|
123
|
+
},
|
|
124
|
+
'archive backfill': () => {
|
|
125
|
+
cmdArchiveBackfill(cwd, { force: flags.force });
|
|
126
|
+
},
|
|
127
|
+
|
|
115
128
|
'resolve-model': () => {
|
|
116
129
|
if (!filteredArgs[1]) error('Missing agent type. Usage: resolve-model <agent-type>');
|
|
117
130
|
cmdResolveModel(cwd, filteredArgs[1], raw);
|
|
@@ -136,6 +149,7 @@ Commands:
|
|
|
136
149
|
todo list [--all] List TODOs sorted by priority (--all includes eliminated)
|
|
137
150
|
todo next-id Next available TODO-XXX number
|
|
138
151
|
todo reindex Regenerate INDEX.md from TODO files
|
|
152
|
+
todo check-stale Report drift between TODO-*.md and INDEX.md
|
|
139
153
|
queue next First actionable spec from queue table
|
|
140
154
|
state get Current active spec, status, next step (legacy shim)
|
|
141
155
|
state set-active <id> <status> [next] Update active spec, status, next step (legacy shim)
|
|
@@ -144,6 +158,8 @@ Commands:
|
|
|
144
158
|
state remove-active <id> Remove one row (under advisory lock)
|
|
145
159
|
state resolve [SPEC-ID] Resolve active spec; emit JSON contract
|
|
146
160
|
state migrate One-shot idempotent migration to new schema
|
|
161
|
+
archive summarize <SPEC-ID> Generate L1 summary for one archived spec
|
|
162
|
+
archive backfill [--force] Generate missing summaries for all archived specs
|
|
147
163
|
resolve-model <agent-type> Resolve model for agent by current profile
|
|
148
164
|
verify-structure Check .specflow/ directory integrity
|
|
149
165
|
generate-slug <text> Convert text to URL-safe slug
|
package/commands/sf/done.md
CHANGED
|
@@ -318,6 +318,16 @@ Move spec to archive:
|
|
|
318
318
|
mv .specflow/specs/SPEC-XXX.md .specflow/archive/
|
|
319
319
|
```
|
|
320
320
|
|
|
321
|
+
## Step 8.5: Generate L1 Summary
|
|
322
|
+
|
|
323
|
+
Generate a compact summary of the just-archived spec for agent consumption:
|
|
324
|
+
|
|
325
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs archive summarize SPEC-XXX
|
|
326
|
+
|
|
327
|
+
On success, `.specflow/archive/SPEC-XXX.summary.md` exists.
|
|
328
|
+
|
|
329
|
+
If the command fails (parser cannot extract required fields), log a warning to the completion summary but do NOT abort archival — the full spec is already archived and the summary can be regenerated later via `node ~/.claude/specflow-cc/bin/sf-tools.cjs archive backfill`.
|
|
330
|
+
|
|
321
331
|
## Step 9: Update STATE.md
|
|
322
332
|
|
|
323
333
|
### Remove from Active Specifications Table
|
|
@@ -457,6 +467,7 @@ git commit -m "docs(sf): complete SPEC-XXX"
|
|
|
457
467
|
- [ ] Decisions extracted (if any)
|
|
458
468
|
- [ ] Source TODO file deleted (if `source:` field exists in spec and file exists in todos/)
|
|
459
469
|
- [ ] Spec moved to archive
|
|
470
|
+
- [ ] L1 summary file created at .specflow/archive/SPEC-XXX.summary.md (or warning logged)
|
|
460
471
|
- [ ] STATE.md updated (cleared active, removed from queue)
|
|
461
472
|
- [ ] Final commit created
|
|
462
473
|
- [ ] Clear completion summary shown
|
|
@@ -13,7 +13,7 @@ Migrate an existing monolithic `.specflow/todos/TODO.md` to the new per-file for
|
|
|
13
13
|
This is a one-time migration command. After migration:
|
|
14
14
|
- `TODO.md` is renamed to `TODO.md.bak` (NOT deleted — safety net)
|
|
15
15
|
- Each TODO becomes its own `TODO-XXX.md` file
|
|
16
|
-
- `INDEX.md` is
|
|
16
|
+
- `INDEX.md` is regenerated from the new files via the shared `todo reindex` helper
|
|
17
17
|
- All other commands will use the new per-file format automatically
|
|
18
18
|
|
|
19
19
|
Use `--dry-run` to preview the migration without writing any files.
|
|
@@ -173,24 +173,14 @@ created: {YYYY-MM-DD}
|
|
|
173
173
|
|
|
174
174
|
## Step 7: Generate INDEX.md
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
Invoke the shared regen helper to build `.specflow/todos/INDEX.md` from the migrated files:
|
|
177
177
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
> Auto-generated from individual TODO files. Do not edit manually.
|
|
182
|
-
> Regenerate with `/sf:todos`.
|
|
183
|
-
|
|
184
|
-
| # | ID | Title | Priority | Status | Created |
|
|
185
|
-
|---|-----|-------|----------|--------|---------|
|
|
186
|
-
{one row per TODO, sorted by priority then created date}
|
|
187
|
-
|
|
188
|
-
**Total:** {N} items ({high} high, {medium} medium, {low} low, {unset} unset)
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
*Last regenerated: {YYYY-MM-DD HH:MM}*
|
|
178
|
+
```bash
|
|
179
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
192
180
|
```
|
|
193
181
|
|
|
182
|
+
Do NOT write INDEX.md inline — the helper is the single source of truth for its layout (see `templates/todo-index.md`).
|
|
183
|
+
|
|
194
184
|
## Step 8: Rename Legacy TODO.md
|
|
195
185
|
|
|
196
186
|
```bash
|
|
@@ -245,7 +235,7 @@ Migrated {N} TODOs from TODO.md to per-file format.
|
|
|
245
235
|
- [ ] Individual TODO-XXX.md files created for each block
|
|
246
236
|
- [ ] Each file has valid YAML frontmatter (id, title, priority, status, created)
|
|
247
237
|
- [ ] Title derived from description (first sentence, ~80 chars)
|
|
248
|
-
- [ ] INDEX.md
|
|
238
|
+
- [ ] INDEX.md regenerated via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex`
|
|
249
239
|
- [ ] TODO.md renamed to TODO.md.bak (NOT deleted)
|
|
250
240
|
- [ ] Clear migration summary shown
|
|
251
241
|
- [ ] Cleanup instructions provided
|
package/commands/sf/plan.md
CHANGED
|
@@ -171,6 +171,14 @@ rm .specflow/todos/TODO-{XXX}.md
|
|
|
171
171
|
|
|
172
172
|
**Important:** Only remove after confirmed spec creation. No "Last updated" lines to update.
|
|
173
173
|
|
|
174
|
+
3. **Refresh INDEX.md** via the shared regen helper so the cache no longer references the removed file:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
178
|
+
```
|
|
179
|
+
|
|
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
|
+
|
|
174
182
|
## Step 8: Display Result
|
|
175
183
|
|
|
176
184
|
**IMPORTANT:** Output the following directly as formatted text, NOT wrapped in a markdown code block:
|
|
@@ -232,7 +240,12 @@ Use `/sf:new "{todo description}"` logic:
|
|
|
232
240
|
|
|
233
241
|
### Remove Todo
|
|
234
242
|
|
|
235
|
-
Delete the file `.specflow/todos/TODO-{XXX}.md
|
|
243
|
+
Delete the file `.specflow/todos/TODO-{XXX}.md`, then refresh INDEX.md:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
rm .specflow/todos/TODO-{XXX}.md
|
|
247
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
248
|
+
```
|
|
236
249
|
|
|
237
250
|
</fallback>
|
|
238
251
|
|
|
@@ -246,5 +259,6 @@ Delete the file `.specflow/todos/TODO-{XXX}.md`.
|
|
|
246
259
|
- [ ] Priority inherited from todo
|
|
247
260
|
- [ ] TODO-XXX.md file deleted (not edited — whole file removed)
|
|
248
261
|
- [ ] Deletion verified (file no longer exists)
|
|
262
|
+
- [ ] INDEX.md refreshed via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex`
|
|
249
263
|
- [ ] Clear result with next step
|
|
250
264
|
</success_criteria>
|
package/commands/sf/priority.md
CHANGED
|
@@ -123,7 +123,12 @@ If input matches pattern `{ID}={priority}`:
|
|
|
123
123
|
|
|
124
124
|
1. Validate priority (high | medium | low)
|
|
125
125
|
2. **If ID is a spec (SPEC-XXX):** Update `priority:` in frontmatter of `.specflow/specs/SPEC-XXX.md` using the Edit tool
|
|
126
|
-
3. **If ID is a TODO (TODO-XXX):**
|
|
126
|
+
3. **If ID is a TODO (TODO-XXX):**
|
|
127
|
+
a. Read `.specflow/todos/TODO-XXX.md`, update `priority:` line in frontmatter using the Edit tool
|
|
128
|
+
b. Refresh INDEX.md so the cached priority column matches:
|
|
129
|
+
```bash
|
|
130
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
131
|
+
```
|
|
127
132
|
4. Display confirmation
|
|
128
133
|
5. Return to Step 3
|
|
129
134
|
|
|
@@ -203,6 +208,7 @@ Use `/sf:next` to work on highest priority task.
|
|
|
203
208
|
- [ ] Reorder command works
|
|
204
209
|
- [ ] Technical order suggestion available
|
|
205
210
|
- [ ] TODO priority updated in individual file frontmatter (not TODO.md)
|
|
211
|
+
- [ ] INDEX.md refreshed via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex` after any TODO priority change
|
|
206
212
|
- [ ] STATE.md Queue updated after spec reorder
|
|
207
213
|
- [ ] Clear feedback on changes
|
|
208
214
|
</success_criteria>
|
package/commands/sf/revise.md
CHANGED
|
@@ -511,8 +511,12 @@ After recording the Response, if any items were marked "Deferred":
|
|
|
511
511
|
Origin: {SPEC-XXX} Response v{N}. {reason for deferral}
|
|
512
512
|
```
|
|
513
513
|
3. Append "**TODOs Created:**" subsection to the Response in Audit History listing created TODO IDs
|
|
514
|
+
4. After the loop completes (at least one TODO created), refresh INDEX.md:
|
|
515
|
+
```bash
|
|
516
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
517
|
+
```
|
|
514
518
|
|
|
515
|
-
**This step is mandatory.** Every "Deferred" decision MUST produce a corresponding TODO-XXX.md file.
|
|
519
|
+
**This step is mandatory.** Every "Deferred" decision MUST produce a corresponding TODO-XXX.md file AND the reindex helper MUST run if any TODO was created — otherwise INDEX.md silently drifts out of sync with `todos/`.
|
|
516
520
|
|
|
517
521
|
### Update Status
|
|
518
522
|
|
|
@@ -532,6 +536,7 @@ node bin/sf-tools.cjs state add-active SPEC-XXX auditing /sf:audit
|
|
|
532
536
|
- [ ] Changes applied correctly
|
|
533
537
|
- [ ] Response recorded in Audit History
|
|
534
538
|
- [ ] Deferred items (if any) created as individual TODO-XXX.md files in `.specflow/todos/`
|
|
539
|
+
- [ ] INDEX.md refreshed via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex` (if any TODO was created)
|
|
535
540
|
- [ ] Spec frontmatter status updated
|
|
536
541
|
- [ ] STATE.md updated
|
|
537
542
|
- [ ] Clear summary of changes shown
|
package/commands/sf/status.md
CHANGED
|
@@ -103,6 +103,30 @@ Likely cause: Spec numbering bug - archive was not checked when generating ID.
|
|
|
103
103
|
Fix: Rename the spec in specs/ to next available ID.
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
### TODO Index Freshness
|
|
107
|
+
|
|
108
|
+
Compare the set of TODO files on disk to the IDs listed in `.specflow/todos/INDEX.md`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node bin/sf-tools.cjs todo check-stale
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Parse the JSON response. If `stale: true`, add a warning to the Warnings section:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
INDEX.md stale — run /sf:todos (or `node bin/sf-tools.cjs todo reindex`).
|
|
118
|
+
{If missing_from_index is non-empty:}
|
|
119
|
+
Missing from INDEX.md (TODO file exists on disk but not listed):
|
|
120
|
+
{comma-separated list}
|
|
121
|
+
{If extra_in_index is non-empty:}
|
|
122
|
+
Stale entries in INDEX.md (listed but TODO file no longer exists):
|
|
123
|
+
{comma-separated list}
|
|
124
|
+
{If !index_exists and todo_count > 0:}
|
|
125
|
+
INDEX.md does not exist yet but {todo_count} TODO file(s) are on disk.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This is a safety net — every command that mutates `todos/` is supposed to call the regen helper itself, but external edits, manual `rm`, or a missed call will surface here. Do NOT auto-fix from `/sf:status`; only report. The user runs `/sf:todos` (or the helper directly) to clear the warning.
|
|
129
|
+
|
|
106
130
|
## Step 5: Determine Next Action
|
|
107
131
|
|
|
108
132
|
Based on current status:
|
|
@@ -208,6 +232,7 @@ Based on state, provide additional guidance:
|
|
|
208
232
|
- [ ] STATE.md loaded
|
|
209
233
|
- [ ] PROJECT.md info extracted
|
|
210
234
|
- [ ] Statistics calculated
|
|
235
|
+
- [ ] TODO index freshness checked via `node bin/sf-tools.cjs todo check-stale` (warning surfaced if stale)
|
|
211
236
|
- [ ] Current position displayed
|
|
212
237
|
- [ ] Queue shown
|
|
213
238
|
- [ ] Recommended next step clear
|
package/commands/sf/todos.md
CHANGED
|
@@ -8,7 +8,7 @@ allowed-tools:
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
<purpose>
|
|
11
|
-
Display all to-do items from the backlog, sorted by priority. Reads individual TODO-XXX.md files (or legacy TODO.md for backward compatibility).
|
|
11
|
+
Display all to-do items from the backlog, sorted by priority. Reads individual TODO-XXX.md files (or legacy TODO.md for backward compatibility). Refreshes the INDEX.md cache via the shared regen helper after display. Provides quick access to convert items to specifications.
|
|
12
12
|
</purpose>
|
|
13
13
|
|
|
14
14
|
<context>
|
|
@@ -109,27 +109,15 @@ From the list:
|
|
|
109
109
|
|
|
110
110
|
## Step 6: Regenerate INDEX.md
|
|
111
111
|
|
|
112
|
-
After displaying the list,
|
|
112
|
+
After displaying the list, regenerate `.specflow/todos/INDEX.md` by invoking the shared helper:
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```markdown
|
|
117
|
-
# To-Do Index
|
|
118
|
-
|
|
119
|
-
> Auto-generated from individual TODO files. Do not edit manually.
|
|
120
|
-
> Regenerate with `/sf:todos`.
|
|
121
|
-
|
|
122
|
-
| # | ID | Title | Priority | Status | Created |
|
|
123
|
-
|---|-----|-------|----------|--------|---------|
|
|
124
|
-
{rows from sorted list — one row per TODO}
|
|
125
|
-
|
|
126
|
-
**Total:** {N} items ({high} high, {medium} medium, {low} low, {unset} unset)
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
*Last regenerated: {YYYY-MM-DD HH:MM}*
|
|
114
|
+
```bash
|
|
115
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
130
116
|
```
|
|
131
117
|
|
|
132
|
-
|
|
118
|
+
The helper scans `.specflow/todos/TODO-*.md`, sorts the rows the same way Step 2 does, and writes INDEX.md in the format defined by `templates/todo-index.md`. It is the single source of truth for INDEX layout — do NOT write INDEX.md manually here.
|
|
119
|
+
|
|
120
|
+
**Important:** INDEX.md is a cache. Other commands that mutate `todos/` (`/sf:todo`, `/sf:plan`, `/sf:done`, `/sf:triage`, `/sf:revise`, `/sf:priority`, `/sf:migrate-todos`, and the `sf-spec-reviser` agent) must call the same helper after their mutation so the cache stays consistent between `/sf:todos` invocations. `/sf:status` runs `todo check-stale` and warns if drift is detected.
|
|
133
121
|
|
|
134
122
|
</workflow>
|
|
135
123
|
|
|
@@ -142,6 +130,6 @@ Use the format from `templates/todo-index.md`:
|
|
|
142
130
|
- [ ] `--all` flag shows eliminated items visually distinct
|
|
143
131
|
- [ ] Statistics shown (total, by priority)
|
|
144
132
|
- [ ] Clear actions provided
|
|
145
|
-
- [ ] INDEX.md
|
|
146
|
-
- [ ] INDEX.md
|
|
133
|
+
- [ ] INDEX.md regenerated via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex` after display
|
|
134
|
+
- [ ] INDEX.md header describes it as a cache refreshed by `/sf:todos` or the regen helper
|
|
147
135
|
</success_criteria>
|
package/commands/sf/triage.md
CHANGED
|
@@ -188,6 +188,16 @@ created: {YYYY-MM-DD}
|
|
|
188
188
|
|
|
189
189
|
Do NOT append to TODO.md. Do NOT update any "Last updated" lines. Each finding gets its own separate TODO-XXX.md file.
|
|
190
190
|
|
|
191
|
+
### 6.3 Refresh INDEX.md
|
|
192
|
+
|
|
193
|
+
After all selected TODO files have been written, regenerate the cache once:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This is mandatory whenever the triage loop creates at least one TODO. Skipping it leaves INDEX.md missing the just-created entries until the next `/sf:todos` run.
|
|
200
|
+
|
|
191
201
|
## Step 7: Display Results
|
|
192
202
|
|
|
193
203
|
**IMPORTANT:** Output the following directly as formatted text, NOT wrapped in a markdown code block:
|
|
@@ -255,5 +265,6 @@ Run /sf:triage again to review findings.
|
|
|
255
265
|
- [ ] Each file has valid YAML frontmatter (id, title, priority, status, created)
|
|
256
266
|
- [ ] Priority preserved from scan
|
|
257
267
|
- [ ] Source reference included in notes (scan date, files, problem)
|
|
268
|
+
- [ ] INDEX.md refreshed via `node ~/.claude/specflow-cc/bin/sf-tools.cjs todo reindex` (skip only if zero TODOs created)
|
|
258
269
|
- [ ] Clear summary of created TODOs
|
|
259
270
|
</success_criteria>
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
spec_id: {SPEC-ID}
|
|
3
|
+
title: {full title}
|
|
4
|
+
type: {feature|refactor|bugfix}
|
|
5
|
+
completed: {YYYY-MM-DD}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# {SPEC-ID} Summary
|
|
9
|
+
|
|
10
|
+
**Goal:** {one-sentence goal extracted from Goal Statement or Context}
|
|
11
|
+
|
|
12
|
+
**Key Decisions:**
|
|
13
|
+
- {decision 1}
|
|
14
|
+
- {decision 2}
|
|
15
|
+
- {decision 3}
|
|
16
|
+
|
|
17
|
+
**Key Files:**
|
|
18
|
+
- {path 1} — {one-line purpose}
|
|
19
|
+
- {path 2} — {one-line purpose}
|
|
20
|
+
|
|
21
|
+
**Tests:** {test/foo.test.cjs, ...} or "none"
|
|
22
|
+
|
|
23
|
+
**Full Spec:** [.specflow/archive/{SPEC-ID}.md](./{SPEC-ID}.md)
|
|
24
|
+
|
|
25
|
+
**Related Future Specs:** none yet
|
package/templates/todo-index.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# To-Do Index
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
3
|
+
> Cache of individual TODO files. Refreshed when `/sf:todos` runs OR when an
|
|
4
|
+
> INDEX-mutating command explicitly invokes the regen helper
|
|
5
|
+
> (`node bin/sf-tools.cjs todo reindex`). Do not edit manually — changes will
|
|
6
|
+
> be overwritten on the next regen.
|
|
5
7
|
|
|
6
8
|
| # | ID | Title | Priority | Status | Created |
|
|
7
9
|
|---|-----|-------|----------|--------|---------|
|