specflow-cc 1.21.0 → 1.22.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 +22 -0
- package/agents/impl-reviewer.md +32 -0
- package/agents/spec-auditor.md +32 -0
- package/bin/lib/recommend.cjs +57 -0
- package/bin/lib/todo.cjs +27 -0
- package/bin/sf-tools.cjs +100 -0
- package/commands/sf/audit.md +9 -0
- package/commands/sf/done.md +81 -0
- package/commands/sf/fix.md +6 -0
- package/commands/sf/review.md +14 -1
- package/commands/sf/revise.md +9 -1
- package/commands/sf/run.md +71 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.22.1] - 2026-05-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `todo next-id` now scans `.specflow/specs/` and `.specflow/archive/` for `source: TODO-XXX` frontmatter entries, preventing reissue of retired IDs. Previously, a TODO file deleted during promotion to a spec could have its ID reassigned to an unrelated new TODO, leaving downstream `/sf:plan` unable to operate on the new TODO (it rejects the ID as "already promoted").
|
|
13
|
+
|
|
14
|
+
## [1.22.0] - 2026-05-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`Recommendation:` line on `/sf:audit` and `/sf:review` output** — after every audit or review, the Next Step block now includes a deterministic `**Recommendation:** {action} — {reason}` line (e.g. `done — implementation is clean, ready to finalize`, `run --apply=minor — 2 non-blocking recommendations, apply inline`, `revise — 1 critical issue blocks execution`). Removes the "what next" guesswork when the result has only non-blocking findings. Emitted by `sf-spec-auditor` and `sf-impl-reviewer` agents via a new Step 7.5 that shells out to `node bin/sf-tools.cjs recommend`. STATE.md's canonical `Next Step` is unchanged — the Recommendation line is advisory in agent output only.
|
|
19
|
+
- **`--apply=minor` flag on `/sf:done` and `/sf:run`** — quick-fix path for non-blocking findings. `/sf:done --apply=minor` (review path): requires spec status `review` with only Minor findings; invokes `/sf:fix --internal` to apply each finding as an atomic commit; runs project test + lint gate; on success, proceeds to standard finalization. `/sf:run --apply=minor` (audit path): requires status `audited` with only Recommendations; invokes `/sf:revise --internal`; runs structural `spec validate` gate; on success, proceeds to standard execution. **No second audit/review cycle is invoked.** Both refuse with a clear error when Critical/Major findings exist. On gate failure: applied commits remain in git, but STATE.md status is unchanged (sole rollback signal).
|
|
20
|
+
- **`recommend` CLI subcommand** in `bin/sf-tools.cjs` — pure mapping module exposed via `node bin/sf-tools.cjs recommend --source <audit|review> --critical N --major N --minor N`. Emits JSON `{action, reason}` to stdout. Single source of truth for the recommendation truth-table consumed by both agents and `--apply=minor` callers.
|
|
21
|
+
- **`spec validate` CLI subcommand** in `bin/sf-tools.cjs` — lightweight structural validation (frontmatter parses, required fields present, `## Requirements` heading present). Exits 0 on success with no stdout; exits 1 with `Error: spec validation failed: {reason}` on stderr. Used by `/sf:run --apply=minor` as the post-revise integrity gate (distinct from content-driven `/sf:audit`).
|
|
22
|
+
- **`--internal` flag on `/sf:fix` and `/sf:revise`** — symmetric guard that suppresses the Step 8 `state add-active` STATE.md mutation. Lets `/sf:done --apply=minor` and `/sf:run --apply=minor` shell out to the existing fix/revise machinery without losing control of the lifecycle transition. Reusable pattern for future composite commands.
|
|
23
|
+
- `bin/lib/recommend.cjs` — pure 57-line `recommend({source, critical, major, minor})` → `{action, reason}` function; no I/O, no state mutation. Zero new runtime npm dependencies — Node.js built-ins only.
|
|
24
|
+
- 39 new tests across `test/recommend.test.cjs` (28 tests: truth-table coverage, CLI integration, error cases) and `test/spec-validate.test.cjs` (11 tests: success + all five failure modes). Full suite: 93/93 pass.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Brittle hardcoded-spec test in spec-validate suite** — removed a test that referenced a live `SPEC-013.md` file, which broke the moment SPEC-013 was archived (the normal end of every spec's lifecycle). The success path is fully covered by an adjacent temp-fixture test, so the duplicate was removed rather than rewritten.
|
|
29
|
+
|
|
8
30
|
## [1.21.0] - 2026-05-15
|
|
9
31
|
|
|
10
32
|
### Added
|
package/agents/impl-reviewer.md
CHANGED
|
@@ -283,6 +283,31 @@ Append to specification's Review History:
|
|
|
283
283
|
**Summary:** {Brief overall assessment}
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
+
## Step 7.5: Emit Recommendation
|
|
287
|
+
|
|
288
|
+
Using the Critical, Major, and Minor counts determined in Step 5:
|
|
289
|
+
|
|
290
|
+
1. Shell out to obtain the recommendation:
|
|
291
|
+
```
|
|
292
|
+
node bin/sf-tools.cjs recommend --source review --critical <N> --major <M> --minor <K>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
2. Parse the JSON response: `{ "action": "...", "reason": "..." }`
|
|
296
|
+
|
|
297
|
+
3. In the REVIEW RESULT output block (within the "Next Step" section), emit:
|
|
298
|
+
```
|
|
299
|
+
**Recommendation:** {action} — {reason}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
4. Also append the same line to the Review History entry (in Step 7) below the existing fields:
|
|
303
|
+
```
|
|
304
|
+
**Recommendation:** {action}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Verb mapping per source:** For `source=review`, blocker verb is `fix` (matches STATE.md canonical `/sf:fix`); non-blocker verbs are `done` / `done --apply=minor`. This asymmetry is deliberate: Recommendation verbs align with the existing per-path canonical commands.
|
|
308
|
+
|
|
309
|
+
**Note:** STATE.md Next Step (Step 8) continues to use the canonical command (`/sf:done`, `/sf:fix`) without any `--apply=minor` suffix. The Recommendation line is advisory and appears only in agent output and review history.
|
|
310
|
+
|
|
286
311
|
## Step 8: Update STATE.md
|
|
287
312
|
|
|
288
313
|
- If APPROVED: Status → "done", Next Step → "/sf:done"
|
|
@@ -344,14 +369,21 @@ Output directly as formatted text (not wrapped in a code block):
|
|
|
344
369
|
## Next Step
|
|
345
370
|
|
|
346
371
|
{If APPROVED with NO minor issues:}
|
|
372
|
+
**Recommendation:** {action} — {reason}
|
|
373
|
+
|
|
347
374
|
`/sf:done` — finalize and archive
|
|
348
375
|
|
|
349
376
|
{If APPROVED WITH minor issues:}
|
|
377
|
+
**Recommendation:** {action} — {reason}
|
|
378
|
+
|
|
350
379
|
Choose one:
|
|
351
380
|
• `/sf:done` — finalize as-is (minor issues are optional)
|
|
352
381
|
• `/sf:fix` — address minor issues first
|
|
382
|
+
• `/sf:done --apply=minor` — apply minor fixes inline and finalize in one step
|
|
353
383
|
|
|
354
384
|
{If CHANGES_REQUESTED:}
|
|
385
|
+
**Recommendation:** {action} — {reason}
|
|
386
|
+
|
|
355
387
|
`/sf:fix` — address issues
|
|
356
388
|
|
|
357
389
|
Options:
|
package/agents/spec-auditor.md
CHANGED
|
@@ -770,6 +770,30 @@ N+1. [recommendation]
|
|
|
770
770
|
**Comment:** [Brief positive note about spec quality]
|
|
771
771
|
```
|
|
772
772
|
|
|
773
|
+
## Step 7.5: Emit Recommendation
|
|
774
|
+
|
|
775
|
+
Using the Critical count and Recommendations count determined in Step 5:
|
|
776
|
+
|
|
777
|
+
1. Shell out to obtain the recommendation:
|
|
778
|
+
```
|
|
779
|
+
node bin/sf-tools.cjs recommend --source audit --critical <N> --minor <M>
|
|
780
|
+
```
|
|
781
|
+
Note: The CLI flag is `--minor` even though the auditor uses the label "Recommendations" — this is intentional for parser symmetry across audit/review sources.
|
|
782
|
+
|
|
783
|
+
2. Parse the JSON response: `{ "action": "...", "reason": "..." }`
|
|
784
|
+
|
|
785
|
+
3. In the AUDIT RESULT output block (within the "Next Step" section), emit:
|
|
786
|
+
```
|
|
787
|
+
**Recommendation:** {action} — {reason}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
4. Also append the same line to the Audit History entry (in Step 7) below the existing fields:
|
|
791
|
+
```
|
|
792
|
+
**Recommendation:** {action}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
**Note:** STATE.md Next Step (Step 8) continues to use the canonical command (`/sf:run`, `/sf:revise`, `/sf:split`) without any `--apply=minor` suffix. The Recommendation line is advisory and appears only in agent output and audit history.
|
|
796
|
+
|
|
773
797
|
## Step 8: Update STATE.md
|
|
774
798
|
|
|
775
799
|
Update status:
|
|
@@ -826,6 +850,8 @@ Output directly as formatted text (not wrapped in a code block):
|
|
|
826
850
|
|
|
827
851
|
### Next Step
|
|
828
852
|
|
|
853
|
+
**Recommendation:** {action} — {reason}
|
|
854
|
+
|
|
829
855
|
Choose one:
|
|
830
856
|
- `/sf:run --parallel` — execute with subagent orchestration
|
|
831
857
|
- `/sf:split` — decompose into smaller specs
|
|
@@ -846,6 +872,8 @@ Choose one:
|
|
|
846
872
|
|
|
847
873
|
### Next Step
|
|
848
874
|
|
|
875
|
+
**Recommendation:** {action} — {reason}
|
|
876
|
+
|
|
849
877
|
`/sf:revise` — address critical issues
|
|
850
878
|
|
|
851
879
|
---
|
|
@@ -858,6 +886,8 @@ Choose one:
|
|
|
858
886
|
|
|
859
887
|
### Next Step
|
|
860
888
|
|
|
889
|
+
**Recommendation:** {action} — {reason}
|
|
890
|
+
|
|
861
891
|
`/sf:run` — implement specification
|
|
862
892
|
|
|
863
893
|
Tip: `/clear` recommended — executor needs fresh context for implementation
|
|
@@ -877,6 +907,8 @@ N+1. [recommendation]
|
|
|
877
907
|
|
|
878
908
|
### Next Steps
|
|
879
909
|
|
|
910
|
+
**Recommendation:** {action} — {reason}
|
|
911
|
+
|
|
880
912
|
Choose one:
|
|
881
913
|
- `/sf:run` — implement specification as-is
|
|
882
914
|
- `/sf:revise` — apply optional recommendations first ({N} items)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/lib/recommend.cjs — Pure mapping module for audit/review recommendations.
|
|
3
|
+
*
|
|
4
|
+
* Exports: recommend({ source, critical, major, minor }) → { action, reason }
|
|
5
|
+
*
|
|
6
|
+
* No I/O, no state mutation, fully unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map severity counts and source to a recommended action and human-readable reason.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {string} opts.source - 'audit' or 'review'
|
|
16
|
+
* @param {number} [opts.critical=0] - Number of critical findings
|
|
17
|
+
* @param {number} [opts.major=0] - Number of major findings (ignored for source 'audit')
|
|
18
|
+
* @param {number} [opts.minor=0] - Number of minor findings / recommendations
|
|
19
|
+
* @returns {{ action: string, reason: string }}
|
|
20
|
+
*/
|
|
21
|
+
function recommend({ source, critical = 0, major = 0, minor = 0 } = {}) {
|
|
22
|
+
if (source !== 'audit' && source !== 'review') {
|
|
23
|
+
throw new Error(`Unknown source: ${source}. Expected 'audit' or 'review'.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
!Number.isInteger(critical) || critical < 0 ||
|
|
28
|
+
!Number.isInteger(major) || major < 0 ||
|
|
29
|
+
!Number.isInteger(minor) || minor < 0
|
|
30
|
+
) {
|
|
31
|
+
throw new Error('Counts must be non-negative integers.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (source === 'audit') {
|
|
35
|
+
if (critical >= 1) {
|
|
36
|
+
return { action: 'revise', reason: `${critical} critical issue(s) block execution` };
|
|
37
|
+
}
|
|
38
|
+
if (minor >= 1) {
|
|
39
|
+
return { action: 'run --apply=minor', reason: `${minor} non-blocking recommendation(s), apply inline` };
|
|
40
|
+
}
|
|
41
|
+
return { action: 'run', reason: 'spec is clean, ready for execution' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// source === 'review'
|
|
45
|
+
if (critical >= 1) {
|
|
46
|
+
return { action: 'fix', reason: `${critical} critical finding(s) block finalize` };
|
|
47
|
+
}
|
|
48
|
+
if (major >= 1) {
|
|
49
|
+
return { action: 'fix', reason: `${major} major finding(s) block finalize` };
|
|
50
|
+
}
|
|
51
|
+
if (minor >= 1) {
|
|
52
|
+
return { action: 'done --apply=minor', reason: `${minor} minor finding(s), apply inline before finalize` };
|
|
53
|
+
}
|
|
54
|
+
return { action: 'done', reason: 'implementation is clean, ready to finalize' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { recommend };
|
package/bin/lib/todo.cjs
CHANGED
|
@@ -183,6 +183,8 @@ function cmdTodoList(cwd, raw, { showAll } = {}) {
|
|
|
183
183
|
* Scans:
|
|
184
184
|
* 1. .specflow/todos/TODO-*.md filenames using fs.readdirSync() + JS regex
|
|
185
185
|
* 2. .specflow/todos/TODO.md for legacy IDs using fs.readFileSync() + /TODO-(\d+)/g
|
|
186
|
+
* 3. .specflow/specs/*.md and .specflow/archive/*.md for `source: TODO-XXX`
|
|
187
|
+
* frontmatter entries (retired IDs from promoted TODOs).
|
|
186
188
|
*
|
|
187
189
|
* NOTE: Does NOT use grep -oP (GNU-only, unavailable on macOS).
|
|
188
190
|
*
|
|
@@ -222,6 +224,31 @@ function cmdTodoNextId(cwd, raw) {
|
|
|
222
224
|
// file may not exist — skip
|
|
223
225
|
}
|
|
224
226
|
|
|
227
|
+
// Scan promoted-spec frontmatter for retired TODO IDs.
|
|
228
|
+
// On promotion, the source TODO file is deleted; the only surviving
|
|
229
|
+
// record is `source: TODO-XXX` in the spec's frontmatter. Without this
|
|
230
|
+
// scan, next-id can reissue a retired ID and downstream `/sf:plan` will
|
|
231
|
+
// reject the new TODO because the archive still records the old promotion.
|
|
232
|
+
for (const sub of ['specs', 'archive']) {
|
|
233
|
+
const dir = path.join(cwd, '.specflow', sub);
|
|
234
|
+
let files;
|
|
235
|
+
try {
|
|
236
|
+
files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
237
|
+
} catch (e) { continue; }
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
let content;
|
|
240
|
+
try {
|
|
241
|
+
content = fs.readFileSync(path.join(dir, file), 'utf8');
|
|
242
|
+
} catch (e) { continue; }
|
|
243
|
+
const regex = /(?:^|\n)source:\s*TODO-(\d+)/g;
|
|
244
|
+
let match;
|
|
245
|
+
while ((match = regex.exec(content)) !== null) {
|
|
246
|
+
const num = parseInt(match[1], 10);
|
|
247
|
+
if (num > maxNum) maxNum = num;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
225
252
|
const nextNumber = maxNum + 1;
|
|
226
253
|
const nextId = 'TODO-' + String(nextNumber).padStart(3, '0');
|
|
227
254
|
|
package/bin/sf-tools.cjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
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
|
+
* spec validate <id> Validate spec frontmatter and required headings
|
|
12
13
|
* todo load <id> Parse TODO file, return frontmatter + body
|
|
13
14
|
* todo list [--all] List all TODOs sorted by priority
|
|
14
15
|
* todo next-id Next available TODO-XXX number
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
* state migrate One-shot idempotent migration to new schema
|
|
25
26
|
* archive summarize <SPEC-ID> Generate L1 summary for one archived spec
|
|
26
27
|
* archive backfill [--force] Generate missing summaries for all archived specs
|
|
28
|
+
* recommend Map severity counts to recommended action
|
|
27
29
|
* resolve-model <agent-type> Model for agent by current profile
|
|
28
30
|
* verify-structure Check .specflow/ integrity
|
|
29
31
|
* generate-slug <text> Text to URL-safe slug
|
|
@@ -47,6 +49,7 @@ const { cmdTodoLoad, cmdTodoList, cmdTodoNextId, cmdTodoReindex, cmdTodoCheckSta
|
|
|
47
49
|
const { cmdResolveModel } = require('./lib/config.cjs');
|
|
48
50
|
const { cmdVerifyStructure } = require('./lib/verify.cjs');
|
|
49
51
|
const { cmdArchiveSummarize, cmdArchiveBackfill } = require('./lib/archive-summary.cjs');
|
|
52
|
+
const { recommend } = require('./lib/recommend.cjs');
|
|
50
53
|
|
|
51
54
|
const cwd = process.cwd();
|
|
52
55
|
const args = process.argv.slice(2);
|
|
@@ -72,6 +75,49 @@ const COMMANDS = {
|
|
|
72
75
|
},
|
|
73
76
|
'spec list': () => cmdSpecList(cwd, raw),
|
|
74
77
|
'spec next-id': () => cmdSpecNextId(cwd, raw),
|
|
78
|
+
'spec validate': () => {
|
|
79
|
+
const specId = filteredArgs[2];
|
|
80
|
+
if (!specId) {
|
|
81
|
+
process.stderr.write('Error: spec validation failed: missing spec ID. Usage: spec validate <SPEC-XXX>\n');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const { safeReadFile, parseFrontmatter } = require('./lib/core.cjs');
|
|
85
|
+
const specPath = require('path').join(cwd, '.specflow', 'specs', specId + '.md');
|
|
86
|
+
const content = safeReadFile(specPath);
|
|
87
|
+
if (content === null) {
|
|
88
|
+
process.stderr.write(`Error: spec validation failed: spec file not found at ${specPath}\n`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
let frontmatter;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = parseFrontmatter(content);
|
|
94
|
+
frontmatter = parsed.frontmatter;
|
|
95
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
96
|
+
throw new Error('invalid frontmatter');
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
process.stderr.write('Error: spec validation failed: invalid or missing frontmatter\n');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
// Require ---...--- block to exist (parseFrontmatter returns empty obj if absent)
|
|
103
|
+
if (!content.match(/^---\r?\n[\s\S]*?\r?\n---/)) {
|
|
104
|
+
process.stderr.write('Error: spec validation failed: invalid or missing frontmatter\n');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const required = ['id', 'type', 'status', 'priority'];
|
|
108
|
+
for (const field of required) {
|
|
109
|
+
if (!frontmatter[field]) {
|
|
110
|
+
process.stderr.write(`Error: spec validation failed: missing frontmatter field '${field}'\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!content.match(/^## Requirements/m)) {
|
|
115
|
+
process.stderr.write("Error: spec validation failed: missing required heading '## Requirements'\n");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
// Success: no stdout, exit 0
|
|
119
|
+
process.exit(0);
|
|
120
|
+
},
|
|
75
121
|
'todo load': () => {
|
|
76
122
|
if (!filteredArgs[2]) error('Missing TODO ID. Usage: todo load <id>');
|
|
77
123
|
cmdTodoLoad(cwd, filteredArgs[2], raw);
|
|
@@ -125,6 +171,58 @@ const COMMANDS = {
|
|
|
125
171
|
cmdArchiveBackfill(cwd, { force: flags.force });
|
|
126
172
|
},
|
|
127
173
|
|
|
174
|
+
'recommend': () => {
|
|
175
|
+
// Parse --source, --critical, --major, --minor from filteredArgs
|
|
176
|
+
// Flags take the form: --source audit or --critical 2 etc.
|
|
177
|
+
const flagValues = {};
|
|
178
|
+
for (let i = 1; i < filteredArgs.length; i++) {
|
|
179
|
+
const a = filteredArgs[i];
|
|
180
|
+
if (a.startsWith('--')) {
|
|
181
|
+
const key = a.slice(2);
|
|
182
|
+
const val = filteredArgs[i + 1];
|
|
183
|
+
if (val !== undefined && !val.startsWith('--')) {
|
|
184
|
+
flagValues[key] = val;
|
|
185
|
+
i++; // skip value token
|
|
186
|
+
} else {
|
|
187
|
+
flagValues[key] = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const source = flagValues['source'];
|
|
193
|
+
if (!source || source === true) {
|
|
194
|
+
process.stderr.write('Error: --source is required (audit|review)\n');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Parse integer counts with validation
|
|
199
|
+
function parseCount(flagName) {
|
|
200
|
+
const raw = flagValues[flagName];
|
|
201
|
+
if (raw === undefined || raw === true) return 0;
|
|
202
|
+
const n = Number(raw);
|
|
203
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
204
|
+
process.stderr.write(`Error: --${flagName} must be a non-negative integer\n`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
return n;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const critical = parseCount('critical');
|
|
211
|
+
const major = parseCount('major');
|
|
212
|
+
const minor = parseCount('minor');
|
|
213
|
+
|
|
214
|
+
let result;
|
|
215
|
+
try {
|
|
216
|
+
result = recommend({ source, critical, major, minor });
|
|
217
|
+
} catch (e) {
|
|
218
|
+
process.stderr.write('Error: ' + e.message + '\n');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
223
|
+
process.exit(0);
|
|
224
|
+
},
|
|
225
|
+
|
|
128
226
|
'resolve-model': () => {
|
|
129
227
|
if (!filteredArgs[1]) error('Missing agent type. Usage: resolve-model <agent-type>');
|
|
130
228
|
cmdResolveModel(cwd, filteredArgs[1], raw);
|
|
@@ -145,6 +243,7 @@ Commands:
|
|
|
145
243
|
spec load <id> Parse spec file, return frontmatter + body
|
|
146
244
|
spec list List all specs from .specflow/specs/
|
|
147
245
|
spec next-id Next available SPEC-XXX number
|
|
246
|
+
spec validate <id> Validate spec frontmatter and required headings
|
|
148
247
|
todo load <id> Parse TODO file, return frontmatter + body
|
|
149
248
|
todo list [--all] List TODOs sorted by priority (--all includes eliminated)
|
|
150
249
|
todo next-id Next available TODO-XXX number
|
|
@@ -160,6 +259,7 @@ Commands:
|
|
|
160
259
|
state migrate One-shot idempotent migration to new schema
|
|
161
260
|
archive summarize <SPEC-ID> Generate L1 summary for one archived spec
|
|
162
261
|
archive backfill [--force] Generate missing summaries for all archived specs
|
|
262
|
+
recommend Map severity counts to recommended action
|
|
163
263
|
resolve-model <agent-type> Resolve model for agent by current profile
|
|
164
264
|
verify-structure Check .specflow/ directory integrity
|
|
165
265
|
generate-slug <text> Convert text to URL-safe slug
|
package/commands/sf/audit.md
CHANGED
|
@@ -290,6 +290,8 @@ After the agent updates STATE.md, check if rotation is needed:
|
|
|
290
290
|
|
|
291
291
|
## Next Step
|
|
292
292
|
|
|
293
|
+
**Recommendation:** run — spec is clean, ready for execution
|
|
294
|
+
|
|
293
295
|
`/sf:run` — implement specification
|
|
294
296
|
|
|
295
297
|
Tip: `/clear` recommended — executor needs fresh context for implementation
|
|
@@ -297,6 +299,8 @@ Tip: `/clear` recommended — executor needs fresh context for implementation
|
|
|
297
299
|
|
|
298
300
|
### If APPROVED (with optional recommendations):
|
|
299
301
|
|
|
302
|
+
The `Recommendation:` line is emitted by the auditor agent (Step 7.5 in `agents/spec-auditor.md`) using `node bin/sf-tools.cjs recommend --source audit --critical 0 --minor N`. The STATE.md Next Step remains `/sf:run` (without the `--apply=minor` suffix) — the suffix is advisory here only.
|
|
303
|
+
|
|
300
304
|
```
|
|
301
305
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
302
306
|
AUDIT PASSED
|
|
@@ -318,8 +322,11 @@ Tip: `/clear` recommended — executor needs fresh context for implementation
|
|
|
318
322
|
|
|
319
323
|
## Next Step
|
|
320
324
|
|
|
325
|
+
**Recommendation:** run --apply=minor — {N} non-blocking recommendation(s), apply inline
|
|
326
|
+
|
|
321
327
|
Choose one:
|
|
322
328
|
• `/sf:run` — implement specification as-is
|
|
329
|
+
• `/sf:run --apply=minor` — apply recommendations inline then execute
|
|
323
330
|
• `/sf:revise` — apply optional recommendations first ({N} items)
|
|
324
331
|
|
|
325
332
|
Tip: `/clear` recommended before `/sf:run` — executor needs fresh context
|
|
@@ -352,6 +359,8 @@ Tip: `/clear` recommended before `/sf:run` — executor needs fresh context
|
|
|
352
359
|
|
|
353
360
|
## Next Step
|
|
354
361
|
|
|
362
|
+
**Recommendation:** revise — {N} critical issue(s) block execution
|
|
363
|
+
|
|
355
364
|
`/sf:revise` — address critical issues
|
|
356
365
|
|
|
357
366
|
Options:
|
package/commands/sf/done.md
CHANGED
|
@@ -59,6 +59,87 @@ Parse the JSON response:
|
|
|
59
59
|
Options: {id — title (status)} for each entry
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
## Step 2.5: Handle `--apply=minor` Flag
|
|
63
|
+
|
|
64
|
+
**Check if `--apply=minor` was passed in the invocation arguments.**
|
|
65
|
+
|
|
66
|
+
**If `--apply=minor` is NOT present:** Continue to Step 3 (existing behavior unchanged).
|
|
67
|
+
|
|
68
|
+
**If `--apply=minor` IS present:**
|
|
69
|
+
|
|
70
|
+
### 2.5.a Verify Status Precondition
|
|
71
|
+
|
|
72
|
+
Confirm the resolved spec has `status == "review"` in its frontmatter.
|
|
73
|
+
|
|
74
|
+
If status is NOT `review`:
|
|
75
|
+
```
|
|
76
|
+
Error: --apply=minor requires status 'review' (current: {status})
|
|
77
|
+
```
|
|
78
|
+
Exit 1. No state mutation.
|
|
79
|
+
|
|
80
|
+
### 2.5.b Parse Severity Counts from Latest Review History
|
|
81
|
+
|
|
82
|
+
Read the spec file and find the most recent `### Review v[N]` entry in Review History.
|
|
83
|
+
|
|
84
|
+
Extract Critical, Major, and Minor counts from that entry.
|
|
85
|
+
|
|
86
|
+
Run:
|
|
87
|
+
```bash
|
|
88
|
+
node bin/sf-tools.cjs recommend --source review --critical N --major M --minor K
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Parse the JSON response.
|
|
92
|
+
|
|
93
|
+
If `action != "done --apply=minor"`:
|
|
94
|
+
```
|
|
95
|
+
Error: --apply=minor cannot be used when Critical or Major findings exist (found {N} Critical, {M} Major). Run /sf:fix instead.
|
|
96
|
+
```
|
|
97
|
+
Exit 1. No state mutation.
|
|
98
|
+
|
|
99
|
+
### 2.5.c Apply Minor Fixes via `/sf:fix` Machinery
|
|
100
|
+
|
|
101
|
+
Parse the latest Review History entry and extract the numbered list of Minor findings (the sequential numbers as they appear in the Minor section, e.g. `"4,5,7"`).
|
|
102
|
+
|
|
103
|
+
Invoke existing `/sf:fix` machinery passing the numbered target list and `--internal` flag (so `/sf:fix` Step 8 does NOT mutate STATE.md — the caller owns the status transition):
|
|
104
|
+
```
|
|
105
|
+
/sf:fix SPEC-XXX "{N,M,K}" --internal
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This reuses `/sf:fix`'s existing per-fix atomic commit behavior. Do NOT duplicate fix logic.
|
|
109
|
+
|
|
110
|
+
### 2.5.d Test Gate
|
|
111
|
+
|
|
112
|
+
Detect and run the project test command:
|
|
113
|
+
|
|
114
|
+
1. If `package.json` exists and has `scripts.test` → run `npm test`
|
|
115
|
+
2. Else if `test/` directory exists → run `node --test test/`
|
|
116
|
+
3. Else → note `No test command detected; proceeding without test gate.`
|
|
117
|
+
|
|
118
|
+
If test command exits non-zero:
|
|
119
|
+
- Print captured stdout+stderr
|
|
120
|
+
- Leave STATE.md status as `review` (no transition)
|
|
121
|
+
- Exit 1
|
|
122
|
+
- Note: Fix commits remain in git history; user can manually `git revert` or run full `/sf:fix` cycle.
|
|
123
|
+
|
|
124
|
+
### 2.5.e Lint Gate
|
|
125
|
+
|
|
126
|
+
Detect and run lint:
|
|
127
|
+
|
|
128
|
+
1. If `package.json` exists and has `scripts.lint` → run `npm run lint`
|
|
129
|
+
2. Else if `.eslintrc*` or `eslint.config.*` present → run `npx eslint .`
|
|
130
|
+
3. Else → skip silently
|
|
131
|
+
|
|
132
|
+
If lint exits non-zero:
|
|
133
|
+
- Same abort semantics as 2.5.d (print output, leave STATUS as `review`, exit 1)
|
|
134
|
+
|
|
135
|
+
### 2.5.f On Gate Success: Continue to Finalization
|
|
136
|
+
|
|
137
|
+
On all gates passing: continue into existing Step 3+ finalization path (update spec frontmatter status → "done", archive, generate L1 summary per SPEC-012).
|
|
138
|
+
|
|
139
|
+
All STATE.md mutations use Read+Write per SPEC-004 (not Bash/awk/sed).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
62
143
|
## Step 3: Load Specification
|
|
63
144
|
|
|
64
145
|
Read the active spec file: `.specflow/specs/SPEC-XXX.md`
|
package/commands/sf/fix.md
CHANGED
|
@@ -15,6 +15,8 @@ allowed-tools:
|
|
|
15
15
|
|
|
16
16
|
<purpose>
|
|
17
17
|
Fix the implementation based on review feedback. Can apply all fixes, specific numbered items, or custom fixes described by user. Creates atomic commits for each fix.
|
|
18
|
+
|
|
19
|
+
Accepts an `--internal` flag: when present, Step 8 (STATE.md mutation) is suppressed. Used by `/sf:done --apply=minor` to apply minor fixes inline without advancing the spec lifecycle status prematurely.
|
|
18
20
|
</purpose>
|
|
19
21
|
|
|
20
22
|
<context>
|
|
@@ -175,6 +177,10 @@ Append to Review History:
|
|
|
175
177
|
|
|
176
178
|
## Step 8: Update STATE.md
|
|
177
179
|
|
|
180
|
+
**If `--internal` flag was passed:** SKIP this step entirely. Do NOT mutate STATE.md or spec frontmatter status. The caller (`/sf:done --apply=minor`) owns the status transition and needs the status to remain `review` until its test+lint gate passes.
|
|
181
|
+
|
|
182
|
+
**If `--internal` is NOT set (normal invocation):**
|
|
183
|
+
|
|
178
184
|
```bash
|
|
179
185
|
node bin/sf-tools.cjs state add-active SPEC-XXX review /sf:review
|
|
180
186
|
```
|
package/commands/sf/review.md
CHANGED
|
@@ -158,6 +158,8 @@ After the agent updates STATE.md, check if rotation is needed:
|
|
|
158
158
|
|
|
159
159
|
### If APPROVED (no minor issues):
|
|
160
160
|
|
|
161
|
+
The `Recommendation:` line is emitted by the reviewer agent (Step 7.5 in `agents/impl-reviewer.md`) using `node bin/sf-tools.cjs recommend --source review --critical 0 --major 0 --minor 0`. The STATE.md Next Step remains `/sf:done` (canonical).
|
|
162
|
+
|
|
161
163
|
```
|
|
162
164
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
163
165
|
REVIEW PASSED
|
|
@@ -185,11 +187,15 @@ After the agent updates STATE.md, check if rotation is needed:
|
|
|
185
187
|
|
|
186
188
|
## Next Step
|
|
187
189
|
|
|
190
|
+
**Recommendation:** done — implementation is clean, ready to finalize
|
|
191
|
+
|
|
188
192
|
`/sf:done` — finalize and archive specification
|
|
189
193
|
```
|
|
190
194
|
|
|
191
195
|
### If APPROVED (with minor suggestions):
|
|
192
196
|
|
|
197
|
+
The `Recommendation:` line uses action `done --apply=minor` when only Minor findings exist. STATE.md Next Step stays `/sf:done` (canonical; the `--apply=minor` suffix is advisory here only).
|
|
198
|
+
|
|
193
199
|
```
|
|
194
200
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
195
201
|
REVIEW PASSED
|
|
@@ -218,13 +224,18 @@ After the agent updates STATE.md, check if rotation is needed:
|
|
|
218
224
|
|
|
219
225
|
## Next Step
|
|
220
226
|
|
|
227
|
+
**Recommendation:** done --apply=minor — {N} minor finding(s), apply inline before finalize
|
|
228
|
+
|
|
221
229
|
Choose one:
|
|
222
230
|
• `/sf:done` — finalize and archive as-is
|
|
223
|
-
• `/sf:
|
|
231
|
+
• `/sf:done --apply=minor` — apply minor fixes inline and finalize in one step
|
|
232
|
+
• `/sf:fix` — apply minor suggestions first ({N} items) then finalize
|
|
224
233
|
```
|
|
225
234
|
|
|
226
235
|
### If CHANGES_REQUESTED:
|
|
227
236
|
|
|
237
|
+
The `Recommendation:` line uses action `fix` when Critical or Major findings exist (STATE.md Next Step is `/sf:fix`).
|
|
238
|
+
|
|
228
239
|
```
|
|
229
240
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
230
241
|
REVIEW: CHANGES REQUESTED
|
|
@@ -262,6 +273,8 @@ Choose one:
|
|
|
262
273
|
|
|
263
274
|
## Next Step
|
|
264
275
|
|
|
276
|
+
**Recommendation:** fix — {N} critical/major finding(s) block finalize
|
|
277
|
+
|
|
265
278
|
`/sf:fix` — address the issues
|
|
266
279
|
|
|
267
280
|
Options:
|
package/commands/sf/revise.md
CHANGED
|
@@ -14,6 +14,8 @@ allowed-tools:
|
|
|
14
14
|
|
|
15
15
|
<purpose>
|
|
16
16
|
Revise the active specification based on audit feedback. Can apply all comments, specific numbered items, or custom changes described by user.
|
|
17
|
+
|
|
18
|
+
Accepts an `--internal` flag: when present, Step 8 STATE.md mutation (status → `auditing`, Next Step → `/sf:audit`) is suppressed. Used by `/sf:run --apply=minor` to apply Recommendations inline without advancing the spec lifecycle status prematurely.
|
|
17
19
|
</purpose>
|
|
18
20
|
|
|
19
21
|
<context>
|
|
@@ -330,7 +332,9 @@ Apply the specified changes and record the revision response.
|
|
|
330
332
|
|
|
331
333
|
## Step 8: Handle Agent Response
|
|
332
334
|
|
|
333
|
-
The agent
|
|
335
|
+
**If `--internal` flag was passed:** The agent applies revisions and records Response v[N] in Audit History, but DOES NOT update status to "auditing" and DOES NOT update STATE.md. Return to caller after revisions are applied.
|
|
336
|
+
|
|
337
|
+
**If `--internal` is NOT set (normal invocation),** the agent will:
|
|
334
338
|
1. Parse the latest audit
|
|
335
339
|
2. Apply requested revisions
|
|
336
340
|
3. Record Response v[N] in Audit History
|
|
@@ -520,6 +524,10 @@ After recording the Response, if any items were marked "Deferred":
|
|
|
520
524
|
|
|
521
525
|
### Update Status
|
|
522
526
|
|
|
527
|
+
**If `--internal` flag was passed:** SKIP this step entirely. Do NOT mutate STATE.md or spec frontmatter status. The caller (`/sf:run --apply=minor`) owns the status transition and needs the status to remain `audited` until its structural-validate gate passes.
|
|
528
|
+
|
|
529
|
+
**If `--internal` is NOT set (normal invocation):**
|
|
530
|
+
|
|
523
531
|
In spec frontmatter: `status: auditing`
|
|
524
532
|
|
|
525
533
|
In STATE.md:
|
package/commands/sf/run.md
CHANGED
|
@@ -66,6 +66,77 @@ Parse the JSON response:
|
|
|
66
66
|
|
|
67
67
|
Read the active spec file: `.specflow/specs/SPEC-XXX.md`
|
|
68
68
|
|
|
69
|
+
## Step 3.5: Handle `--apply=minor` Flag
|
|
70
|
+
|
|
71
|
+
**Check if `--apply=minor` was passed in the invocation arguments.**
|
|
72
|
+
|
|
73
|
+
**If `--apply=minor` is NOT present:** Continue to Step 4 (existing behavior unchanged).
|
|
74
|
+
|
|
75
|
+
**If `--apply=minor` IS present:**
|
|
76
|
+
|
|
77
|
+
### 3.5.a Verify Status Precondition
|
|
78
|
+
|
|
79
|
+
Confirm the resolved spec has `status == "audited"` in its frontmatter.
|
|
80
|
+
|
|
81
|
+
If status is NOT `audited`:
|
|
82
|
+
```
|
|
83
|
+
Error: --apply=minor requires status 'audited' (current: {status})
|
|
84
|
+
```
|
|
85
|
+
Exit 1. No state mutation.
|
|
86
|
+
|
|
87
|
+
### 3.5.b Parse Severity Counts from Latest Audit History
|
|
88
|
+
|
|
89
|
+
Read the spec file and find the most recent `### Audit v[N]` entry in Audit History.
|
|
90
|
+
|
|
91
|
+
Extract Critical count and Recommendations count from that entry. Map Recommendations count to `--minor` (per R2 CLI contract).
|
|
92
|
+
|
|
93
|
+
Run:
|
|
94
|
+
```bash
|
|
95
|
+
node bin/sf-tools.cjs recommend --source audit --critical N --minor M
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Parse the JSON response.
|
|
99
|
+
|
|
100
|
+
If `action != "run --apply=minor"`:
|
|
101
|
+
```
|
|
102
|
+
Error: --apply=minor requires only Recommendations (found {N} Critical). Run /sf:revise instead.
|
|
103
|
+
```
|
|
104
|
+
Exit 1. No state mutation.
|
|
105
|
+
|
|
106
|
+
### 3.5.c Apply Recommendations via `/sf:revise` Machinery
|
|
107
|
+
|
|
108
|
+
Parse the latest Audit History Recommendations list and extract numbered items as a comma-separated string (e.g. `"2,3,5"` — the sequence numbers as they appear in the Recommendations section).
|
|
109
|
+
|
|
110
|
+
Invoke existing `/sf:revise` machinery passing the numbered target list and `--internal` flag (so `/sf:revise` Step 8 does NOT mutate STATE.md — the caller owns the status transition; status must remain `audited` until the structural-validate gate passes):
|
|
111
|
+
```
|
|
112
|
+
/sf:revise SPEC-XXX "{N,M,K}" --internal
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This reuses `/sf:revise`'s existing per-item commit behavior. Do NOT duplicate revise logic.
|
|
116
|
+
|
|
117
|
+
### 3.5.d Structural Validation Gate
|
|
118
|
+
|
|
119
|
+
Run spec structural validation:
|
|
120
|
+
```bash
|
|
121
|
+
node bin/sf-tools.cjs spec validate SPEC-XXX
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This is the exact gate specified in R2.5: verifies frontmatter parses, required fields present (`id`, `type`, `status`, `priority`), and `## Requirements` heading present. No fallback path.
|
|
125
|
+
|
|
126
|
+
If `spec validate` exits non-zero:
|
|
127
|
+
- Print error output
|
|
128
|
+
- Leave STATE.md status as `audited` (no transition)
|
|
129
|
+
- Exit 1
|
|
130
|
+
- Note: Revise commits remain in git history; user can manually `git revert` or run full `/sf:revise` cycle. STATE.md status is the sole rollback signal.
|
|
131
|
+
|
|
132
|
+
### 3.5.e On Gate Success: Continue to Execution
|
|
133
|
+
|
|
134
|
+
On validation passing: skip Steps 4–7 (audit status check, mode determination, pre-execution summary, model profile, status update) and proceed directly to Step 8 (Spawn Executor Agent) with mode="orchestrated" (or "single" based on the spec's Implementation Tasks section — same logic as Step 4.5).
|
|
135
|
+
|
|
136
|
+
All STATE.md mutations use Read+Write per SPEC-004 (not Bash/awk/sed).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
69
140
|
## Step 4: Check Audit Status
|
|
70
141
|
|
|
71
142
|
**If status is "audited":**
|