specflow-cc 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,55 @@ 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.14.0] - 2026-03-05
9
+
10
+ ### Added
11
+
12
+ - **Context Monitor Hook** — agent-facing context awareness via PostToolUse hook
13
+ - Statusline writes bridge file to `/tmp/claude-ctx-{session}.json`
14
+ - New `hooks/context-monitor.js` reads metrics and injects WARNING (35% remaining) / CRITICAL (25%) warnings into agent context
15
+ - Debounce (5 tool uses between warnings), severity escalation bypasses debounce
16
+ - Integrates with `/sf:pause` for graceful session saves
17
+ - Installer auto-registers the hook in settings.json
18
+
19
+ - **`/sf:health`** — diagnose `.specflow/` directory integrity
20
+ - 13 error codes across 3 severity levels (error, warning, info)
21
+ - Checks: STATE.md integrity, orphaned specs, queue consistency, missing directories, stale execution state
22
+ - `--repair` flag for safe auto-fixes (create missing dirs, regenerate STATE.md, clear stale state)
23
+ - Repair verification: re-runs checks after repair to confirm resolution
24
+
25
+ - **`/sf:validate`** — run validation checklist from specification
26
+ - Executes automated checks (test commands), code verifications (grep/glob), and manual prompts
27
+ - Pass/fail report per checklist item with overall validation status
28
+ - Graceful handling when spec has no validation checklist
29
+
30
+ - **Validation Checklist in spec template** — spec-creator generates `## Validation Checklist` section for medium/large specs
31
+ - 3-5 concrete verification steps with expected outcomes
32
+ - Each item: action + expected result (e.g., "Run `npm test` — all pass")
33
+
34
+ - **Enriched completion summaries** in `/sf:done`
35
+ - New sections: Outcome, Key Files, Patterns Established, Deviations
36
+ - Decisions extracted from both spec content and completion section
37
+
38
+ - **Centralized CLI Tooling** (`bin/sf-tools.cjs`) — single Node.js CLI for SpecFlow operations
39
+ - `spec load <id>` — parse spec file, return frontmatter + sections as JSON
40
+ - `spec list [--status <s>]` — list specs with optional status filter
41
+ - `spec next-id` — next available SPEC-XXX number (checks specs/ + archive/)
42
+ - `queue next` — first actionable spec from queue
43
+ - `state get` / `state set-active <id> <status>` — STATE.md CRUD
44
+ - `resolve-model <agent-type>` — model resolution by profile
45
+ - `verify-structure` — `.specflow/` integrity checks
46
+ - `generate-slug <text>` — URL-safe slug generation
47
+ - Modular architecture: `bin/lib/core.cjs`, `state.cjs`, `spec.cjs`, `config.cjs`, `verify.cjs`
48
+ - 42 tests using Node.js `assert` (no external dependencies)
49
+
50
+ ### Fixed
51
+
52
+ - Parent spec now correctly archived after `/sf:split`
53
+ - `.specflow/` directory excluded from git tracking
54
+
55
+ ---
56
+
8
57
  ## [1.13.0] - 2026-02-11
9
58
 
10
59
  ### Added
package/README.md CHANGED
@@ -200,15 +200,16 @@ If issues are found, `/sf:fix` addresses them. Loop until approved.
200
200
 
201
201
  ---
202
202
 
203
- ### 5. Verify (Optional)
203
+ ### 5. Validate & Verify (Optional)
204
204
 
205
205
  ```
206
+ /sf:validate
206
207
  /sf:verify
207
208
  ```
208
209
 
209
- Automated checks confirm code exists and tests pass. But does it actually *work*?
210
+ **Validate** runs the spec's validation checklist — automated test commands, code checks, and manual verification prompts. Pass/fail per item.
210
211
 
211
- This step walks you through manual verification:
212
+ **Verify** walks you through interactive acceptance testing:
212
213
 
213
214
  - "Can you log in with OAuth?"
214
215
  - "Does the redirect work?"
@@ -216,7 +217,7 @@ This step walks you through manual verification:
216
217
 
217
218
  You confirm each item. If something's broken, the system helps diagnose and creates fix plans.
