agileflow 2.95.2 → 2.96.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/api-routes.js +605 -0
  4. package/lib/api-server.js +260 -0
  5. package/lib/claude-cli-bridge.js +221 -0
  6. package/lib/dashboard-protocol.js +541 -0
  7. package/lib/dashboard-server.js +1601 -0
  8. package/lib/drivers/claude-driver.ts +310 -0
  9. package/lib/drivers/codex-driver.ts +454 -0
  10. package/lib/drivers/driver-manager.ts +158 -0
  11. package/lib/drivers/gemini-driver.ts +485 -0
  12. package/lib/drivers/index.ts +17 -0
  13. package/lib/flag-detection.js +350 -0
  14. package/lib/git-operations.js +267 -0
  15. package/lib/lock-file.js +144 -0
  16. package/lib/merge-operations.js +959 -0
  17. package/lib/protocol/driver.ts +360 -0
  18. package/lib/protocol/index.ts +12 -0
  19. package/lib/protocol/ir.ts +271 -0
  20. package/lib/session-display.js +330 -0
  21. package/lib/worktree-operations.js +221 -0
  22. package/package.json +2 -2
  23. package/scripts/agileflow-welcome.js +272 -24
  24. package/scripts/api-server-runner.js +177 -0
  25. package/scripts/archive-completed-stories.sh +22 -0
  26. package/scripts/automation-run-due.js +126 -0
  27. package/scripts/backfill-ideation-status.js +124 -0
  28. package/scripts/claude-tmux.sh +62 -1
  29. package/scripts/context-loader.js +292 -0
  30. package/scripts/dashboard-serve.js +323 -0
  31. package/scripts/lib/automation-registry.js +544 -0
  32. package/scripts/lib/automation-runner.js +476 -0
  33. package/scripts/lib/concurrency-limiter.js +513 -0
  34. package/scripts/lib/configure-features.js +46 -0
  35. package/scripts/lib/context-formatter.js +61 -0
  36. package/scripts/lib/damage-control-utils.js +29 -4
  37. package/scripts/lib/hook-metrics.js +324 -0
  38. package/scripts/lib/ideation-index.js +1196 -0
  39. package/scripts/lib/process-cleanup.js +359 -0
  40. package/scripts/lib/quality-gates.js +574 -0
  41. package/scripts/lib/status-task-bridge.js +522 -0
  42. package/scripts/lib/sync-ideation-status.js +292 -0
  43. package/scripts/lib/task-registry-cache.js +490 -0
  44. package/scripts/lib/task-registry.js +1181 -0
  45. package/scripts/migrate-ideation-index.js +515 -0
  46. package/scripts/precompact-context.sh +104 -0
  47. package/scripts/ralph-loop.js +2 -2
  48. package/scripts/session-manager.js +363 -2770
  49. package/scripts/spawn-parallel.js +45 -9
  50. package/src/core/agents/api-validator.md +180 -0
  51. package/src/core/agents/api.md +2 -0
  52. package/src/core/agents/code-reviewer.md +289 -0
  53. package/src/core/agents/configuration/damage-control.md +17 -0
  54. package/src/core/agents/database.md +2 -0
  55. package/src/core/agents/error-analyzer.md +203 -0
  56. package/src/core/agents/logic-analyzer-edge.md +171 -0
  57. package/src/core/agents/logic-analyzer-flow.md +254 -0
  58. package/src/core/agents/logic-analyzer-invariant.md +207 -0
  59. package/src/core/agents/logic-analyzer-race.md +267 -0
  60. package/src/core/agents/logic-analyzer-type.md +218 -0
  61. package/src/core/agents/logic-consensus.md +256 -0
  62. package/src/core/agents/orchestrator.md +89 -1
  63. package/src/core/agents/schema-validator.md +451 -0
  64. package/src/core/agents/team-coordinator.md +328 -0
  65. package/src/core/agents/ui-validator.md +328 -0
  66. package/src/core/agents/ui.md +2 -0
  67. package/src/core/commands/api.md +267 -0
  68. package/src/core/commands/automate.md +415 -0
  69. package/src/core/commands/babysit.md +290 -9
  70. package/src/core/commands/ideate/history.md +403 -0
  71. package/src/core/commands/{ideate.md → ideate/new.md} +244 -34
  72. package/src/core/commands/logic/audit.md +368 -0
  73. package/src/core/commands/roadmap/analyze.md +1 -1
  74. package/src/core/experts/documentation/expertise.yaml +29 -2
  75. package/src/core/templates/CONTEXT.md.example +49 -0
  76. package/src/core/templates/claude-settings.advanced.example.json +4 -0
  77. package/tools/cli/commands/serve.js +456 -0
  78. package/tools/cli/installers/core/installer.js +7 -2
  79. package/tools/cli/installers/ide/claude-code.js +85 -0
  80. package/tools/cli/lib/content-injector.js +27 -1
  81. package/tools/cli/lib/ui.js +26 -57
