specflow-cc 1.0.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/bin/install.js ADDED
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SpecFlow installer
5
+ * Copies commands, agents, templates, and hooks to ~/.claude/
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const readline = require('readline');
12
+
13
+ // Colors
14
+ const cyan = '\x1b[36m';
15
+ const green = '\x1b[32m';
16
+ const yellow = '\x1b[33m';
17
+ const dim = '\x1b[2m';
18
+ const reset = '\x1b[0m';
19
+
20
+ // Get version from package.json
21
+ const pkg = require('../package.json');
22
+
23
+ const banner = `
24
+ ${cyan} ███████╗██████╗ ███████╗ ██████╗███████╗██╗ ██████╗ ██╗ ██╗
25
+ ██╔════╝██╔══██╗██╔════╝██╔════╝██╔════╝██║ ██╔═══██╗██║ ██║
26
+ ███████╗██████╔╝█████╗ ██║ █████╗ ██║ ██║ ██║██║ █╗ ██║
27
+ ╚════██║██╔═══╝ ██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║███╗██║
28
+ ███████║██║ ███████╗╚██████╗██║ ███████╗╚██████╔╝╚███╔███╔╝
29
+ ╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝${reset}
30
+
31
+ Spec-Driven Development ${dim}v${pkg.version}${reset}
32
+ Quality-first workflow for Claude Code
33
+ `;
34
+
35
+ // Parse args
36
+ const args = process.argv.slice(2);
37
+ const hasGlobal = args.includes('--global') || args.includes('-g');
38
+ const hasLocal = args.includes('--local') || args.includes('-l');
39
+ const hasHelp = args.includes('--help') || args.includes('-h');
40
+ const forceStatusline = args.includes('--force-statusline');
41
+
42
+ console.log(banner);
43
+
44
+ // Show help if requested
45
+ if (hasHelp) {
46
+ console.log(` ${yellow}Usage:${reset} npx specflow-cc [options]
47
+
48
+ ${yellow}Options:${reset}
49
+ ${cyan}-g, --global${reset} Install globally (to ~/.claude)
50
+ ${cyan}-l, --local${reset} Install locally (to ./.claude)
51
+ ${cyan}--force-statusline${reset} Replace existing statusline config
52
+ ${cyan}-h, --help${reset} Show this help message
53
+
54
+ ${yellow}Examples:${reset}
55
+ ${dim}# Install to default ~/.claude directory${reset}
56
+ npx specflow-cc --global
57
+
58
+ ${dim}# Install to current project only${reset}
59
+ npx specflow-cc --local
60
+ `);
61
+ process.exit(0);
62
+ }
63
+
64
+ /**
65
+ * Read and parse settings.json
66
+ */
67
+ function readSettings(settingsPath) {
68
+ if (fs.existsSync(settingsPath)) {
69
+ try {
70
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
71
+ } catch (e) {
72
+ return {};
73
+ }
74
+ }
75
+ return {};
76
+ }
77
+
78
+ /**
79
+ * Write settings.json with proper formatting
80
+ */
81
+ function writeSettings(settingsPath, settings) {
82
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
83
+ }
84
+
85
+ /**
86
+ * Recursively copy directory, replacing paths in .md files
87
+ */
88
+ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
89
+ if (fs.existsSync(destDir)) {
90
+ fs.rmSync(destDir, { recursive: true });
91
+ }
92
+ fs.mkdirSync(destDir, { recursive: true });
93
+
94
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
95
+
96
+ for (const entry of entries) {
97
+ const srcPath = path.join(srcDir, entry.name);
98
+ const destPath = path.join(destDir, entry.name);
99
+
100
+ if (entry.isDirectory()) {
101
+ copyWithPathReplacement(srcPath, destPath, pathPrefix);
102
+ } else if (entry.name.endsWith('.md')) {
103
+ // Replace ~/.claude/specflow-cc/ with the appropriate prefix
104
+ let content = fs.readFileSync(srcPath, 'utf8');
105
+ content = content.replace(/~\/\.claude\/specflow-cc\//g, pathPrefix);
106
+ fs.writeFileSync(destPath, content);
107
+ } else {
108
+ fs.copyFileSync(srcPath, destPath);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Verify a directory exists and contains files
115
+ */
116
+ function verifyInstalled(dirPath, description) {
117
+ if (!fs.existsSync(dirPath)) {
118
+ console.error(` ${yellow}✗${reset} Failed to install ${description}`);
119
+ return false;
120
+ }
121
+ try {
122
+ const entries = fs.readdirSync(dirPath);
123
+ if (entries.length === 0) {
124
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: empty`);
125
+ return false;
126
+ }
127
+ } catch (e) {
128
+ console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
129
+ return false;
130
+ }
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Install to the specified directory
136
+ */
137
+ function install(isGlobal) {
138
+ const src = path.join(__dirname, '..');
139
+ const claudeDir = isGlobal
140
+ ? path.join(os.homedir(), '.claude')
141
+ : path.join(process.cwd(), '.claude');
142
+
143
+ const specflowDir = path.join(claudeDir, 'specflow-cc');
144
+ const locationLabel = isGlobal ? '~/.claude' : './.claude';
145
+
146
+ // Path prefix for file references
147
+ const pathPrefix = isGlobal ? '~/.claude/specflow-cc/' : './.claude/specflow-cc/';
148
+
149
+ console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
150
+
151
+ const failures = [];
152
+
153
+ // Ensure directories exist
154
+ fs.mkdirSync(specflowDir, { recursive: true });
155
+ fs.mkdirSync(path.join(claudeDir, 'commands'), { recursive: true });
156
+
157
+ // Copy commands/sf
158
+ const commandsSrc = path.join(src, 'commands', 'sf');
159
+ const commandsDest = path.join(claudeDir, 'commands', 'sf');
160
+ if (fs.existsSync(commandsSrc)) {
161
+ copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix);
162
+ if (verifyInstalled(commandsDest, 'commands/sf')) {
163
+ console.log(` ${green}✓${reset} Installed commands/sf`);
164
+ } else {
165
+ failures.push('commands/sf');
166
+ }
167
+ }
168
+
169
+ // Copy agents to specflow-cc/agents (and also to ~/.claude/agents for subagent discovery)
170
+ const agentsSrc = path.join(src, 'agents');
171
+ if (fs.existsSync(agentsSrc)) {
172
+ // Copy to specflow-cc/agents
173
+ const agentsDest = path.join(specflowDir, 'agents');
174
+ copyWithPathReplacement(agentsSrc, agentsDest, pathPrefix);
175
+
176
+ // Also copy to ~/.claude/agents for subagent_type discovery
177
+ const globalAgentsDest = path.join(claudeDir, 'agents');
178
+ fs.mkdirSync(globalAgentsDest, { recursive: true });
179
+
180
+ // Remove old sf-* agents, then copy new ones
181
+ if (fs.existsSync(globalAgentsDest)) {
182
+ for (const file of fs.readdirSync(globalAgentsDest)) {
183
+ if (file.startsWith('sf-') && file.endsWith('.md')) {
184
+ fs.unlinkSync(path.join(globalAgentsDest, file));
185
+ }
186
+ }
187
+ }
188
+
189
+ for (const entry of fs.readdirSync(agentsSrc)) {
190
+ if (entry.endsWith('.md')) {
191
+ let content = fs.readFileSync(path.join(agentsSrc, entry), 'utf8');
192
+ content = content.replace(/~\/\.claude\/specflow-cc\//g, pathPrefix);
193
+ fs.writeFileSync(path.join(globalAgentsDest, `sf-${entry}`), content);
194
+ }
195
+ }
196
+
197
+ if (verifyInstalled(agentsDest, 'agents')) {
198
+ console.log(` ${green}✓${reset} Installed agents`);
199
+ } else {
200
+ failures.push('agents');
201
+ }
202
+ }
203
+
204
+ // Copy templates
205
+ const templatesSrc = path.join(src, 'templates');
206
+ if (fs.existsSync(templatesSrc)) {
207
+ const templatesDest = path.join(specflowDir, 'templates');
208
+ copyWithPathReplacement(templatesSrc, templatesDest, pathPrefix);
209
+ if (verifyInstalled(templatesDest, 'templates')) {
210
+ console.log(` ${green}✓${reset} Installed templates`);
211
+ } else {
212
+ failures.push('templates');
213
+ }
214
+ }
215
+
216
+ // Copy hooks
217
+ const hooksSrc = path.join(src, 'hooks');
218
+ if (fs.existsSync(hooksSrc)) {
219
+ const hooksDest = path.join(claudeDir, 'hooks');
220
+ fs.mkdirSync(hooksDest, { recursive: true });
221
+ for (const entry of fs.readdirSync(hooksSrc)) {
222
+ fs.copyFileSync(path.join(hooksSrc, entry), path.join(hooksDest, entry));
223
+ }
224
+ if (verifyInstalled(hooksDest, 'hooks')) {
225
+ console.log(` ${green}✓${reset} Installed hooks`);
226
+ } else {
227
+ failures.push('hooks');
228
+ }
229
+ }
230
+
231
+ // Copy CHANGELOG.md
232
+ const changelogSrc = path.join(src, 'CHANGELOG.md');
233
+ if (fs.existsSync(changelogSrc)) {
234
+ fs.copyFileSync(changelogSrc, path.join(specflowDir, 'CHANGELOG.md'));
235
+ console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
236
+ }
237
+
238
+ // Write VERSION file
239
+ fs.writeFileSync(path.join(specflowDir, 'VERSION'), pkg.version);
240
+ console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
241
+
242
+ if (failures.length > 0) {
243
+ console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
244
+ process.exit(1);
245
+ }
246
+
247
+ // Configure statusline in settings.json
248
+ const settingsPath = path.join(claudeDir, 'settings.json');
249
+ const settings = readSettings(settingsPath);
250
+ const statuslineCommand = isGlobal
251
+ ? 'node "$HOME/.claude/hooks/statusline.js"'
252
+ : 'node .claude/hooks/statusline.js';
253
+
254
+ return { settingsPath, settings, statuslineCommand };
255
+ }
256
+
257
+ /**
258
+ * Apply statusline config and print completion message
259
+ */
260
+ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline) {
261
+ if (shouldInstallStatusline) {
262
+ settings.statusLine = {
263
+ type: 'command',
264
+ command: statuslineCommand
265
+ };
266
+ console.log(` ${green}✓${reset} Configured statusline`);
267
+ }
268
+
269
+ writeSettings(settingsPath, settings);
270
+
271
+ console.log(`
272
+ ${green}Done!${reset} Launch Claude Code and run ${cyan}/sf help${reset}.
273
+
274
+ ${yellow}Quick start:${reset}
275
+ /sf init - Initialize project
276
+ /sf new - Create specification
277
+ /sf help - Show all commands
278
+ `);
279
+ }
280
+
281
+ /**
282
+ * Handle statusline configuration
283
+ */
284
+ function handleStatusline(settings, isInteractive, callback) {
285
+ const hasExisting = settings.statusLine != null;
286
+
287
+ if (!hasExisting) {
288
+ callback(true);
289
+ return;
290
+ }
291
+
292
+ if (forceStatusline) {
293
+ callback(true);
294
+ return;
295
+ }
296
+
297
+ if (!isInteractive) {
298
+ console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
299
+ console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
300
+ callback(false);
301
+ return;
302
+ }
303
+
304
+ const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
305
+
306
+ const rl = readline.createInterface({
307
+ input: process.stdin,
308
+ output: process.stdout
309
+ });
310
+
311
+ console.log(`
312
+ ${yellow}⚠${reset} Existing statusline detected
313
+
314
+ Your current statusline:
315
+ ${dim}command: ${existingCmd}${reset}
316
+
317
+ SpecFlow includes a statusline showing:
318
+ • Model name
319
+ • Current spec status [SF: SPEC-XXX status]
320
+ • Context window usage (color-coded)
321
+
322
+ ${cyan}1${reset}) Keep existing
323
+ ${cyan}2${reset}) Replace with SpecFlow statusline
324
+ `);
325
+
326
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
327
+ rl.close();
328
+ const choice = answer.trim() || '1';
329
+ callback(choice === '2');
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Prompt for install location
335
+ */
336
+ function promptLocation() {
337
+ if (!process.stdin.isTTY) {
338
+ console.log(` ${yellow}Non-interactive terminal, defaulting to global install${reset}\n`);
339
+ const { settingsPath, settings, statuslineCommand } = install(true);
340
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
341
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
342
+ });
343
+ return;
344
+ }
345
+
346
+ const rl = readline.createInterface({
347
+ input: process.stdin,
348
+ output: process.stdout
349
+ });
350
+
351
+ let answered = false;
352
+
353
+ rl.on('close', () => {
354
+ if (!answered) {
355
+ answered = true;
356
+ console.log(`\n ${yellow}Input closed, defaulting to global install${reset}\n`);
357
+ const { settingsPath, settings, statuslineCommand } = install(true);
358
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
359
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
360
+ });
361
+ }
362
+ });
363
+
364
+ console.log(` ${yellow}Where would you like to install?${reset}
365
+
366
+ ${cyan}1${reset}) Global ${dim}(~/.claude)${reset} - available in all projects
367
+ ${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
368
+ `);
369
+
370
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
371
+ answered = true;
372
+ rl.close();
373
+ const choice = answer.trim() || '1';
374
+ const isGlobal = choice !== '2';
375
+ const { settingsPath, settings, statuslineCommand } = install(isGlobal);
376
+ handleStatusline(settings, true, (shouldInstallStatusline) => {
377
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
378
+ });
379
+ });
380
+ }
381
+
382
+ // Main
383
+ if (hasGlobal && hasLocal) {
384
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
385
+ process.exit(1);
386
+ } else if (hasGlobal) {
387
+ const { settingsPath, settings, statuslineCommand } = install(true);
388
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
389
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
390
+ });
391
+ } else if (hasLocal) {
392
+ const { settingsPath, settings, statuslineCommand } = install(false);
393
+ handleStatusline(settings, false, (shouldInstallStatusline) => {
394
+ finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline);
395
+ });
396
+ } else {
397
+ promptLocation();
398
+ }
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: sf:audit
3
+ description: Audit the active specification in a fresh context
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Bash
8
+ - Glob
9
+ - Grep
10
+ - Task
11
+ ---
12
+
13
+ <purpose>
14
+ Audit the active specification using a fresh context subagent. The auditor evaluates clarity, completeness, testability, scope, and feasibility without bias from the creation process.
15
+ </purpose>
16
+
17
+ <context>
18
+ @.specflow/STATE.md
19
+ @.specflow/PROJECT.md
20
+ @~/.claude/specflow-cc/agents/spec-auditor.md
21
+ </context>
22
+
23
+ <workflow>
24
+
25
+ ## Step 1: Verify Initialization
26
+
27
+ ```bash
28
+ [ -d .specflow ] && echo "OK" || echo "NOT_INITIALIZED"
29
+ ```
30
+
31
+ **If NOT_INITIALIZED:**
32
+ ```
33
+ SpecFlow not initialized.
34
+
35
+ Run `/sf init` first.
36
+ ```
37
+ Exit.
38
+
39
+ ## Step 2: Get Active Specification
40
+
41
+ Read `.specflow/STATE.md` and extract Active Specification.
42
+
43
+ **If no active specification:**
44
+ ```
45
+ No active specification to audit.
46
+
47
+ Run `/sf new "task description"` to create one.
48
+ ```
49
+ Exit.
50
+
51
+ ## Step 3: Load Specification
52
+
53
+ Read the active spec file: `.specflow/specs/SPEC-XXX.md`
54
+
55
+ **If status is not 'draft' or 'revision_requested':**
56
+ ```
57
+ Specification SPEC-XXX is already audited (status: {status}).
58
+
59
+ Use `/sf run` to implement or `/sf status` to see current state.
60
+ ```
61
+ Exit.
62
+
63
+ ## Step 4: Spawn Auditor Agent
64
+
65
+ Launch the spec-auditor subagent with fresh context:
66
+
67
+ ```
68
+ Task(prompt="
69
+ <specification>
70
+ @.specflow/specs/SPEC-XXX.md
71
+ </specification>
72
+
73
+ <project_context>
74
+ @.specflow/PROJECT.md
75
+ </project_context>
76
+
77
+ Audit this specification following the spec-auditor agent instructions.
78
+ Do NOT read any conversation history — audit with fresh eyes.
79
+ ", subagent_type="sf-spec-auditor", description="Audit specification")
80
+ ```
81
+
82
+ ## Step 5: Handle Agent Response
83
+
84
+ The agent will:
85
+ 1. Evaluate 5 quality dimensions
86
+ 2. Categorize issues (critical vs recommendations)
87
+ 3. Append audit to spec's Audit History
88
+ 4. Update STATE.md
89
+ 5. Return structured result
90
+
91
+ ## Step 6: Display Result
92
+
93
+ ### If APPROVED:
94
+
95
+ ```
96
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97
+ AUDIT PASSED
98
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99
+
100
+ **Specification:** SPEC-XXX
101
+ **Status:** APPROVED
102
+
103
+ {Comment from auditor}
104
+
105
+ ---
106
+
107
+ ## Next Step
108
+
109
+ `/sf run` — implement specification
110
+ ```
111
+
112
+ ### If NEEDS_REVISION:
113
+
114
+ ```
115
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
116
+ AUDIT: NEEDS REVISION
117
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118
+
119
+ **Specification:** SPEC-XXX
120
+ **Status:** NEEDS_REVISION
121
+
122
+ ### Critical Issues
123
+
124
+ 1. [Issue 1]
125
+ 2. [Issue 2]
126
+
127
+ ### Recommendations
128
+
129
+ 3. [Recommendation 1]
130
+
131
+ ---
132
+
133
+ ## Next Step
134
+
135
+ `/sf revise` — address critical issues
136
+
137
+ Options:
138
+ - `/sf revise all` — apply all feedback
139
+ - `/sf revise 1,2` — fix specific issues
140
+ - `/sf revise [instructions]` — custom changes
141
+ ```
142
+
143
+ </workflow>
144
+
145
+ <fallback>
146
+
147
+ **If agent spawning fails**, execute inline:
148
+
149
+ ## Inline Audit
150
+
151
+ ### Check Quality Dimensions
152
+
153
+ Read spec and evaluate:
154
+
155
+ **Clarity:**
156
+ - [ ] Title clearly describes task
157
+ - [ ] Context explains why
158
+ - [ ] No vague terms
159
+
160
+ **Completeness:**
161
+ - [ ] All files listed
162
+ - [ ] Interfaces defined (if needed)
163
+ - [ ] Deletions specified (if refactor)
164
+
165
+ **Testability:**
166
+ - [ ] Each criterion measurable
167
+ - [ ] Concrete success conditions
168
+
169
+ **Scope:**
170
+ - [ ] Clear boundaries
171
+ - [ ] No scope creep
172
+
173
+ **Feasibility:**
174
+ - [ ] Technically sound
175
+ - [ ] Reasonable assumptions
176
+
177
+ ### Record Audit
178
+
179
+ Get audit version number:
180
+
181
+ ```bash
182
+ AUDIT_COUNT=$(grep -c "### Audit v" .specflow/specs/SPEC-XXX.md 2>/dev/null || echo 0)
183
+ NEXT_VERSION=$((AUDIT_COUNT + 1))
184
+ ```
185
+
186
+ Append to spec's Audit History:
187
+
188
+ ```markdown
189
+ ### Audit v{N} ({date} {time})
190
+ **Status:** [APPROVED | NEEDS_REVISION]
191
+
192
+ {Issues and recommendations}
193
+ ```
194
+
195
+ ### Update STATE.md
196
+
197
+ - If APPROVED: Status → "audited", Next Step → "/sf run"
198
+ - If NEEDS_REVISION: Status → "revision_requested", Next Step → "/sf revise"
199
+
200
+ </fallback>
201
+
202
+ <success_criteria>
203
+ - [ ] Active specification identified
204
+ - [ ] Fresh context audit performed
205
+ - [ ] All 5 dimensions evaluated
206
+ - [ ] Issues categorized
207
+ - [ ] Audit recorded in spec's Audit History
208
+ - [ ] STATE.md updated
209
+ - [ ] Clear next step provided
210
+ </success_criteria>