218
219
 
219
- **Creates:** Verification record
220
+ **Creates:** Validation/verification record
220
221
 
221
222
  ---
222
223
 
@@ -237,12 +238,12 @@ Your spec becomes documentation: why the code exists, what decisions were made,
237
238
  ```
238
239
  /sf:quick (trivial tasks)
239
240
 
240
- /sf:new /sf:audit /sf:run /sf:review /sf:verify /sf:done
241
-
242
- /sf:revise /sf:fix (optional UAT)
243
- (if needed) (if needed)
241
+ /sf:new /sf:audit /sf:run /sf:review /sf:validate → /sf:verify /sf:done
242
+
243
+ /sf:revise /sf:fix (checklist) (optional UAT)
244
+ (if needed) (if needed)
244
245
 
245
- /sf:autopilot — runs the entire flow above automatically
246
+ /sf:autopilot — runs the entire flow above automatically
246
247
  ```
247
248
 
248
249
  **Key principle:** Audits and reviews run in fresh context — no bias from creation.
@@ -312,6 +313,7 @@ Six months later, you can read the spec and understand not just *what* was built
312
313
  | `/sf:run` | Implement specification |
313
314
  | `/sf:review` | Review implementation (fresh context) |
314
315
  | `/sf:fix` | Fix based on review feedback |
316
+ | `/sf:validate` | Run validation checklist from spec |
315
317
  | `/sf:verify` | Interactive user acceptance testing |
316
318
  | `/sf:done` | Complete and archive |
317
319
  | `/sf:autopilot` | Run full lifecycle autonomously |
@@ -396,6 +398,7 @@ before showing interactive options.
396
398
  | `/sf:deps` | Show spec dependencies |
397
399
  | `/sf:pause` | Save session context |
398
400
  | `/sf:resume` | Restore session |
401
+ | `/sf:health` | Diagnose `.specflow/` integrity |
399
402
  | `/sf:help` | Command reference |
400
403
 
401
404
  ---
@@ -481,6 +484,10 @@ Use `max` for maximum quality everywhere, `quality` for critical features, `budg
481
484
  - Use `/sf:split` to decompose into smaller specs
482
485
  - Or let the system auto-decompose during `/sf:run`
483
486
 
487
+ **STATE.md or queue seems corrupted?**
488
+ - Run `/sf:health` to diagnose issues
489
+ - Use `/sf:health --repair` for safe auto-fixes
490
+
484
491
  ---
485
492
 
486
493
  ## Philosophy
@@ -146,8 +146,9 @@ Write to `.specflow/specs/SPEC-XXX.md` using the template structure:
146
146
  4. **Task:** What to do
147
147
  5. **Requirements:** Files, interfaces, deletions
148
148
  6. **Acceptance Criteria:** Specific, measurable
149
- 7. **Constraints:** What NOT to do
150
- 8. **Assumptions:** What you assumed (clearly marked)
149
+ 7. **Validation Checklist** (medium/large specs only): 3-5 concrete verification steps with expected outcomes. Each item = action + expected result. Examples: "Run `npm test` — all pass", "POST /api/users with invalid email — returns 422", "Open settings page — new toggle visible"
150
+ 8. **Constraints:** What NOT to do
151
+ 9. **Assumptions:** What you assumed (clearly marked)
151
152
  - **If `<prior_discussion>` provided:** Decisions from discussion are facts, not assumptions
152
153
 
153
154
  ## Step 5.5: Generate Implementation Tasks (for medium and large specs)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: sf-spec-splitter
3
3
  description: Analyzes large specifications and splits them into manageable sub-specifications with dependencies
4
- tools: Read, Write, Glob, Grep
4
+ tools: Read, Write, Glob, Grep, Bash
5
5
  ---
6
6
 
7
7
  <role>
package/bin/install.js CHANGED
@@ -266,6 +266,26 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
266
266
  console.log(` ${green}✓${reset} Configured statusline`);
267
267
  }
268
268
 
269
+ // Configure context-monitor hook
270
+ const isGlobal = statuslineCommand.includes('$HOME');
271
+ const monitorCommand = isGlobal
272
+ ? 'node "$HOME/.claude/hooks/context-monitor.js"'
273
+ : 'node .claude/hooks/context-monitor.js';
274
+
275
+ if (!settings.hooks) settings.hooks = {};
276
+ if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
277
+
278
+ const hasMonitor = settings.hooks.PostToolUse.some(h =>
279
+ h.command && h.command.includes('context-monitor')
280
+ );
281
+ if (!hasMonitor) {
282
+ settings.hooks.PostToolUse.push({
283
+ type: 'command',
284
+ command: monitorCommand
285
+ });
286
+ console.log(` ${green}✓${reset} Configured context monitor hook`);
287
+ }
288
+
269
289
  writeSettings(settingsPath, settings);
270
290
 
271
291
  console.log(`