@@ -0,0 +1,456 @@
1
+ /**
2
+ * AgileFlow CLI - Serve Command
3
+ *
4
+ * Starts a WebSocket server for the AgileFlow Dashboard to connect to.
5
+ * Enables real-time communication between the dashboard and Claude Code.
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const path = require('node:path');
10
+ const { displayLogo, displaySection, success, warning, info } = require('../lib/ui');
11
+
12
+ module.exports = {
13
+ name: 'serve',
14
+ description: 'Start WebSocket server for AgileFlow Dashboard',
15
+ options: [
16
+ ['-p, --port <number>', 'Port to listen on (default: 8765)'],
17
+ ['-H, --host <host>', 'Host to bind to (default: 0.0.0.0)'],
18
+ ['-k, --api-key <key>', 'API key for authentication'],
19
+ ['--require-auth', 'Require API key for connections'],
20
+ ['--no-tunnel', 'Disable automatic tunnel (local only)'],
21
+ ['--tunnel-provider <provider>', 'Tunnel provider: cloudflared (default) or ngrok'],
22
+ ],
23
+ action: async options => {
24
+ try {
25
+ // Import server modules
26
+ const {
27
+ createDashboardServer,
28
+ startDashboardServer,
29
+ stopDashboardServer,
30
+ } = require('../../../lib/dashboard-server');
31
+ const {
32
+ createNotification,
33
+ createTextDelta,
34
+ createToolStart,
35
+ createToolResult,
36
+ } = require('../../../lib/dashboard-protocol');
37
+ const { execSync } = require('child_process');
38
+ const readline = require('readline');
39
+
40
+ let port = parseInt(options.port, 10) || 8765;
41
+ const host = options.host || '0.0.0.0';
42
+
43
+ // Check if port is in use and handle it
44
+ port = await handlePortConflict(port, readline);
45
+
46
+ const serverOptions = {
47
+ port,
48
+ host,
49
+ apiKey: options.apiKey || null,
50
+ requireAuth: options.requireAuth || !!options.apiKey,
51
+ };
52
+
53
+ // Display banner
54
+ printBanner();
55
+ console.log('Starting server...\n');
56
+
57
+ // Create server
58
+ const server = createDashboardServer(serverOptions);
59
+
60
+ // Set up event handlers
61
+ setupEventHandlers(server, { createTextDelta, createToolStart, createToolResult });
62
+
63
+ // Start server
64
+ const { wsUrl } = await startDashboardServer(server);
65
+
66
+ // Start tunnel automatically (unless --no-tunnel)
67
+ let tunnelUrl = null;
68
+ if (options.tunnel !== false) {
69
+ const provider = options.tunnelProvider || 'cloudflared';
70
+ console.log(chalk.dim(` Starting ${provider} tunnel...`));
71
+ tunnelUrl = await startTunnel(serverOptions.port, provider);
72
+ }
73
+
74
+ console.log('─────────────────────────────────────────────────────────────');
75
+ console.log('');
76
+ if (tunnelUrl) {
77
+ console.log(chalk.green(' Ready!') + ' Connect your dashboard to:');
78
+ console.log('');
79
+ console.log(chalk.cyan.bold(` ${tunnelUrl}`));
80
+ console.log('');
81
+ console.log(chalk.dim(` Dashboard: https://dashboard.agileflow.projectquestorg.com`));
82
+ console.log(chalk.dim(` Paste the URL above into the WebSocket URL field.`));
83
+ } else {
84
+ console.log(chalk.green(' Ready!') + ' Local connection:');
85
+ console.log(chalk.cyan(` ${wsUrl}`));
86
+ console.log('');
87
+ console.log(chalk.yellow(' ⚠️ For cloud dashboard, run with tunnel:'));
88
+ console.log(chalk.dim(' npx agileflow serve'));
89
+ console.log(chalk.dim(' (tunnels are enabled by default)'));
90
+ }
91
+ console.log('');
92
+ if (serverOptions.apiKey) {
93
+ console.log(chalk.dim(` API Key: ${serverOptions.apiKey.slice(0, 8)}...`));
94
+ console.log('');
95
+ }
96
+ console.log(chalk.dim(' Press Ctrl+C to stop.'));
97
+ console.log('');
98
+ console.log('─────────────────────────────────────────────────────────────');
99
+ console.log('');
100
+
101
+ // Handle shutdown
102
+ const shutdown = async () => {
103
+ console.log('\nShutting down...');
104
+ await stopDashboardServer(server);
105
+ process.exit(0);
106
+ };
107
+
108
+ process.on('SIGINT', shutdown);
109
+ process.on('SIGTERM', shutdown);
110
+ } catch (err) {
111
+ console.error(chalk.red('Error:'), err.message);
112
+ if (process.env.DEBUG) {
113
+ console.error(err.stack);
114
+ }
115
+ process.exit(1);
116
+ }
117
+ },
118
+ };
119
+
120
+ function printBanner() {
121
+ console.log(`
122
+ ${chalk.hex('#e8683a')('╔═══════════════════════════════════════════════════════════╗')}
123
+ ${chalk.hex('#e8683a')('║')} ${chalk.hex('#e8683a')('║')}
124
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('█████╗ ██████╗ ██╗██╗ ███████╗███████╗██╗ ')}${chalk.hex('#e8683a')('║')}
125
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('██╔══██╗██╔════╝ ██║██║ ██╔════╝██╔════╝██║ ')}${chalk.hex('#e8683a')('║')}
126
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('███████║██║ ███╗██║██║ █████╗ █████╗ ██║ ')}${chalk.hex('#e8683a')('║')}
127
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('██╔══██║██║ ██║██║██║ ██╔══╝ ██╔══╝ ██║ ')}${chalk.hex('#e8683a')('║')}
128
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('██║ ██║╚██████╔╝██║███████╗███████╗██║ ███████╗ ')}${chalk.hex('#e8683a')('║')}
129
+ ${chalk.hex('#e8683a')('║')} ${chalk.bold.hex('#e8683a')('╚═╝ ╚═╝ ╚═════╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚══════╝ ')}${chalk.hex('#e8683a')('║')}
130
+ ${chalk.hex('#e8683a')('║')} ${chalk.hex('#e8683a')('║')}
131
+ ${chalk.hex('#e8683a')('║')} ${chalk.white('Dashboard WebSocket Server')} ${chalk.hex('#e8683a')('║')}
132
+ ${chalk.hex('#e8683a')('║')} ${chalk.hex('#e8683a')('║')}
133
+ ${chalk.hex('#e8683a')('╚═══════════════════════════════════════════════════════════╝')}
134
+ `);
135
+ }
136
+
137
+ function setupEventHandlers(server, protocol) {
138
+ const { createTextDelta, createToolStart, createToolResult } = protocol;
139
+
140
+ // Session events
141
+ server.on('session:connected', (sessionId, session) => {
142
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Session connected: ${chalk.cyan(sessionId)}`);
143
+ });
144
+
145
+ server.on('session:disconnected', sessionId => {
146
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Session disconnected: ${chalk.yellow(sessionId)}`);
147
+ });
148
+
149
+ // User message handler - uses real Claude CLI
150
+ server.on('user:message', async (session, content) => {
151
+ console.log(
152
+ chalk.dim(`[${new Date().toISOString()}]`) +
153
+ ` Message from ${chalk.cyan(session.id)}: ${chalk.white(content.slice(0, 50))}...`
154
+ );
155
+
156
+ // Use real Claude CLI
157
+ await handleClaudeMessage(session, content, { createTextDelta, createToolStart, createToolResult });
158
+ });
159
+
160
+ // Cancel handler
161
+ server.on('user:cancel', session => {
162
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Cancel from ${chalk.yellow(session.id)}`);
163
+ // Note: To properly cancel, we'd need to track the bridge per session
164
+ // For now, this just logs the cancel request
165
+ session.setState('idle');
166
+ });
167
+
168
+ // Refresh handlers
169
+ server.on('refresh:tasks', session => {
170
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Task refresh for ${chalk.cyan(session.id)}`);
171
+ });
172
+
173
+ server.on('refresh:status', session => {
174
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Status refresh for ${chalk.cyan(session.id)}`);
175
+ });
176
+ }
177
+
178
+ async function handleClaudeMessage(session, content, protocol) {
179
+ const { createTextDelta, createToolStart, createToolResult } = protocol;
180
+ const { createClaudeBridge } = require('../../../lib/claude-cli-bridge');
181
+
182
+ // Set session state to thinking
183
+ session.setState('thinking');
184
+
185
+ try {
186
+ const bridge = createClaudeBridge({
187
+ cwd: process.cwd(),
188
+
189
+ onText: (text, done) => {
190
+ // Always send text deltas (even empty ones with done=true to signal completion)
191
+ if (text) {
192
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Text: ${chalk.green(text.slice(0, 50))}${text.length > 50 ? '...' : ''}`);
193
+ }
194
+ session.send(createTextDelta(text || '', done));
195
+ if (done) {
196
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + chalk.green(' Response complete'));
197
+ session.setState('idle');
198
+ }
199
+ },
200
+
201
+ onToolStart: (id, name, input) => {
202
+ console.log(chalk.dim(`[${new Date().toISOString()}]`) + ` Tool: ${chalk.yellow(name)}`);
203
+ session.send(createToolStart(id, name, input));
204
+ },
205
+
206
+ onToolResult: (id, output, isError, toolName) => {
207
+ session.send(createToolResult(id, output, isError ? output : null));
208
+ },
209
+
210
+ onError: (error) => {
211
+ console.error(chalk.red(`[${new Date().toISOString()}]`) + ` Error: ${error}`);
212
+ session.send(createTextDelta(`\n\nError: ${error}`, true));
213
+ session.setState('error');
214
+ },
215
+
216
+ onComplete: (fullResponse) => {
217
+ session.addMessage('assistant', fullResponse);
218
+ session.setState('idle');
219
+ }
220
+ });
221
+
222
+ // Send the message to Claude
223
+ await bridge.sendMessage(content);
224
+
225
+ } catch (err) {
226
+ console.error(chalk.red('Claude error:'), err.message);
227
+ session.send(createTextDelta(`Error: ${err.message}`, true));
228
+ session.setState('error');
229
+ }
230
+ }
231
+
232
+ async function startTunnel(port, provider = 'cloudflared') {
233
+ const { spawn, exec } = require('child_process');
234
+
235
+ // Try cloudflared first (free, no signup needed)
236
+ if (provider === 'cloudflared') {
237
+ const url = await startCloudflaredTunnel(port, spawn, exec);
238
+ if (url) return url;
239
+ // Fall back to ngrok if cloudflared fails
240
+ console.log(chalk.dim(' Trying ngrok as fallback...'));
241
+ }
242
+
243
+ // Try ngrok
244
+ return startNgrokTunnel(port, exec);
245
+ }
246
+
247
+ async function startCloudflaredTunnel(port, spawn, exec) {
248
+ return new Promise((resolve) => {
249
+ // Check if cloudflared is installed
250
+ exec('which cloudflared', (error) => {
251
+ if (error) {
252
+ console.log(chalk.yellow(' cloudflared not found.'));
253
+ console.log(chalk.dim(' Install: brew install cloudflared (mac) or sudo apt install cloudflared (linux)'));
254
+ console.log(chalk.dim(' Or download from: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
255
+ resolve(null);
256
+ return;
257
+ }
258
+
259
+ // Start cloudflared tunnel (quick tunnel, no account needed)
260
+ const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
261
+ stdio: ['ignore', 'pipe', 'pipe']
262
+ });
263
+
264
+ let resolved = false;
265
+
266
+ const handleOutput = (data) => {
267
+ const output = data.toString();
268
+ // Look for the tunnel URL in output
269
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
270
+ if (urlMatch && !resolved) {
271
+ resolved = true;
272
+ const httpsUrl = urlMatch[0];
273
+ const wssUrl = httpsUrl.replace('https://', 'wss://');
274
+ console.log(chalk.green(' ✓ Tunnel ready'));
275
+ resolve(wssUrl);
276
+ }
277
+ };
278
+
279
+ tunnel.stdout.on('data', handleOutput);
280
+ tunnel.stderr.on('data', handleOutput);
281
+
282
+ tunnel.on('error', (err) => {
283
+ if (!resolved) {
284
+ console.log(chalk.yellow(` cloudflared error: ${err.message}`));
285
+ resolve(null);
286
+ }
287
+ });
288
+
289
+ // Timeout after 15 seconds
290
+ setTimeout(() => {
291
+ if (!resolved) {
292
+ console.log(chalk.yellow(' cloudflared tunnel timeout'));
293
+ resolve(null);
294
+ }
295
+ }, 15000);
296
+ });
297
+ });
298
+ }
299
+
300
+ async function startNgrokTunnel(port, exec) {
301
+ return new Promise((resolve) => {
302
+ exec('which ngrok', (error) => {
303
+ if (error) {
304
+ console.log(chalk.yellow(' ngrok not found either.'));
305
+ console.log('');
306
+ console.log(chalk.yellow(' To enable cloud dashboard access, install a tunnel:'));
307
+ console.log(chalk.dim(' brew install cloudflared # Recommended (free, no signup)'));
308
+ console.log(chalk.dim(' npm install -g ngrok # Alternative (requires signup)'));
309
+ resolve(null);
310
+ return;
311
+ }
312
+
313
+ const ngrok = exec(`ngrok http ${port} --log stdout`, { encoding: 'utf8' });
314
+
315
+ let resolved = false;
316
+
317
+ ngrok.stdout.on('data', data => {
318
+ const urlMatch = data.match(/url=(https?:\/\/[^\s]+)/);
319
+ if (urlMatch && !resolved) {
320
+ resolved = true;
321
+ const tunnelUrl = urlMatch[1].replace('https://', 'wss://').replace('http://', 'ws://');
322
+ console.log(chalk.green(' ✓ ngrok tunnel ready'));
323
+ resolve(tunnelUrl);
324
+ }
325
+ });
326
+
327
+ ngrok.stderr.on('data', data => {
328
+ if (!resolved && data.includes('error')) {
329
+ console.error(chalk.red(' ngrok error:'), data);
330
+ }
331
+ });
332
+
333
+ setTimeout(() => {
334
+ if (!resolved) {
335
+ console.log(chalk.yellow(' ngrok tunnel timeout'));
336
+ resolve(null);
337
+ }
338
+ }, 10000);
339
+ });
340
+ });
341
+ }
342
+
343
+ function sleep(ms) {
344
+ return new Promise(resolve => setTimeout(resolve, ms));
345
+ }
346
+
347
+ /**
348
+ * Check if port is in use and handle conflict
349
+ */
350
+ async function handlePortConflict(port, readline) {
351
+ const { execSync } = require('child_process');
352
+ const net = require('net');
353
+
354
+ // Check if port is in use
355
+ const isPortInUse = await new Promise((resolve) => {
356
+ const server = net.createServer();
357
+ server.once('error', (err) => {
358
+ if (err.code === 'EADDRINUSE') {
359
+ resolve(true);
360
+ } else {
361
+ resolve(false);
362
+ }
363
+ });
364
+ server.once('listening', () => {
365
+ server.close();
366
+ resolve(false);
367
+ });
368
+ server.listen(port);
369
+ });
370
+
371
+ if (!isPortInUse) {
372
+ return port;
373
+ }
374
+
375
+ // Port is in use - find out what's using it
376
+ let processInfo = '';
377
+ try {
378
+ processInfo = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf-8' }).trim();
379
+ } catch (e) {
380
+ // lsof might not be available or port check failed
381
+ }
382
+
383
+ console.log(chalk.yellow(`\n Port ${port} is already in use.`));
384
+
385
+ if (processInfo) {
386
+ console.log(chalk.dim(` Process ID: ${processInfo}`));
387
+ }
388
+
389
+ // Ask user what to do
390
+ const rl = readline.createInterface({
391
+ input: process.stdin,
392
+ output: process.stdout
393
+ });
394
+
395
+ return new Promise((resolve) => {
396
+ console.log('');
397
+ console.log(' Options:');
398
+ console.log(chalk.cyan(' [k]') + ' Kill existing process and use this port');
399
+ console.log(chalk.cyan(' [n]') + ' Use next available port');
400
+ console.log(chalk.cyan(' [q]') + ' Quit');
401
+ console.log('');
402
+
403
+ rl.question(' Your choice (k/n/q): ', async (answer) => {
404
+ rl.close();
405
+ const choice = answer.toLowerCase().trim();
406
+
407
+ if (choice === 'k' && processInfo) {
408
+ // Kill the process
409
+ try {
410
+ execSync(`kill ${processInfo}`, { encoding: 'utf-8' });
411
+ console.log(chalk.green(` Killed process ${processInfo}`));
412
+ await sleep(500); // Give it a moment to release the port
413
+ resolve(port);
414
+ } catch (e) {
415
+ console.log(chalk.red(` Failed to kill process: ${e.message}`));
416
+ console.log(chalk.dim(' Trying next available port instead...'));
417
+ resolve(await findNextAvailablePort(port));
418
+ }
419
+ } else if (choice === 'n') {
420
+ const newPort = await findNextAvailablePort(port);
421
+ console.log(chalk.green(` Using port ${newPort}`));
422
+ resolve(newPort);
423
+ } else {
424
+ console.log(chalk.dim(' Exiting.'));
425
+ process.exit(0);
426
+ }
427
+ });
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Find next available port starting from given port
433
+ */
434
+ async function findNextAvailablePort(startPort) {
435
+ const net = require('net');
436
+ let port = startPort + 1;
437
+
438
+ while (port < startPort + 100) {
439
+ const available = await new Promise((resolve) => {
440
+ const server = net.createServer();
441
+ server.once('error', () => resolve(false));
442
+ server.once('listening', () => {
443
+ server.close();
444
+ resolve(true);
445
+ });
446
+ server.listen(port);
447
+ });
448
+
449
+ if (available) {
450
+ return port;
451
+ }
452
+ port++;
453
+ }
454
+
455
+ throw new Error('Could not find an available port');
456
+ }
@@ -139,6 +139,7 @@ class Installer {
139
139
  fileOps,
140
140
  force: effectiveForce,
141
141
  timestamp,
142
+ docsFolder: docsFolder || 'docs',
142
143
  });
143
144
  } else {
144
145
  // Fallback: copy from old structure (commands, agents, skills at root)
@@ -149,6 +150,7 @@ class Installer {
149
150
  fileOps,
150
151
  force: effectiveForce,
151
152
  timestamp,
153
+ docsFolder: docsFolder || 'docs',
152
154
  });
153
155
  }
