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 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
@@ -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:
@@ -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
@@ -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:
@@ -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`
@@ -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
  ```
@@ -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:fix` — apply minor suggestions first ({N} items)
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:
@@ -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 will:
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:
@@ -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":**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specflow-cc",
3
- "version": "1.21.0",
3
+ "version": "1.22.1",
4
4
  "description": "Spec-driven development system for Claude Code — quality-first workflow with explicit audit cycles",
5
5
  "bin": {
6
6
  "specflow-cc": "bin/install.js"