specflow-cc 1.17.1 → 1.18.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 +29 -0
- package/agents/spec-creator.md +2 -0
- package/agents/spec-reviser.md +29 -12
- package/bin/install.js +14 -0
- package/bin/lib/todo.cjs +238 -0
- package/bin/sf-tools.cjs +21 -0
- package/commands/sf/done.md +15 -6
- package/commands/sf/health.md +11 -3
- package/commands/sf/init.md +2 -2
- package/commands/sf/metrics.md +15 -6
- package/commands/sf/migrate-todos.md +252 -0
- package/commands/sf/plan.md +35 -24
- package/commands/sf/priority.md +13 -8
- package/commands/sf/revise.md +27 -7
- package/commands/sf/status.md +2 -2
- package/commands/sf/todo.md +30 -36
- package/commands/sf/todos.md +68 -56
- package/commands/sf/triage.md +30 -30
- package/package.json +1 -1
- package/templates/todo-file.md +18 -0
- package/templates/todo-index.md +13 -0
- package/templates/todo.md +0 -15
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.18.1] - 2026-04-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Installer now copies `bin/`** — the installer previously shipped `agents/`, `templates/`, `commands/`, and `hooks/` into `~/.claude/specflow-cc/` but never copied `bin/`, so any slash command invoking `node bin/sf-tools.cjs ...` failed with `MODULE_NOT_FOUND` in user projects. Affected commands: `/sf:todos`, `/sf:priority`, `/sf:metrics`, `/sf:plan`, `/sf:triage`, `/sf:revise`, `/sf:todo`
|
|
13
|
+
- Installer now copies `bin/` recursively (excluding `install.js` itself) via the existing `copyWithPathReplacement` helper
|
|
14
|
+
- All 12 affected command invocations rewritten to use the absolute path `node ~/.claude/specflow-cc/bin/sf-tools.cjs` (auto-rewritten to `./.claude/specflow-cc/` for local installs by the existing path-replacement pass)
|
|
15
|
+
- New smoke test `tests/install-bin-cli.test.cjs` runs the full installer in a temp project and invokes the installed CLI end-to-end, preventing regression
|
|
16
|
+
|
|
17
|
+
## [1.18.0] - 2026-04-08
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Per-file TODO storage** — TODOs migrated from monolithic `TODO.md` to individual `TODO-XXX.md` files with YAML frontmatter, mirroring the established `SPEC-XXX.md` pattern
|
|
22
|
+
- Each TODO is now a standalone file in `.specflow/todos/` with structured metadata (id, title, priority, complexity, status, effort, depends_on, created)
|
|
23
|
+
- Auto-generated `INDEX.md` serves as a human-readable cache, regenerated by `/sf:todos`
|
|
24
|
+
- New `bin/lib/todo.cjs` utility module with `cmdTodoLoad`, `cmdTodoList`, `cmdTodoNextId` functions
|
|
25
|
+
- New CLI commands: `todo load <id>`, `todo list [--all]`, `todo next-id`
|
|
26
|
+
- New `/sf:migrate-todos` command for one-time migration from legacy format (with `--dry-run` support)
|
|
27
|
+
- Backward compatibility: projects with legacy `TODO.md` continue to work transparently
|
|
28
|
+
- Eliminated TODOs use `status: eliminated` soft-delete instead of file deletion, preserving history
|
|
29
|
+
- **Updated commands** — all 10 TODO-touching commands and 3 agents updated to use per-file format:
|
|
30
|
+
`sf:todo`, `sf:todos`, `sf:plan`, `sf:done`, `sf:priority`, `sf:triage`, `sf:revise`, `sf:health`, `sf:status`, `sf:metrics`, `sf:init`, `spec-reviser`, `spec-creator`
|
|
31
|
+
- **New health checks** — W009 (TODO file without valid frontmatter), W010 (legacy TODO.md alongside per-file TODOs)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **W001 health check** — now checks for `todos/` directory instead of `TODO.md` file
|
|
36
|
+
|
|
8
37
|
## [1.17.1] - 2026-04-07
|
|
9
38
|
|
|
10
39
|
### Fixed
|
package/agents/spec-creator.md
CHANGED
|
@@ -156,6 +156,8 @@ If no specs exist in either directory, start with SPEC-001.
|
|
|
156
156
|
Write to `.specflow/specs/SPEC-XXX.md` using the template structure:
|
|
157
157
|
|
|
158
158
|
1. **Frontmatter:** id, type, status (draft), priority, complexity, created, source (if `<todo_context>` provided — set to the TODO ID, e.g., `source: TODO-006`), and optionally `delta: true` (only for brownfield tasks detected in Step 2.7)
|
|
159
|
+
|
|
160
|
+
**Note on `source:` field:** The `source: TODO-XXX` value refers to the identifier of a per-file TODO stored at `.specflow/todos/TODO-XXX.md`. It does NOT refer to an entry in a monolithic `TODO.md`. When `/sf:plan` converts a TODO and then `/sf:done` completes the spec, the cleanup step deletes `.specflow/todos/TODO-XXX.md` based on this field.
|
|
159
161
|
2. **Title:** Clear, action-oriented
|
|
160
162
|
3. **Context:** Why this is needed
|
|
161
163
|
- **If `<prior_discussion>` provided:** Add "Prior Discussion" subsection linking to PRE-XXX or DISC-XXX with key decisions
|
package/agents/spec-reviser.md
CHANGED
|
@@ -143,16 +143,33 @@ After recording the Response, check if any items were marked as deferred (⏸).
|
|
|
143
143
|
|
|
144
144
|
For each deferred item:
|
|
145
145
|
|
|
146
|
-
1.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
1. Generate next TODO ID:
|
|
147
|
+
```bash
|
|
148
|
+
node bin/sf-tools.cjs todo next-id --raw
|
|
149
|
+
```
|
|
150
|
+
2. Create `.specflow/todos/TODO-{XXX}.md` using the Write tool:
|
|
151
|
+
```markdown
|
|
152
|
+
---
|
|
153
|
+
id: TODO-{XXX}
|
|
154
|
+
title: "{item description} (deferred from {SPEC-XXX} audit v{N})"
|
|
155
|
+
priority: —
|
|
156
|
+
complexity: —
|
|
157
|
+
status: open
|
|
158
|
+
effort: —
|
|
159
|
+
depends_on: —
|
|
160
|
+
created: {YYYY-MM-DD}
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Description
|
|
164
|
+
|
|
165
|
+
{item description} (deferred from {SPEC-XXX} audit v{N})
|
|
166
|
+
|
|
167
|
+
## Notes
|
|
168
|
+
|
|
169
|
+
Origin: {SPEC-XXX} Response v{N}. {reason for deferral}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
3. Append a "TODOs Created" subsection to the Response in Audit History:
|
|
156
173
|
|
|
157
174
|
```markdown
|
|
158
175
|
**TODOs Created:**
|
|
@@ -209,7 +226,7 @@ Output directly as formatted text (not wrapped in a code block):
|
|
|
209
226
|
|
|
210
227
|
- .specflow/specs/SPEC-XXX.md
|
|
211
228
|
{If TODOs created:}
|
|
212
|
-
- .specflow/todos/TODO.md
|
|
229
|
+
- .specflow/todos/TODO-{XXX}.md
|
|
213
230
|
|
|
214
231
|
### Next Step
|
|
215
232
|
|
|
@@ -226,7 +243,7 @@ Tip: `/clear` recommended — auditor needs fresh context
|
|
|
226
243
|
- [ ] User's revision scope understood
|
|
227
244
|
- [ ] Changes applied precisely
|
|
228
245
|
- [ ] Revision Response recorded in Audit History
|
|
229
|
-
- [ ] Deferred items (if any) created as
|
|
246
|
+
- [ ] Deferred items (if any) created as individual `.specflow/todos/TODO-XXX.md` files
|
|
230
247
|
- [ ] TODOs Created subsection appended to Response (if deferred items exist)
|
|
231
248
|
- [ ] Frontmatter status updated
|
|
232
249
|
- [ ] STATE.md updated
|
package/bin/install.js
CHANGED
|
@@ -213,6 +213,20 @@ function install(isGlobal) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// Copy bin (CLI tools) — exclude install.js, which is the installer itself
|
|
217
|
+
const binSrc = path.join(src, 'bin');
|
|
218
|
+
if (fs.existsSync(binSrc)) {
|
|
219
|
+
const binDest = path.join(specflowDir, 'bin');
|
|
220
|
+
copyWithPathReplacement(binSrc, binDest, pathPrefix);
|
|
221
|
+
const installerCopy = path.join(binDest, 'install.js');
|
|
222
|
+
if (fs.existsSync(installerCopy)) fs.unlinkSync(installerCopy);
|
|
223
|
+
if (verifyInstalled(binDest, 'bin')) {
|
|
224
|
+
console.log(` ${green}✓${reset} Installed bin (CLI tools)`);
|
|
225
|
+
} else {
|
|
226
|
+
failures.push('bin');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
216
230
|
// Copy hooks
|
|
217
231
|
const hooksSrc = path.join(src, 'hooks');
|
|
218
232
|
if (fs.existsSync(hooksSrc)) {
|
package/bin/lib/todo.cjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/todo.cjs — TODO operations
|
|
3
|
+
*
|
|
4
|
+
* Exports: cmdTodoLoad(), cmdTodoList(), cmdTodoNextId()
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the pattern of bin/lib/spec.cjs.
|
|
7
|
+
* Supports both per-file format (TODO-XXX.md) and legacy monolithic TODO.md.
|
|
8
|
+
* Format detection is based on presence of TODO-*.md files — INDEX.md is NOT
|
|
9
|
+
* the detection signal (it may not exist until sf:todos is first run).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { output, error, safeReadFile, parseFrontmatter } = require('./core.cjs');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Priority sort order (lower number = higher priority in sort).
|
|
20
|
+
*/
|
|
21
|
+
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get sort key for a priority value.
|
|
25
|
+
* @param {string} priority
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
function priorityKey(priority) {
|
|
29
|
+
if (priority && PRIORITY_ORDER[priority] !== undefined) {
|
|
30
|
+
return PRIORITY_ORDER[priority];
|
|
31
|
+
}
|
|
32
|
+
return 3; // unset / —
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load and parse a TODO file.
|
|
37
|
+
* @param {string} cwd - Working directory
|
|
38
|
+
* @param {string} id - TODO ID (e.g., "TODO-007")
|
|
39
|
+
* @param {boolean} raw - Output raw string
|
|
40
|
+
*/
|
|
41
|
+
function cmdTodoLoad(cwd, id, raw) {
|
|
42
|
+
if (!id) {
|
|
43
|
+
error('Missing TODO ID. Usage: todo load <id>');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const todoPath = path.join(cwd, '.specflow', 'todos', id + '.md');
|
|
47
|
+
const content = safeReadFile(todoPath);
|
|
48
|
+
|
|
49
|
+
if (!content) {
|
|
50
|
+
error('TODO not found: ' + todoPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = parseFrontmatter(content);
|
|
54
|
+
|
|
55
|
+
output({
|
|
56
|
+
frontmatter: parsed.frontmatter,
|
|
57
|
+
body: parsed.body,
|
|
58
|
+
}, raw, parsed.body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* List all TODOs.
|
|
63
|
+
*
|
|
64
|
+
* Format detection:
|
|
65
|
+
* 1. If TODO-*.md files exist in .specflow/todos/ — use per-file format
|
|
66
|
+
* 2. If no per-file TODOs but TODO.md exists — use legacy format
|
|
67
|
+
* 3. If neither — return empty list
|
|
68
|
+
*
|
|
69
|
+
* @param {string} cwd - Working directory
|
|
70
|
+
* @param {boolean} raw - Output raw string
|
|
71
|
+
* @param {object} options - Options
|
|
72
|
+
* @param {boolean} options.showAll - If true, include eliminated items
|
|
73
|
+
*/
|
|
74
|
+
function cmdTodoList(cwd, raw, { showAll } = {}) {
|
|
75
|
+
const todosDir = path.join(cwd, '.specflow', 'todos');
|
|
76
|
+
|
|
77
|
+
// Check for per-file TODOs
|
|
78
|
+
let perFiles;
|
|
79
|
+
try {
|
|
80
|
+
perFiles = fs.readdirSync(todosDir).filter(f => /^TODO-\d+\.md$/.test(f)).sort();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
perFiles = [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (perFiles.length > 0) {
|
|
86
|
+
// Per-file format
|
|
87
|
+
const todos = [];
|
|
88
|
+
|
|
89
|
+
for (const file of perFiles) {
|
|
90
|
+
const content = safeReadFile(path.join(todosDir, file));
|
|
91
|
+
if (!content) continue;
|
|
92
|
+
|
|
93
|
+
const parsed = parseFrontmatter(content);
|
|
94
|
+
const fm = parsed.frontmatter;
|
|
95
|
+
|
|
96
|
+
// Filter eliminated unless showAll
|
|
97
|
+
if (!showAll && fm.status === 'eliminated') continue;
|
|
98
|
+
|
|
99
|
+
todos.push({
|
|
100
|
+
id: fm.id || file.replace('.md', ''),
|
|
101
|
+
title: fm.title || '',
|
|
102
|
+
priority: fm.priority || '—',
|
|
103
|
+
status: fm.status || 'open',
|
|
104
|
+
complexity: fm.complexity || '—',
|
|
105
|
+
created: fm.created || '',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Sort by priority (high > medium > low > unset), then by created date (oldest first)
|
|
110
|
+
todos.sort((a, b) => {
|
|
111
|
+
const pa = priorityKey(a.priority);
|
|
112
|
+
const pb = priorityKey(b.priority);
|
|
113
|
+
if (pa !== pb) return pa - pb;
|
|
114
|
+
// Compare dates lexicographically (ISO dates sort correctly as strings)
|
|
115
|
+
if (a.created < b.created) return -1;
|
|
116
|
+
if (a.created > b.created) return 1;
|
|
117
|
+
return 0;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
output(todos, raw, todos.map(t => t.id).join('\n'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Legacy format: check for monolithic TODO.md
|
|
125
|
+
const legacyPath = path.join(todosDir, 'TODO.md');
|
|
126
|
+
const legacyContent = safeReadFile(legacyPath);
|
|
127
|
+
|
|
128
|
+
if (legacyContent) {
|
|
129
|
+
// Parse legacy TODO blocks: ## TODO-XXX — YYYY-MM-DD
|
|
130
|
+
const todos = [];
|
|
131
|
+
const blockRegex = /^## (TODO-\d+) — (\d{4}-\d{2}-\d{2})\s*\n([\s\S]*?)(?=^## TODO-|\Z)/gm;
|
|
132
|
+
let match;
|
|
133
|
+
|
|
134
|
+
// Append sentinel heading so the last block's lazy [\s\S]*? terminates correctly
|
|
135
|
+
while ((match = blockRegex.exec(legacyContent + '\n## TODO-END')) !== null) {
|
|
136
|
+
const id = match[1];
|
|
137
|
+
const created = match[2];
|
|
138
|
+
const body = match[3];
|
|
139
|
+
|
|
140
|
+
// Extract description
|
|
141
|
+
const descMatch = body.match(/\*\*Description:\*\*\s*(.+)/);
|
|
142
|
+
const title = descMatch ? descMatch[1].trim() : '';
|
|
143
|
+
|
|
144
|
+
// Extract priority
|
|
145
|
+
const prioMatch = body.match(/\*\*Priority:\*\*\s*(\S+)/);
|
|
146
|
+
const priority = prioMatch ? prioMatch[1].trim() : '—';
|
|
147
|
+
|
|
148
|
+
if (!showAll) {
|
|
149
|
+
// Legacy format has no eliminated status — always include
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
todos.push({
|
|
153
|
+
id,
|
|
154
|
+
title,
|
|
155
|
+
priority,
|
|
156
|
+
status: 'open',
|
|
157
|
+
complexity: '—',
|
|
158
|
+
created,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sort same way
|
|
163
|
+
todos.sort((a, b) => {
|
|
164
|
+
const pa = priorityKey(a.priority);
|
|
165
|
+
const pb = priorityKey(b.priority);
|
|
166
|
+
if (pa !== pb) return pa - pb;
|
|
167
|
+
if (a.created < b.created) return -1;
|
|
168
|
+
if (a.created > b.created) return 1;
|
|
169
|
+
return 0;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
output(todos, raw, todos.map(t => t.id).join('\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Neither format found — empty list
|
|
177
|
+
output([], raw, '');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Calculate the next available TODO-XXX number.
|
|
182
|
+
*
|
|
183
|
+
* Scans:
|
|
184
|
+
* 1. .specflow/todos/TODO-*.md filenames using fs.readdirSync() + JS regex
|
|
185
|
+
* 2. .specflow/todos/TODO.md for legacy IDs using fs.readFileSync() + /TODO-(\d+)/g
|
|
186
|
+
*
|
|
187
|
+
* NOTE: Does NOT use grep -oP (GNU-only, unavailable on macOS).
|
|
188
|
+
*
|
|
189
|
+
* @param {string} cwd - Working directory
|
|
190
|
+
* @param {boolean} raw - Output raw string
|
|
191
|
+
*/
|
|
192
|
+
function cmdTodoNextId(cwd, raw) {
|
|
193
|
+
const todosDir = path.join(cwd, '.specflow', 'todos');
|
|
194
|
+
|
|
195
|
+
let maxNum = 0;
|
|
196
|
+
|
|
197
|
+
// Scan per-file TODOs
|
|
198
|
+
try {
|
|
199
|
+
const files = fs.readdirSync(todosDir);
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
const match = file.match(/^TODO-(\d+)\.md$/);
|
|
202
|
+
if (match) {
|
|
203
|
+
const num = parseInt(match[1], 10);
|
|
204
|
+
if (num > maxNum) maxNum = num;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// directory may not exist yet
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Scan legacy TODO.md for any IDs referenced there
|
|
212
|
+
const legacyPath = path.join(todosDir, 'TODO.md');
|
|
213
|
+
try {
|
|
214
|
+
const legacyContent = fs.readFileSync(legacyPath, 'utf8');
|
|
215
|
+
const regex = /TODO-(\d+)/g;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = regex.exec(legacyContent)) !== null) {
|
|
218
|
+
const num = parseInt(match[1], 10);
|
|
219
|
+
if (num > maxNum) maxNum = num;
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// file may not exist — skip
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const nextNumber = maxNum + 1;
|
|
226
|
+
const nextId = 'TODO-' + String(nextNumber).padStart(3, '0');
|
|
227
|
+
|
|
228
|
+
output({
|
|
229
|
+
next_id: nextId,
|
|
230
|
+
next_number: nextNumber,
|
|
231
|
+
}, raw, nextId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
cmdTodoLoad,
|
|
236
|
+
cmdTodoList,
|
|
237
|
+
cmdTodoNextId,
|
|
238
|
+
};
|
package/bin/sf-tools.cjs
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* spec load <id> Parse spec file, return frontmatter + body
|
|
10
10
|
* spec list List all specs
|
|
11
11
|
* spec next-id Next available SPEC-XXX number
|
|
12
|
+
* todo load <id> Parse TODO file, return frontmatter + body
|
|
13
|
+
* todo list [--all] List all TODOs sorted by priority
|
|
14
|
+
* todo next-id Next available TODO-XXX number
|
|
12
15
|
* queue next First actionable spec from queue
|
|
13
16
|
* state get Current active spec, status, next step
|
|
14
17
|
* state set-active <id> <status> [next] Update active spec in STATE.md
|
|
@@ -22,6 +25,7 @@
|
|
|
22
25
|
const { output, error, generateSlug } = require('./lib/core.cjs');
|
|
23
26
|
const { cmdStateGet, cmdStateSetActive, cmdQueueNext } = require('./lib/state.cjs');
|
|
24
27
|
const { cmdSpecLoad, cmdSpecList, cmdSpecNextId } = require('./lib/spec.cjs');
|
|
28
|
+
const { cmdTodoLoad, cmdTodoList, cmdTodoNextId } = require('./lib/todo.cjs');
|
|
25
29
|
const { cmdResolveModel } = require('./lib/config.cjs');
|
|
26
30
|
const { cmdVerifyStructure } = require('./lib/verify.cjs');
|
|
27
31
|
|
|
@@ -34,6 +38,14 @@ const filteredArgs = args.filter(a => a !== '--raw');
|
|
|
34
38
|
* Command dispatch table.
|
|
35
39
|
* Keys are "command subcommand" or just "command".
|
|
36
40
|
*/
|
|
41
|
+
const flags = {};
|
|
42
|
+
for (const arg of filteredArgs) {
|
|
43
|
+
if (arg.startsWith('--')) {
|
|
44
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
45
|
+
flags[key] = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
const COMMANDS = {
|
|
38
50
|
'spec load': () => {
|
|
39
51
|
if (!filteredArgs[2]) error('Missing spec ID. Usage: spec load <id>');
|
|
@@ -41,6 +53,12 @@ const COMMANDS = {
|
|
|
41
53
|
},
|
|
42
54
|
'spec list': () => cmdSpecList(cwd, raw),
|
|
43
55
|
'spec next-id': () => cmdSpecNextId(cwd, raw),
|
|
56
|
+
'todo load': () => {
|
|
57
|
+
if (!filteredArgs[2]) error('Missing TODO ID. Usage: todo load <id>');
|
|
58
|
+
cmdTodoLoad(cwd, filteredArgs[2], raw);
|
|
59
|
+
},
|
|
60
|
+
'todo list': () => cmdTodoList(cwd, raw, { showAll: flags.all ?? false }),
|
|
61
|
+
'todo next-id': () => cmdTodoNextId(cwd, raw),
|
|
44
62
|
'queue next': () => cmdQueueNext(cwd, raw),
|
|
45
63
|
'state get': () => cmdStateGet(cwd, raw),
|
|
46
64
|
'state set-active': () => {
|
|
@@ -69,6 +87,9 @@ Commands:
|
|
|
69
87
|
spec load <id> Parse spec file, return frontmatter + body
|
|
70
88
|
spec list List all specs from .specflow/specs/
|
|
71
89
|
spec next-id Next available SPEC-XXX number
|
|
90
|
+
todo load <id> Parse TODO file, return frontmatter + body
|
|
91
|
+
todo list [--all] List TODOs sorted by priority (--all includes eliminated)
|
|
92
|
+
todo next-id Next available TODO-XXX number
|
|
72
93
|
queue next First actionable spec from queue table
|
|
73
94
|
state get Current active spec, status, next step
|
|
74
95
|
state set-active <id> <status> [next] Update active spec, status, next step
|
package/commands/sf/done.md
CHANGED
|
@@ -277,12 +277,21 @@ Check if the spec frontmatter contains a `source:` field (e.g., `source: TODO-00
|
|
|
277
277
|
|
|
278
278
|
**If `source:` field exists:**
|
|
279
279
|
|
|
280
|
-
1.
|
|
281
|
-
2. Check if the referenced TODO-XXX entry still exists in the file
|
|
282
|
-
3. **If it exists** — use the **Edit** tool (NOT Write) to remove the entire block (from `## TODO-XXX` heading through the next `---` separator, inclusive) — replace with empty string
|
|
283
|
-
4. Use the **Edit** tool to update `*Last updated:` timestamp with note: `TODO-XXX cleaned up (completed via SPEC-YYY)`
|
|
280
|
+
1. Check if `.specflow/todos/{source}.md` exists (per-file format):
|
|
284
281
|
|
|
285
|
-
|
|
282
|
+
```bash
|
|
283
|
+
[ -f .specflow/todos/{source}.md ] && echo "FOUND" || echo "NOT_FOUND"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
2. **If FOUND:** Delete the file:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
rm .specflow/todos/{source}.md
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
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
|
+
|
|
294
|
+
No "Last updated" lines to update in per-file format.
|
|
286
295
|
|
|
287
296
|
**If no `source:` field or TODO already removed:** Skip — no action needed.
|
|
288
297
|
|
|
@@ -431,7 +440,7 @@ git commit -m "docs(sf): complete SPEC-XXX"
|
|
|
431
440
|
- [ ] Spec status updated to "done"
|
|
432
441
|
- [ ] Completion section added
|
|
433
442
|
- [ ] Decisions extracted (if any)
|
|
434
|
-
- [ ] Source TODO
|
|
443
|
+
- [ ] Source TODO file deleted (if `source:` field exists in spec and file exists in todos/)
|
|
435
444
|
- [ ] Spec moved to archive
|
|
436
445
|
- [ ] STATE.md updated (cleared active, removed from queue)
|
|
437
446
|
- [ ] Final commit created
|
package/commands/sf/health.md
CHANGED
|
@@ -50,16 +50,22 @@ Initialize collectors:
|
|
|
50
50
|
|-------|------|----------|------------|
|
|
51
51
|
| E001 | `.specflow/STATE.md` missing | error | Yes — regenerate minimal STATE.md |
|
|
52
52
|
| E002 | `.specflow/STATE.md` missing `## Queue` section | error | No |
|
|
53
|
-
| W001 | `.specflow/todos
|
|
53
|
+
| W001 | `.specflow/todos/` directory missing | warning | Yes — create directory |
|
|
54
54
|
| W002 | `.specflow/specs/` directory missing | warning | Yes — create directory |
|
|
55
55
|
| W003 | `.specflow/archive/` directory missing | warning | Yes — create directory |
|
|
56
56
|
| W004 | `.specflow/execution/` directory missing | warning | Yes — create directory |
|
|
57
|
+
| W009 | TODO file without valid YAML frontmatter | warning | No — inspect manually |
|
|
58
|
+
| W010 | Legacy `TODO.md` exists alongside per-file TODOs (suggest migration) | info | No — run `/sf:migrate-todos` |
|
|
57
59
|
|
|
58
60
|
For each check:
|
|
59
61
|
1. Test existence
|
|
60
62
|
2. If missing and repairable and `--repair`: fix it, add to `repairs[]`
|
|
61
63
|
3. If missing and not repairable: add to `errors[]` or `warnings[]`
|
|
62
64
|
|
|
65
|
+
**For W009:** List all `TODO-*.md` files in `.specflow/todos/` and check each for valid YAML frontmatter (presence of `id:`, `status:`, `created:` fields).
|
|
66
|
+
|
|
67
|
+
**For W010:** If both `TODO-*.md` files AND `TODO.md` exist in `.specflow/todos/`, add info note suggesting `/sf:migrate-todos` to complete migration.
|
|
68
|
+
|
|
63
69
|
### 3.2 STATE.md Integrity
|
|
64
70
|
|
|
65
71
|
Read STATE.md and validate:
|
|
@@ -181,7 +187,7 @@ Display final status.
|
|
|
181
187
|
| E001 | error | STATE.md not found | Yes |
|
|
182
188
|
| E002 | error | STATE.md missing Queue section | No |
|
|
183
189
|
| E003 | error | Active spec references non-existent file | Yes |
|
|
184
|
-
| W001 | warning |
|
|
190
|
+
| W001 | warning | todos/ directory missing | Yes |
|
|
185
191
|
| W002 | warning | specs/ directory missing | Yes |
|
|
186
192
|
| W003 | warning | archive/ directory missing | Yes |
|
|
187
193
|
| W004 | warning | execution/ directory missing | Yes |
|
|
@@ -189,6 +195,8 @@ Display final status.
|
|
|
189
195
|
| W006 | warning | Queue has duplicate spec IDs | No |
|
|
190
196
|
| W007 | warning | STATE.md missing required sections | No |
|
|
191
197
|
| W008 | warning | Stale execution state files | Yes |
|
|
198
|
+
| W009 | warning | TODO file without valid frontmatter | No |
|
|
199
|
+
| W010 | info | Legacy TODO.md alongside per-file TODOs | No |
|
|
192
200
|
| I001 | info | Spec not in queue (may be WIP) | No |
|
|
193
201
|
| I002 | info | Completed spec not archived | No |
|
|
194
202
|
|
|
@@ -199,7 +207,7 @@ Display final status.
|
|
|
199
207
|
| Action | Effect | Risk |
|
|
200
208
|
|--------|--------|------|
|
|
201
209
|
| Create STATE.md | Minimal template with empty queue | None |
|
|
202
|
-
| Create
|
|
210
|
+
| Create todos/ directory | Empty directory for per-file TODOs | None |
|
|
203
211
|
| Create directories | specs/, archive/, execution/ | None |
|
|
204
212
|
| Clear active spec | Set to "—" if spec file missing | Loses active reference |
|
|
205
213
|
| Delete stale execution | Remove orphaned .json state files | None |
|
package/commands/sf/init.md
CHANGED
|
@@ -39,7 +39,7 @@ Check whether the user invoked `/sf:init --force`. Look at the invocation string
|
|
|
39
39
|
[ -f .specflow/PROJECT.md ] && echo "HAS_PROJECT_MD" || true
|
|
40
40
|
[ -f .specflow/STATE.md ] && echo "HAS_STATE_MD" || true
|
|
41
41
|
[ -f .specflow/config.json ] && echo "HAS_CONFIG_JSON" || true
|
|
42
|
-
[ -
|
|
42
|
+
[ -d .specflow/todos ] && echo "HAS_TODOS_DIR" || true
|
|
43
43
|
[ "$(ls -A .specflow/specs 2>/dev/null)" ] && echo "HAS_SPECS" || true
|
|
44
44
|
[ "$(ls -A .specflow/archive 2>/dev/null)" ] && echo "HAS_ARCHIVE" || true
|
|
45
45
|
```
|
|
@@ -58,7 +58,7 @@ The following files/directories would be overwritten:
|
|
|
58
58
|
- .specflow/PROJECT.md
|
|
59
59
|
- .specflow/STATE.md
|
|
60
60
|
- .specflow/config.json
|
|
61
|
-
- .specflow/todos/
|
|
61
|
+
- .specflow/todos/ (directory)
|
|
62
62
|
- .specflow/specs/ (contains files)
|
|
63
63
|
- .specflow/archive/ (contains files)
|
|
64
64
|
|
package/commands/sf/metrics.md
CHANGED
|
@@ -17,7 +17,7 @@ Calculate and display project statistics including completion rates, quality met
|
|
|
17
17
|
@.specflow/STATE.md
|
|
18
18
|
@.specflow/specs/SPEC-*.md
|
|
19
19
|
@.specflow/archive/SPEC-*.md
|
|
20
|
-
@.specflow/todos/
|
|
20
|
+
@.specflow/todos/
|
|
21
21
|
</context>
|
|
22
22
|
|
|
23
23
|
<workflow>
|
|
@@ -79,13 +79,22 @@ For each `.specflow/archive/SPEC-*.md`:
|
|
|
79
79
|
### Parse To-Dos
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
# Count todos
|
|
83
|
-
|
|
82
|
+
# Count todos via CLI tool (format-agnostic, excludes eliminated by default)
|
|
83
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo list --raw
|
|
84
|
+
```
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
This returns the list of open TODO IDs. For `--all` (including eliminated):
|
|
87
|
+
```bash
|
|
88
|
+
node ~/.claude/specflow-cc/bin/sf-tools.cjs todo list --all --raw
|
|
87
89
|
```
|
|
88
90
|
|
|
91
|
+
Parse priority breakdown from the JSON output to count by priority level.
|
|
92
|
+
|
|
93
|
+
Also check:
|
|
94
|
+
- `TODO_COUNT` = number of open TODOs (from default list)
|
|
95
|
+
- `TODO_COUNT_ALL` = total including eliminated (from --all list)
|
|
96
|
+
- Count converted (todos that became specs) approximated from decisions table
|
|
97
|
+
|
|
89
98
|
### Get Project Start Date
|
|
90
99
|
|
|
91
100
|
Read `.specflow/PROJECT.md` for initialized date, or use earliest spec creation date.
|
|
@@ -241,7 +250,7 @@ Based on calculated metrics, generate 2-4 actionable insights:
|
|
|
241
250
|
|
|
242
251
|
## To-Do Backlog
|
|
243
252
|
|
|
244
|
-
{If TODO
|
|
253
|
+
{If TODO count > 0:}
|
|
245
254
|
| Metric | Value |
|
|
246
255
|
|----------------|-------|
|
|
247
256
|
| Total items | {N} |
|