specflow-cc 1.13.0 → 1.14.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 +49 -0
- package/README.md +16 -9
- package/agents/spec-creator.md +3 -2
- package/agents/spec-splitter.md +1 -1
- package/bin/install.js +20 -0
- package/bin/lib/config.cjs +91 -0
- package/bin/lib/core.cjs +120 -0
- package/bin/lib/spec.cjs +130 -0
- package/bin/lib/state.cjs +241 -0
- package/bin/lib/verify.cjs +117 -0
- package/bin/sf-tools.cjs +103 -0
- package/commands/sf/done.md +21 -2
- package/commands/sf/health.md +220 -0
- package/commands/sf/help.md +2 -0
- package/commands/sf/split.md +14 -3
- package/commands/sf/validate.md +154 -0
- package/hooks/context-monitor.js +121 -0
- package/hooks/statusline.js +17 -0
- package/package.json +1 -1
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.1] - 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
|
-
|
|
210
|
+
**Validate** runs the spec's validation checklist — automated test commands, code checks, and manual verification prompts. Pass/fail per item.
|
|
210
211
|
|
|
211
|
-
|
|
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:**
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
package/agents/spec-creator.md
CHANGED
|
@@ -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. **
|
|
150
|
-
8. **
|
|
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)
|
package/agents/spec-splitter.md
CHANGED
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
|
+
};
|
package/bin/lib/core.cjs
ADDED
|
@@ -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
|
+
};
|
package/bin/lib/spec.cjs
ADDED
|
@@ -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
|
+
};
|