@@ -0,0 +1,91 @@
1
+ /**
2
+ * bin/lib/config.cjs — Config reading and model resolution
3
+ *
4
+ * Exports: MODEL_PROFILES, loadConfig(), cmdResolveModel()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const { output, error, safeReadFile } = require('./core.cjs');
11
+
12
+ /**
13
+ * Hardcoded MODEL_PROFILES table.
14
+ * MUST exactly match the profile table in commands/sf/run.md.
15
+ */
16
+ const MODEL_PROFILES = {
17
+ 'spec-creator': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
18
+ 'spec-auditor': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
19
+ 'spec-splitter': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
20
+ 'discusser': { max: 'opus', quality: 'opus', balanced: 'opus', budget: 'sonnet' },
21
+ 'spec-executor': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
22
+ 'spec-executor-orchestrator': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
23
+ 'spec-executor-worker': { max: 'opus', quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
24
+ 'impl-reviewer': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
25
+ 'spec-reviser': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'sonnet' },
26
+ 'researcher': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
27
+ 'codebase-scanner': { max: 'opus', quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
28
+ };
29
+
30
+ /**
31
+ * Load .specflow/config.json. Returns defaults if not found.
32
+ * @param {string} cwd - Working directory
33
+ * @returns {Object} Config object
34
+ */
35
+ function loadConfig(cwd) {
36
+ const configPath = path.join(cwd, '.specflow', 'config.json');
37
+ const content = safeReadFile(configPath);
38
+
39
+ if (!content) {
40
+ return { model_profile: 'balanced' };
41
+ }
42
+
43
+ try {
44
+ const config = JSON.parse(content);
45
+ return {
46
+ model_profile: config.model_profile || 'balanced',
47
+ ...config,
48
+ };
49
+ } catch (e) {
50
+ return { model_profile: 'balanced' };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Resolve model for a given agent type based on current profile.
56
+ * @param {string} cwd - Working directory
57
+ * @param {string} agentType - Agent type (e.g., "spec-executor")
58
+ * @param {boolean} raw - Output raw string
59
+ */
60
+ function cmdResolveModel(cwd, agentType, raw) {
61
+ if (!agentType) {
62
+ error('Missing agent type. Usage: resolve-model <agent-type>');
63
+ }
64
+
65
+ const config = loadConfig(cwd);
66
+ const profile = config.model_profile || 'balanced';
67
+
68
+ const agentProfiles = MODEL_PROFILES[agentType];
69
+
70
+ if (!agentProfiles) {
71
+ error('Unknown agent type: ' + agentType + '. Known types: ' + Object.keys(MODEL_PROFILES).join(', '));
72
+ }
73
+
74
+ const model = agentProfiles[profile];
75
+
76
+ if (!model) {
77
+ error('Unknown profile: ' + profile + '. Known profiles: max, quality, balanced, budget');
78
+ }
79
+
80
+ output({
81
+ agent: agentType,
82
+ profile: profile,
83
+ model: model,
84
+ }, raw, model);
85
+ }
86
+
87
+ module.exports = {
88
+ MODEL_PROFILES,
89
+ loadConfig,
90
+ cmdResolveModel,
91
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * bin/lib/core.cjs — Shared utilities for sf-tools CLI
3
+ *
4
+ * Exports: output(), error(), safeReadFile(), parseFrontmatter(), generateSlug()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Output result to stdout as JSON, or raw string if raw flag is set.
14
+ * @param {*} result - The result object to output
15
+ * @param {boolean} raw - If true, output rawValue as plain string
16
+ * @param {string} [rawValue] - Plain string to output when raw is true
17
+ */
18
+ function output(result, raw, rawValue) {
19
+ if (raw && rawValue !== undefined) {
20
+ process.stdout.write(String(rawValue) + '\n');
21
+ } else {
22
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Output error message to stderr and exit with code 1.
28
+ * @param {string} message - Error message
29
+ */
30
+ function error(message) {
31
+ process.stderr.write('Error: ' + message + '\n');
32
+ process.exit(1);
33
+ }
34
+
35
+ /**
36
+ * Safely read a file, returning its contents as a string or null if not found.
37
+ * @param {string} filePath - Absolute or relative path to the file
38
+ * @returns {string|null} File contents or null
39
+ */
40
+ function safeReadFile(filePath) {
41
+ try {
42
+ return fs.readFileSync(filePath, 'utf8');
43
+ } catch (e) {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Parse YAML frontmatter from markdown content.
50
+ * Expects `---` delimited YAML at the start of the file.
51
+ * Returns simple key: value pairs only (no nested objects, no arrays).
52
+ * All values are returned as strings. No type coercion is performed.
53
+ *
54
+ * @param {string} content - Full file content
55
+ * @returns {{ frontmatter: Object, body: string }}
56
+ */
57
+ function parseFrontmatter(content) {
58
+ if (!content || typeof content !== 'string') {
59
+ return { frontmatter: {}, body: content || '' };
60
+ }
61
+
62
+ const trimmed = content.trimStart();
63
+
64
+ if (!trimmed.startsWith('---')) {
65
+ return { frontmatter: {}, body: content };
66
+ }
67
+
68
+ // Find the closing ---
69
+ const secondDash = trimmed.indexOf('---', 3);
70
+ if (secondDash === -1) {
71
+ return { frontmatter: {}, body: content };
72
+ }
73
+
74
+ const yamlBlock = trimmed.substring(3, secondDash).trim();
75
+ const body = trimmed.substring(secondDash + 3).replace(/^\r?\n/, '');
76
+
77
+ const frontmatter = {};
78
+ const lines = yamlBlock.split('\n');
79
+
80
+ for (const line of lines) {
81
+ const trimmedLine = line.trim();
82
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
83
+
84
+ const colonIndex = trimmedLine.indexOf(':');
85
+ if (colonIndex === -1) continue;
86
+
87
+ const key = trimmedLine.substring(0, colonIndex).trim();
88
+ const value = trimmedLine.substring(colonIndex + 1).trim();
89
+
90
+ // All values returned as strings — no type coercion
91
+ frontmatter[key] = value;
92
+ }
93
+
94
+ return { frontmatter, body };
95
+ }
96
+
97
+ /**
98
+ * Generate a URL-safe slug from text.
99
+ * Lowercase, replace spaces/special chars with hyphens, collapse multiples, trim edges.
100
+ *
101
+ * @param {string} text - Input text
102
+ * @returns {string} URL-safe slug
103
+ */
104
+ function generateSlug(text) {
105
+ if (!text || typeof text !== 'string') return '';
106
+
107
+ return text
108
+ .toLowerCase()
109
+ .replace(/[^a-z0-9]+/g, '-') // replace non-alphanumeric with hyphens
110
+ .replace(/-+/g, '-') // collapse multiple hyphens
111
+ .replace(/^-|-$/g, ''); // trim leading/trailing hyphens
112
+ }
113
+
114
+ module.exports = {
115
+ output,
116
+ error,
117
+ safeReadFile,
118
+ parseFrontmatter,
119
+ generateSlug,
120
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * bin/lib/spec.cjs — Spec operations
3
+ *
4
+ * Exports: cmdSpecLoad(), cmdSpecList(), cmdSpecNextId()
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { output, error, safeReadFile, parseFrontmatter } = require('./core.cjs');
12
+
13
+ /**
14
+ * Load and parse a spec file.
15
+ * @param {string} cwd - Working directory
16
+ * @param {string} id - Spec ID (e.g., "SPEC-007")
17
+ * @param {boolean} raw - Output raw string
18
+ */
19
+ function cmdSpecLoad(cwd, id, raw) {
20
+ if (!id) {
21
+ error('Missing spec ID. Usage: spec load <id>');
22
+ }
23
+
24
+ const specPath = path.join(cwd, '.specflow', 'specs', id + '.md');
25
+ const content = safeReadFile(specPath);
26
+
27
+ if (!content) {
28
+ error('Spec not found: ' + specPath);
29
+ }
30
+
31
+ const parsed = parseFrontmatter(content);
32
+
33
+ output({
34
+ frontmatter: parsed.frontmatter,
35
+ body: parsed.body,
36
+ }, raw, parsed.body);
37
+ }
38
+
39
+ /**
40
+ * List all specs in .specflow/specs/.
41
+ * Extracts id, title, status, complexity, priority from each.
42
+ * Title is parsed from the first # heading in the body.
43
+ * @param {string} cwd - Working directory
44
+ * @param {boolean} raw - Output raw string
45
+ */
46
+ function cmdSpecList(cwd, raw) {
47
+ const specsDir = path.join(cwd, '.specflow', 'specs');
48
+
49
+ let files;
50
+ try {
51
+ files = fs.readdirSync(specsDir).filter(f => f.endsWith('.md')).sort();
52
+ } catch (e) {
53
+ error('Cannot read specs directory: ' + specsDir);
54
+ }
55
+
56
+ const specs = [];
57
+
58
+ for (const file of files) {
59
+ const content = safeReadFile(path.join(specsDir, file));
60
+ if (!content) continue;
61
+
62
+ const parsed = parseFrontmatter(content);
63
+ const fm = parsed.frontmatter;
64
+
65
+ // Extract title from first # heading in body
66
+ let title = '';
67
+ const titleMatch = parsed.body.match(/^#\s+(.+)$/m);
68
+ if (titleMatch) {
69
+ title = titleMatch[1].trim();
70
+ }
71
+
72
+ specs.push({
73
+ id: fm.id || file.replace('.md', ''),
74
+ title: title,
75
+ status: fm.status || '',
76
+ complexity: fm.complexity || '',
77
+ priority: fm.priority || '',
78
+ });
79
+ }
80
+
81
+ output(specs, raw, specs.map(s => s.id).join('\n'));
82
+ }
83
+
84
+ /**
85
+ * Calculate the next available SPEC-XXX number.
86
+ * Scans both .specflow/specs/ and .specflow/archive/ directories.
87
+ * Ignores non-numeric spec IDs (e.g., SPEC-GSD-A).
88
+ * @param {string} cwd - Working directory
89
+ * @param {boolean} raw - Output raw string
90
+ */
91
+ function cmdSpecNextId(cwd, raw) {
92
+ const dirs = [
93
+ path.join(cwd, '.specflow', 'specs'),
94
+ path.join(cwd, '.specflow', 'archive'),
95
+ ];
96
+
97
+ let maxNum = 0;
98
+
99
+ for (const dir of dirs) {
100
+ let files;
101
+ try {
102
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
103
+ } catch (e) {
104
+ continue; // directory may not exist
105
+ }
106
+
107
+ for (const file of files) {
108
+ // Match SPEC-NNN pattern (numeric only)
109
+ const match = file.match(/^SPEC-(\d+)\.md$/);
110
+ if (match) {
111
+ const num = parseInt(match[1], 10);
112
+ if (num > maxNum) maxNum = num;
113
+ }
114
+ }
115
+ }
116
+
117
+ const nextNumber = maxNum + 1;
118
+ const nextId = 'SPEC-' + String(nextNumber).padStart(3, '0');
119
+
120
+ output({
121
+ next_id: nextId,
122
+ next_number: nextNumber,
123
+ }, raw, nextId);
124
+ }
125
+
126
+ module.exports = {
127
+ cmdSpecLoad,
128
+ cmdSpecList,
129
+ cmdSpecNextId,
130
+ };