154
156
 
@@ -295,8 +297,9 @@ class Installer {
295
297
  * @param {string} source - Source file path
296
298
  * @param {string} dest - Destination file path
297
299
  * @param {string} agileflowFolder - AgileFlow folder name
300
+ * @param {string} docsFolder - Docs folder name (default: 'docs')
298
301
  */
299
- async copyFileWithReplacements(source, dest, agileflowFolder) {
302
+ async copyFileWithReplacements(source, dest, agileflowFolder, docsFolder = 'docs') {
300
303
  const ext = path.extname(source).toLowerCase();
301
304
 
302
305
  if (TEXT_EXTENSIONS.has(ext)) {
@@ -306,6 +309,7 @@ class Installer {
306
309
  content = injectContent(content, {
307
310
  coreDir: this.coreDir,
308
311
  agileflowFolder,
312
+ docsFolder,
309
313
  version: this.version,
310
314
  });
311
315
 
@@ -326,7 +330,7 @@ class Installer {
326
330
  * @param {Object} policy - Copy policy
327
331
  */
328
332
  async copyFileWithPolicy(source, dest, agileflowFolder, policy) {
329
- const { agileflowDir, cfgDir, fileIndex, fileOps, force, timestamp } = policy;
333
+ const { agileflowDir, cfgDir, fileIndex, fileOps, force, timestamp, docsFolder = 'docs' } = policy;
330
334
 
331
335
  const relativePath = toPosixPath(path.relative(agileflowDir, dest));
332
336
  const maybeRecord = fileIndex.files[relativePath];
@@ -342,6 +346,7 @@ class Installer {
342
346
  content = injectContent(content, {
343
347
  coreDir: this.coreDir,
344
348
  agileflowFolder,
349
+ docsFolder,
345
350
  version: this.version,
346
351
  });
347
352
  newContent = content;
@@ -56,6 +56,9 @@ class ClaudeCodeSetup extends BaseIdeSetup {
56
56
  // Claude Code specific: Setup damage control hooks
57
57
  await this.setupDamageControl(projectDir, agileflowDir, ideDir, options);
58
58
 
59
+ // Claude Code specific: Setup SessionStart hooks (welcome, archive, context-loader)
60
+ await this.setupSessionStartHooks(projectDir, agileflowDir, ideDir, options);
61
+
59
62
  return result;
60
63
  }
61
64
 
@@ -93,6 +96,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
93
96
  }
94
97
  }
95
98
 
99
+ // Copy lib/damage-control-utils.js (required by hook scripts via ../lib/damage-control-utils)
100
+ const libSource = path.join(agileflowDir, 'scripts', 'lib', 'damage-control-utils.js');
101
+ const libTarget = path.join(claudeDir, 'hooks', 'lib', 'damage-control-utils.js');
102
+ if (fs.existsSync(libSource)) {
103
+ await this.ensureDir(path.dirname(libTarget));
104
+ await fs.copy(libSource, libTarget);
105
+ }
106
+
96
107
  // Copy patterns.yaml (preserve existing)
97
108
  const patternsSource = path.join(damageControlSource, 'patterns.yaml');
98
109
  const patternsTarget = path.join(damageControlTarget, 'patterns.yaml');
@@ -195,6 +206,80 @@ class ClaudeCodeSetup extends BaseIdeSetup {
195
206
  // Write settings
196
207
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
197
208
  }
209
+
210
+ /**
211
+ * Setup SessionStart hooks (welcome, archive, context-loader)
212
+ * @param {string} projectDir - Project directory
213
+ * @param {string} agileflowDir - AgileFlow installation directory
214
+ * @param {string} claudeDir - .claude directory path
215
+ * @param {Object} options - Setup options
216
+ */
217
+ async setupSessionStartHooks(projectDir, agileflowDir, claudeDir, options = {}) {
218
+ const settingsPath = path.join(claudeDir, 'settings.json');
219
+ let settings = {};
220
+
221
+ // Load existing settings
222
+ if (fs.existsSync(settingsPath)) {
223
+ try {
224
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
225
+ } catch (e) {
226
+ settings = {};
227
+ }
228
+ }
229
+
230
+ // Initialize hooks structure
231
+ if (!settings.hooks) settings.hooks = {};
232
+ if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
233
+
234
+ // Define SessionStart hooks
235
+ const sessionStartHooks = [
236
+ {
237
+ type: 'command',
238
+ command: 'node $CLAUDE_PROJECT_DIR/.agileflow/scripts/agileflow-welcome.js 2>/dev/null || true',
239
+ timeout: 10000,
240
+ },
241
+ {
242
+ type: 'command',
243
+ command:
244
+ 'bash $CLAUDE_PROJECT_DIR/.agileflow/scripts/archive-completed-stories.sh --quiet 2>/dev/null || true',
245
+ timeout: 10000,
246
+ },
247
+ {
248
+ type: 'command',
249
+ command: 'node $CLAUDE_PROJECT_DIR/.agileflow/scripts/context-loader.js 2>/dev/null || true',
250
+ timeout: 5000,
251
+ },
252
+ ];
253
+
254
+ // Check if SessionStart hooks already exist
255
+ const existingEntry = settings.hooks.SessionStart.find(
256
+ h => h.matcher === '' || h.matcher === undefined
257
+ );
258
+
259
+ if (existingEntry) {
260
+ // Merge hooks - add any missing
261
+ if (!existingEntry.hooks) existingEntry.hooks = [];
262
+
263
+ for (const newHook of sessionStartHooks) {
264
+ const alreadyExists = existingEntry.hooks.some(
265
+ h => h.command && h.command.includes(newHook.command.split('/').pop().split(' ')[0])
266
+ );
267
+ if (!alreadyExists) {
268
+ existingEntry.hooks.push(newHook);
269
+ }
270
+ }
271
+ } else {
272
+ // Add new entry
273
+ settings.hooks.SessionStart.push({
274
+ matcher: '',
275
+ hooks: sessionStartHooks,
276
+ });
277
+ }
278
+
279
+ // Write settings
280
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
281
+ console.log(chalk.dim(` - SessionStart hooks: welcome, archive, context-loader`));
282
+ }
198
283
  }
199
284
 
200
285
  module.exports = { ClaudeCodeSetup };
@@ -23,7 +23,12 @@
23
23
  *
24
24
  * FOLDER REFERENCES:
25
25
  * {agileflow_folder} - Name of the agileflow folder (e.g., .agileflow)
26
+ * {docs_folder} - Name of the docs folder (e.g., docs, agileflow-docs)
26
27
  * {project-root} - Project root reference
28
+ *
29
+ * PATH INJECTION:
30
+ * When docsFolder is not 'docs', all path references like `docs/` are replaced
31
+ * with the actual folder name (e.g., `agileflow-docs/`).
27
32
  */
28
33
 
29
34
  const fs = require('fs');
@@ -473,11 +478,12 @@ function clearPreserveRulesCache() {
473
478
  * @param {Object} context - Context for replacements
474
479
  * @param {string} context.coreDir - Path to core directory (commands/, agents/, skills/)
475
480
  * @param {string} context.agileflowFolder - AgileFlow folder name
481
+ * @param {string} context.docsFolder - Docs folder name (default: 'docs')
476
482
  * @param {string} context.version - AgileFlow version
477
483
  * @returns {string} Content with all placeholders replaced
478
484
  */
479
485
  function injectContent(content, context = {}) {
480
- const { coreDir, agileflowFolder = '.agileflow', version = 'unknown' } = context;
486
+ const { coreDir, agileflowFolder = '.agileflow', docsFolder = 'docs', version = 'unknown' } = context;
481
487
 
482
488
  let result = content;
483
489
 
@@ -502,6 +508,10 @@ function injectContent(content, context = {}) {
502
508
  'agileflow_folder',
503
509
  agileflowFolder
504
510
  ).sanitized;
511
+ const safeDocsFolder = validatePlaceholderValue(
512
+ 'docs_folder',
513
+ docsFolder
514
+ ).sanitized;
505
515
 
506
516
  // Replace count placeholders (both formats: {{X}} and <!-- {{X}} -->)
507
517
  result = result.replace(/\{\{COMMAND_COUNT\}\}/g, String(safeCommandCount));
@@ -593,8 +603,22 @@ function injectContent(content, context = {}) {
593
603
 
594
604
  // Replace folder placeholders with sanitized values
595
605
  result = result.replace(/\{agileflow_folder\}/g, safeAgileflowFolder);
606
+ result = result.replace(/\{docs_folder\}/g, safeDocsFolder);
596
607
  result = result.replace(/\{project-root\}/g, '{project-root}'); // Keep as-is for runtime
597
608
 
609
+ // Replace docs/ path references with actual folder if different from default
610
+ // This ensures all path references point to the correct folder
611
+ // Pattern matches: `docs/`, "docs/", 'docs/' but NOT word boundaries like "documents/"
612
+ if (safeDocsFolder !== 'docs') {
613
+ // Replace in code/path contexts: `docs/xxx`, "docs/xxx", 'docs/xxx'
614
+ result = result.replace(/`docs\//g, `\`${safeDocsFolder}/`);
615
+ result = result.replace(/"docs\//g, `"${safeDocsFolder}/`);
616
+ result = result.replace(/'docs\//g, `'${safeDocsFolder}/`);
617
+ // Replace standalone path references like: docs/00-meta, docs/09-agents
618
+ // Must be followed by a path component (letter, number, or dash)
619
+ result = result.replace(/\bdocs\/([0-9a-zA-Z_-])/g, `${safeDocsFolder}/$1`);
620
+ }
621
+
598
622
  return result;
599
623
  }
600
624
 
@@ -690,6 +714,7 @@ function hasPlaceholders(content) {
690
714
  /\{\{QUALITY_GATE_PRIORITIES\}\}/,
691
715
  /\{\{RULES:\w+\}\}/,
692
716
  /\{agileflow_folder\}/,
717
+ /\{docs_folder\}/,
693
718
  ];
694
719
 
695
720
  return patterns.some(pattern => pattern.test(content));
@@ -735,6 +760,7 @@ function getPlaceholderDocs() {
735
760
  },
736
761
  folders: {
737
762
  '{agileflow_folder}': 'Name of the AgileFlow folder',
763
+ '{docs_folder}': 'Name of the docs folder (docs/ paths auto-replaced when different)',
738
764
  '{project-root}': 'Project root reference (kept as-is)',
739
765
  },
740
766
  };