agileflow 3.0.2 → 3.2.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 (116) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +58 -86
  3. package/lib/dashboard-automations.js +130 -0
  4. package/lib/dashboard-git.js +254 -0
  5. package/lib/dashboard-inbox.js +64 -0
  6. package/lib/dashboard-protocol.js +1 -0
  7. package/lib/dashboard-server.js +114 -924
  8. package/lib/dashboard-session.js +136 -0
  9. package/lib/dashboard-status.js +72 -0
  10. package/lib/dashboard-terminal.js +354 -0
  11. package/lib/dashboard-websocket.js +88 -0
  12. package/lib/drivers/codex-driver.ts +4 -4
  13. package/lib/feedback.js +9 -2
  14. package/lib/lazy-require.js +59 -0
  15. package/lib/logger.js +106 -0
  16. package/package.json +4 -2
  17. package/scripts/agileflow-configure.js +14 -2
  18. package/scripts/agileflow-welcome.js +450 -459
  19. package/scripts/claude-tmux.sh +113 -5
  20. package/scripts/context-loader.js +4 -9
  21. package/scripts/lib/command-prereqs.js +280 -0
  22. package/scripts/lib/configure-detect.js +92 -2
  23. package/scripts/lib/configure-features.js +411 -1
  24. package/scripts/lib/context-formatter.js +468 -233
  25. package/scripts/lib/context-loader.js +27 -15
  26. package/scripts/lib/damage-control-utils.js +8 -1
  27. package/scripts/lib/feature-catalog.js +321 -0
  28. package/scripts/lib/portable-tasks-cli.js +274 -0
  29. package/scripts/lib/portable-tasks.js +479 -0
  30. package/scripts/lib/signal-detectors.js +1 -1
  31. package/scripts/lib/team-events.js +86 -1
  32. package/scripts/obtain-context.js +28 -4
  33. package/scripts/smart-detect.js +17 -0
  34. package/scripts/strip-ai-attribution.js +63 -0
  35. package/scripts/team-manager.js +90 -0
  36. package/scripts/welcome-deferred.js +437 -0
  37. package/src/core/agents/legal-analyzer-a11y.md +110 -0
  38. package/src/core/agents/legal-analyzer-ai.md +117 -0
  39. package/src/core/agents/legal-analyzer-consumer.md +108 -0
  40. package/src/core/agents/legal-analyzer-content.md +113 -0
  41. package/src/core/agents/legal-analyzer-international.md +115 -0
  42. package/src/core/agents/legal-analyzer-licensing.md +115 -0
  43. package/src/core/agents/legal-analyzer-privacy.md +108 -0
  44. package/src/core/agents/legal-analyzer-security.md +112 -0
  45. package/src/core/agents/legal-analyzer-terms.md +111 -0
  46. package/src/core/agents/legal-consensus.md +242 -0
  47. package/src/core/agents/perf-analyzer-assets.md +174 -0
  48. package/src/core/agents/perf-analyzer-bundle.md +165 -0
  49. package/src/core/agents/perf-analyzer-caching.md +160 -0
  50. package/src/core/agents/perf-analyzer-compute.md +165 -0
  51. package/src/core/agents/perf-analyzer-memory.md +182 -0
  52. package/src/core/agents/perf-analyzer-network.md +157 -0
  53. package/src/core/agents/perf-analyzer-queries.md +155 -0
  54. package/src/core/agents/perf-analyzer-rendering.md +156 -0
  55. package/src/core/agents/perf-consensus.md +280 -0
  56. package/src/core/agents/security-analyzer-api.md +199 -0
  57. package/src/core/agents/security-analyzer-auth.md +160 -0
  58. package/src/core/agents/security-analyzer-authz.md +168 -0
  59. package/src/core/agents/security-analyzer-deps.md +147 -0
  60. package/src/core/agents/security-analyzer-infra.md +176 -0
  61. package/src/core/agents/security-analyzer-injection.md +148 -0
  62. package/src/core/agents/security-analyzer-input.md +191 -0
  63. package/src/core/agents/security-analyzer-secrets.md +175 -0
  64. package/src/core/agents/security-consensus.md +276 -0
  65. package/src/core/agents/team-lead.md +50 -13
  66. package/src/core/agents/test-analyzer-assertions.md +181 -0
  67. package/src/core/agents/test-analyzer-coverage.md +183 -0
  68. package/src/core/agents/test-analyzer-fragility.md +185 -0
  69. package/src/core/agents/test-analyzer-integration.md +155 -0
  70. package/src/core/agents/test-analyzer-maintenance.md +173 -0
  71. package/src/core/agents/test-analyzer-mocking.md +178 -0
  72. package/src/core/agents/test-analyzer-patterns.md +189 -0
  73. package/src/core/agents/test-analyzer-structure.md +177 -0
  74. package/src/core/agents/test-consensus.md +294 -0
  75. package/src/core/commands/audit/legal.md +446 -0
  76. package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
  77. package/src/core/commands/audit/performance.md +443 -0
  78. package/src/core/commands/audit/security.md +443 -0
  79. package/src/core/commands/audit/test.md +442 -0
  80. package/src/core/commands/babysit.md +505 -463
  81. package/src/core/commands/configure.md +18 -33
  82. package/src/core/commands/research/ask.md +42 -9
  83. package/src/core/commands/research/import.md +14 -8
  84. package/src/core/commands/research/list.md +17 -16
  85. package/src/core/commands/research/synthesize.md +8 -8
  86. package/src/core/commands/research/view.md +28 -4
  87. package/src/core/commands/team/start.md +36 -7
  88. package/src/core/commands/team/stop.md +5 -2
  89. package/src/core/commands/whats-new.md +2 -2
  90. package/src/core/experts/devops/expertise.yaml +13 -2
  91. package/src/core/experts/documentation/expertise.yaml +26 -4
  92. package/src/core/profiles/COMPARISON.md +170 -0
  93. package/src/core/profiles/README.md +178 -0
  94. package/src/core/profiles/claude-code.yaml +111 -0
  95. package/src/core/profiles/codex.yaml +103 -0
  96. package/src/core/profiles/cursor.yaml +134 -0
  97. package/src/core/profiles/examples.js +250 -0
  98. package/src/core/profiles/loader.js +235 -0
  99. package/src/core/profiles/windsurf.yaml +159 -0
  100. package/src/core/teams/logic-audit.json +6 -0
  101. package/src/core/teams/perf-audit.json +71 -0
  102. package/src/core/teams/security-audit.json +71 -0
  103. package/src/core/teams/test-audit.json +71 -0
  104. package/src/core/templates/command-prerequisites.yaml +169 -0
  105. package/src/core/templates/damage-control-patterns.yaml +9 -0
  106. package/tools/cli/installers/ide/_base-ide.js +33 -3
  107. package/tools/cli/installers/ide/claude-code.js +2 -67
  108. package/tools/cli/installers/ide/codex.js +9 -9
  109. package/tools/cli/installers/ide/cursor.js +165 -4
  110. package/tools/cli/installers/ide/windsurf.js +237 -6
  111. package/tools/cli/lib/content-transformer.js +234 -9
  112. package/tools/cli/lib/docs-setup.js +1 -1
  113. package/tools/cli/lib/ide-generator.js +357 -0
  114. package/tools/cli/lib/ide-registry.js +2 -2
  115. package/scripts/tmux-task-name.sh +0 -75
  116. package/scripts/tmux-task-watcher.sh +0 -177
@@ -23,6 +23,7 @@ const fs = require('fs');
23
23
  const path = require('path');
24
24
  const { detectLifecyclePhase, getRelevantPhases } = require('./lib/lifecycle-detector');
25
25
  const { runDetectorsForPhases } = require('./lib/signal-detectors');
26
+ const { buildCatalogWithStatus } = require('./lib/feature-catalog');
26
27
 
27
28
  let safeReadJSON, safeWriteJSON, tryOptional;
28
29
  try {
@@ -134,6 +135,8 @@ function extractSignals(prefetched, sessionState, metadata) {
134
135
  coverage: fs.existsSync('coverage/coverage-summary.json'),
135
136
  playwright: fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js'),
136
137
  screenshots: fs.existsSync('screenshots'),
138
+ browserQaSpecs: fs.existsSync('.agileflow/ui-review/specs'),
139
+ browserQaRuns: fs.existsSync('.agileflow/ui-review/runs'),
137
140
  ciConfig:
138
141
  fs.existsSync('.github/workflows') ||
139
142
  fs.existsSync('.gitlab-ci.yml') ||
@@ -251,6 +254,7 @@ function analyze(prefetched, sessionState, metadata) {
251
254
  detected_at: new Date().toISOString(),
252
255
  lifecycle_phase: 'unknown',
253
256
  recommendations: { immediate: [], available: [], auto_enabled: {} },
257
+ feature_catalog: [],
254
258
  signals_summary: {},
255
259
  disabled: true,
256
260
  };
@@ -276,6 +280,14 @@ function analyze(prefetched, sessionState, metadata) {
276
280
  // Auto-enabled features (existing babysit modes)
277
281
  const autoEnabled = detectAutoModes(signals);
278
282
 
283
+ // Build feature catalog with dynamic status
284
+ const featureCatalog = buildCatalogWithStatus(
285
+ signals,
286
+ { immediate, available },
287
+ autoEnabled,
288
+ metadata
289
+ );
290
+
279
291
  // Build signals summary
280
292
  const signalsSummary = {
281
293
  story: signals.story
@@ -300,6 +312,7 @@ function analyze(prefetched, sessionState, metadata) {
300
312
  available,
301
313
  auto_enabled: autoEnabled,
302
314
  },
315
+ feature_catalog: featureCatalog,
303
316
  signals_summary: signalsSummary,
304
317
  };
305
318
  }
@@ -329,10 +342,14 @@ function detectAutoModes(signals) {
329
342
  // Coverage mode: coverage data exists
330
343
  const coverageMode = !!files.coverage;
331
344
 
345
+ // Browser QA mode: agentic browser testing enabled with specs
346
+ const browserQaMode = !!files.browserQaSpecs;
347
+
332
348
  return {
333
349
  loop_mode: loopMode,
334
350
  visual_mode: visualMode,
335
351
  coverage_mode: coverageMode,
352
+ browser_qa_mode: browserQaMode,
336
353
  };
337
354
  }
338
355
 
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strip-ai-attribution.js - PreToolUse hook for Bash tool
4
+ *
5
+ * Blocks git commit commands that contain AI attribution patterns
6
+ * (Co-Authored-By, "Generated with Claude", noreply@anthropic.com, etc.)
7
+ *
8
+ * Exit codes:
9
+ * 0 - Allow command
10
+ * 2 - Block command (AI attribution detected)
11
+ *
12
+ * Usage: Configured as PreToolUse hook in .claude/settings.json
13
+ */
14
+
15
+ let input = '';
16
+ process.stdin.setEncoding('utf8');
17
+ process.stdin.on('data', chunk => {
18
+ input += chunk;
19
+ });
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ const data = JSON.parse(input);
23
+ const command = data.tool_input?.command || '';
24
+
25
+ // Only check git commit commands
26
+ if (!/git\s+commit/i.test(command)) {
27
+ process.exit(0);
28
+ }
29
+
30
+ // AI attribution patterns (case-insensitive)
31
+ const patterns = [
32
+ /Co-Authored-By:/i,
33
+ /noreply@anthropic\.com/i,
34
+ /noreply@openai\.com/i,
35
+ /noreply@google\.com/i,
36
+ /Generated with \[?Claude/i,
37
+ /Generated by Claude/i,
38
+ /Generated with \[?GPT/i,
39
+ /Generated by GPT/i,
40
+ /Generated by AI/i,
41
+ /\u{1F916}/u, // Robot emoji
42
+ ];
43
+
44
+ for (const pattern of patterns) {
45
+ if (pattern.test(command)) {
46
+ process.stderr.write('[BLOCKED] AI attribution detected in commit message\n');
47
+ process.stderr.write(
48
+ 'Remove Co-Authored-By, "Generated with", or AI mentions from your commit.\n'
49
+ );
50
+ process.stderr.write('Retry with a clean conventional commit message.\n');
51
+ process.exit(2);
52
+ }
53
+ }
54
+
55
+ process.exit(0);
56
+ } catch {
57
+ // Fail open on parse errors
58
+ process.exit(0);
59
+ }
60
+ });
61
+
62
+ // Fail open on timeout
63
+ setTimeout(() => process.exit(0), 4000);
@@ -128,16 +128,46 @@ function listTemplates(rootDir) {
128
128
  return { ok: true, templates };
129
129
  }
130
130
 
131
+ /**
132
+ * Validate template name to prevent path traversal.
133
+ * Only allows alphanumeric characters, hyphens, and underscores.
134
+ */
135
+ function validateTemplateName(name) {
136
+ if (typeof name !== 'string' || name.length === 0 || name.length > 255) {
137
+ return { valid: false, error: 'Template name must be 1-255 characters' };
138
+ }
139
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
140
+ return {
141
+ valid: false,
142
+ error: 'Template name must contain only alphanumeric characters, hyphens, and underscores',
143
+ };
144
+ }
145
+ return { valid: true };
146
+ }
147
+
131
148
  /**
132
149
  * Get a specific template by name
133
150
  */
134
151
  function getTemplate(rootDir, name) {
152
+ const validation = validateTemplateName(name);
153
+ if (!validation.valid) {
154
+ return { ok: false, error: validation.error };
155
+ }
156
+
135
157
  const teamsDir = getTeamsDir(rootDir);
136
158
  if (!teamsDir) {
137
159
  return { ok: false, error: 'No teams directory found' };
138
160
  }
139
161
 
140
162
  const filePath = path.join(teamsDir, `${name}.json`);
163
+
164
+ // Defense-in-depth: verify resolved path stays within teams directory
165
+ const resolvedPath = path.resolve(filePath);
166
+ const resolvedTeamsDir = path.resolve(teamsDir);
167
+ if (!resolvedPath.startsWith(resolvedTeamsDir + path.sep)) {
168
+ return { ok: false, error: 'Invalid template path' };
169
+ }
170
+
141
171
  if (!fs.existsSync(filePath)) {
142
172
  return { ok: false, error: `Template "${name}" not found` };
143
173
  }
@@ -172,6 +202,64 @@ function buildNativeTeamPayload(template, templateName) {
172
202
  };
173
203
  }
174
204
 
205
+ /**
206
+ * Build a rich prompt for a teammate from template data.
207
+ * Used when spawning teammates via the Task tool to give them
208
+ * full context about their role, quality gates, and project state.
209
+ *
210
+ * @param {object} teammate - Teammate entry from template (agent, role, domain, description, instructions)
211
+ * @param {object} template - Full team template (for quality_gates context)
212
+ * @returns {string} Formatted prompt string
213
+ */
214
+ function buildTeammatePrompt(teammate, template) {
215
+ const parts = [];
216
+
217
+ // Role and domain header
218
+ parts.push(`## Role: ${teammate.role || 'teammate'} (${teammate.domain || 'general'})`);
219
+ parts.push('');
220
+
221
+ // Instructions - prefer explicit instructions, fall back to description, then auto-generate
222
+ if (teammate.instructions) {
223
+ parts.push(teammate.instructions);
224
+ } else if (teammate.description) {
225
+ parts.push(teammate.description);
226
+ } else {
227
+ parts.push(
228
+ `You are the ${teammate.role || 'teammate'} agent responsible for the ${teammate.domain || 'general'} domain.`
229
+ );
230
+ }
231
+ parts.push('');
232
+
233
+ // Quality gate awareness
234
+ if (template && template.quality_gates) {
235
+ const gates = template.quality_gates;
236
+ const requirements = [];
237
+
238
+ if (gates.teammate_idle) {
239
+ if (gates.teammate_idle.tests) requirements.push('tests must pass');
240
+ if (gates.teammate_idle.lint) requirements.push('linting must pass');
241
+ if (gates.teammate_idle.types) requirements.push('type checking must pass');
242
+ }
243
+ if (gates.task_completed && gates.task_completed.require_validator_approval) {
244
+ requirements.push('validator approval required');
245
+ }
246
+
247
+ if (requirements.length > 0) {
248
+ parts.push('## Quality Gates');
249
+ parts.push(`Before marking work complete: ${requirements.join(', ')}.`);
250
+ parts.push('');
251
+ }
252
+ }
253
+
254
+ // Project context pointers
255
+ parts.push('## Context');
256
+ parts.push('- Read CLAUDE.md for project conventions');
257
+ parts.push('- Check `docs/09-agents/status.json` for current work items and team state');
258
+ parts.push('');
259
+
260
+ return parts.join('\n');
261
+ }
262
+
175
263
  /**
176
264
  * Start a team from a template.
177
265
  * When native Agent Teams is enabled, builds a TeamCreate-compatible payload.
@@ -458,6 +546,8 @@ module.exports = {
458
546
  stopTeam,
459
547
  getTeamsDir,
460
548
  buildNativeTeamPayload,
549
+ buildTeammatePrompt,
550
+ validateTemplateName,
461
551
  };
462
552
 
463
553
  // Run CLI if invoked directly
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * welcome-deferred.js - Background post-table operations for SessionStart
5
+ *
6
+ * Spawned by agileflow-welcome.js after the table is displayed.
7
+ * Runs with stdio: 'ignore' (detached background process).
8
+ *
9
+ * Handles non-blocking housekeeping tasks:
10
+ * - npm update check (with cache write to session-state.json)
11
+ * - Session health check
12
+ * - Duplicate Claude process detection
13
+ * - Story claiming cleanup
14
+ * - File tracking cleanup
15
+ * - Epic completion check
16
+ * - Ideation sync
17
+ * - Scheduled automations
18
+ *
19
+ * All session-state.json changes are consolidated into a single write
20
+ * at the end to avoid race conditions with the main welcome script.
21
+ *
22
+ * Warnings are saved to session-state.json under `deferred_warnings`
23
+ * and displayed on the NEXT session start.
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ // Parse args: node welcome-deferred.js <rootDir> [--version=X.Y.Z] [--skip-update] [--just-updated]
30
+ const rootDir = process.argv[2];
31
+ if (!rootDir || !fs.existsSync(rootDir)) {
32
+ process.exit(0);
33
+ }
34
+
35
+ const flags = {};
36
+ for (const arg of process.argv.slice(3)) {
37
+ if (arg.startsWith('--')) {
38
+ const eqIdx = arg.indexOf('=');
39
+ if (eqIdx > 0) {
40
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
41
+ } else {
42
+ flags[arg.slice(2)] = true;
43
+ }
44
+ }
45
+ }
46
+
47
+ // Shared utilities
48
+ const { getStatusPath, getSessionStatePath, getMetadataPath } = require('../lib/paths');
49
+ const { tryOptional } = require('../lib/errors');
50
+ const { spawnBackground } = require('../lib/process-executor');
51
+ const { readJSONCached } = require('../lib/file-cache');
52
+
53
+ // Collected warnings to save for next session display
54
+ const warnings = [];
55
+
56
+ // Collected session-state.json mutations (applied in single write at end)
57
+ const stateMutations = {};
58
+
59
+ function addWarning(type, lines) {
60
+ warnings.push({ type, lines, at: new Date().toISOString() });
61
+ }
62
+
63
+ function safeReadJSON(filePath) {
64
+ try {
65
+ if (fs.existsSync(filePath)) {
66
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
67
+ }
68
+ } catch (e) {}
69
+ return null;
70
+ }
71
+
72
+ // 30-second safety timeout to prevent zombie processes
73
+ const TIMEOUT_MS = 30000;
74
+ const timeoutId = setTimeout(() => {
75
+ process.exit(0);
76
+ }, TIMEOUT_MS);
77
+
78
+ async function main() {
79
+ const sessionStatePath = getSessionStatePath(rootDir);
80
+ const version = flags['version'] || 'unknown';
81
+
82
+ // === npm UPDATE CHECK (with cache) ===
83
+ if (!flags['skip-update']) {
84
+ try {
85
+ const checkUpdate = tryOptional(() => require('./check-update.js'), 'check-update');
86
+ if (checkUpdate) {
87
+ const freshUpdateInfo = await checkUpdate.checkForUpdates();
88
+
89
+ // Stage update cache for consolidated write
90
+ stateMutations.update_cache = {
91
+ checked_at: new Date().toISOString(),
92
+ result: {
93
+ available: freshUpdateInfo.updateAvailable || false,
94
+ installed: freshUpdateInfo.installed,
95
+ latest: freshUpdateInfo.latest,
96
+ autoUpdate: freshUpdateInfo.autoUpdate || false,
97
+ justUpdated: freshUpdateInfo.justUpdated || false,
98
+ previousVersion: freshUpdateInfo.previousVersion,
99
+ },
100
+ };
101
+
102
+ // If update available, save warning for next session
103
+ if (freshUpdateInfo.updateAvailable && freshUpdateInfo.latest) {
104
+ addWarning('update_available', [
105
+ `Update available: v${version} -> v${freshUpdateInfo.latest}`,
106
+ `Run: npx agileflow update`,
107
+ ]);
108
+
109
+ // Spawn auto-update if enabled
110
+ if (freshUpdateInfo.autoUpdate) {
111
+ stateMutations.pending_update = {
112
+ from: version,
113
+ to: freshUpdateInfo.latest,
114
+ started_at: new Date().toISOString(),
115
+ };
116
+ spawnBackground('npx', ['agileflow@latest', 'update', '--force'], { cwd: rootDir });
117
+ }
118
+ }
119
+
120
+ // Mark version as seen
121
+ if (freshUpdateInfo.justUpdated || flags['just-updated']) {
122
+ checkUpdate.markVersionSeen(freshUpdateInfo.installed || version);
123
+ }
124
+ }
125
+ } catch (e) {
126
+ // Update check failed, non-critical
127
+ }
128
+ } else if (flags['just-updated']) {
129
+ // Even when skipping update check, mark version as seen
130
+ try {
131
+ const checkUpdate = tryOptional(() => require('./check-update.js'), 'check-update');
132
+ if (checkUpdate) {
133
+ checkUpdate.markVersionSeen(version);
134
+ }
135
+ } catch (e) {}
136
+ }
137
+
138
+ // === SESSION HEALTH WARNINGS ===
139
+ try {
140
+ let sessionManager;
141
+ try {
142
+ sessionManager = require('./session-manager.js');
143
+ } catch (e) {}
144
+
145
+ const health = sessionManager ? sessionManager.getSessionsHealth({ staleDays: 7 }) : null;
146
+
147
+ if (health) {
148
+ const healthLines = [];
149
+
150
+ if (health.uncommitted.length > 0) {
151
+ healthLines.push(`${health.uncommitted.length} session(s) have uncommitted changes`);
152
+ health.uncommitted.slice(0, 3).forEach(sess => {
153
+ const name = sess.nickname ? `"${sess.nickname}"` : `Session ${sess.id}`;
154
+ healthLines.push(` ${name}: ${sess.changeCount} file(s)`);
155
+ });
156
+ }
157
+
158
+ if (health.stale.length > 0) {
159
+ healthLines.push(`${health.stale.length} session(s) inactive for 7+ days`);
160
+ }
161
+
162
+ if (health.orphanedRegistry.length > 0) {
163
+ healthLines.push(`${health.orphanedRegistry.length} session(s) have missing directories`);
164
+ }
165
+
166
+ if (healthLines.length > 0) {
167
+ addWarning('session_health', healthLines);
168
+ }
169
+ }
170
+ } catch (e) {}
171
+
172
+ // === DUPLICATE CLAUDE PROCESS DETECTION ===
173
+ try {
174
+ const processCleanup = tryOptional(
175
+ () => require('./lib/process-cleanup.js'),
176
+ 'process-cleanup'
177
+ );
178
+ if (processCleanup) {
179
+ const cache = { metadata: readJSONCached(getMetadataPath(rootDir)) };
180
+ const autoKillConfigured = cache.metadata?.features?.processCleanup?.autoKill === true;
181
+ const autoKill = autoKillConfigured && process.env.AGILEFLOW_PROCESS_CLEANUP_AUTOKILL === '1';
182
+
183
+ const cleanupResult = processCleanup.cleanupDuplicateProcesses({
184
+ rootDir,
185
+ autoKill,
186
+ dryRun: false,
187
+ });
188
+
189
+ if (cleanupResult.duplicates > 0) {
190
+ const lines = [];
191
+ if (cleanupResult.killed.length > 0) {
192
+ lines.push(`Cleaned ${cleanupResult.killed.length} duplicate Claude process(es)`);
193
+ } else {
194
+ lines.push(`${cleanupResult.duplicates} other Claude process(es) in same directory`);
195
+ }
196
+ addWarning('process_cleanup', lines);
197
+ }
198
+ }
199
+ } catch (e) {}
200
+
201
+ // === STORY CLAIMING CLEANUP ===
202
+ try {
203
+ const storyClaiming = tryOptional(() => require('./lib/story-claiming.js'), 'story-claiming');
204
+ if (storyClaiming) {
205
+ storyClaiming.cleanupStaleClaims({ rootDir });
206
+
207
+ const othersResult = storyClaiming.getStoriesClaimedByOthers({ rootDir });
208
+ if (othersResult.ok && othersResult.stories && othersResult.stories.length > 0) {
209
+ const lines = [`${othersResult.stories.length} story(ies) claimed by other sessions`];
210
+ othersResult.stories.slice(0, 3).forEach(s => {
211
+ lines.push(` ${s.storyId}: claimed by session ${s.sessionId}`);
212
+ });
213
+ addWarning('story_claiming', lines);
214
+ }
215
+ }
216
+ } catch (e) {}
217
+
218
+ // === FILE TRACKING CLEANUP ===
219
+ try {
220
+ const fileTracking = tryOptional(() => require('./lib/file-tracking.js'), 'file-tracking');
221
+ if (fileTracking) {
222
+ fileTracking.cleanupStaleTouches({ rootDir });
223
+
224
+ const overlapsResult = fileTracking.getMyFileOverlaps({ rootDir });
225
+ if (overlapsResult.ok && overlapsResult.overlaps && overlapsResult.overlaps.length > 0) {
226
+ const lines = [`${overlapsResult.overlaps.length} file(s) being edited by other sessions`];
227
+ addWarning('file_tracking', lines);
228
+ }
229
+ }
230
+ } catch (e) {}
231
+
232
+ // === EPIC COMPLETION CHECK ===
233
+ try {
234
+ const storyStateMachine = tryOptional(
235
+ () => require('./lib/story-state-machine.js'),
236
+ 'story-state-machine'
237
+ );
238
+ if (storyStateMachine) {
239
+ const statusPath = getStatusPath(rootDir);
240
+ if (fs.existsSync(statusPath)) {
241
+ const statusData = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
242
+ const incompleteEpics = storyStateMachine.findIncompleteEpics(statusData);
243
+
244
+ if (incompleteEpics.length > 0) {
245
+ let autoCompleted = 0;
246
+ const completedLines = [];
247
+ for (const { epicId, completed, total } of incompleteEpics) {
248
+ const result = storyStateMachine.autoCompleteEpic(statusData, epicId);
249
+ if (result.updated) {
250
+ autoCompleted++;
251
+ completedLines.push(`Auto-completed ${epicId} (${completed}/${total} stories done)`);
252
+ }
253
+ }
254
+ if (autoCompleted > 0) {
255
+ fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2) + '\n');
256
+ addWarning('epic_completion', completedLines);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ } catch (e) {}
262
+
263
+ // === IDEATION SYNC ===
264
+ try {
265
+ const syncIdeationStatus = tryOptional(
266
+ () => require('./lib/sync-ideation-status.js'),
267
+ 'sync-ideation-status'
268
+ );
269
+ if (syncIdeationStatus) {
270
+ const syncResult = syncIdeationStatus.syncImplementedIdeas(rootDir);
271
+ if (syncResult.ok && syncResult.updated > 0) {
272
+ addWarning('ideation_sync', [`Synced ${syncResult.updated} idea(s) as implemented`]);
273
+ }
274
+ }
275
+ } catch (e) {}
276
+
277
+ // === SCHEDULED AUTOMATIONS ===
278
+ try {
279
+ const automationRegistry = tryOptional(
280
+ () => require('./lib/automation-registry.js'),
281
+ 'automation-registry'
282
+ );
283
+ const automationRunner = tryOptional(
284
+ () => require('./lib/automation-runner.js'),
285
+ 'automation-runner'
286
+ );
287
+
288
+ if (automationRegistry && automationRunner) {
289
+ automationRegistry.getAutomationRegistry({ rootDir });
290
+ const runner = automationRunner.getAutomationRunner({ rootDir });
291
+ const dueStatus = runner.getDueStatus();
292
+
293
+ if (dueStatus.due > 0) {
294
+ const lines = [`${dueStatus.due} automation(s) due to run`];
295
+ dueStatus.dueAutomations.slice(0, 3).forEach(auto => {
296
+ lines.push(` ${auto.name}`);
297
+ });
298
+
299
+ // Spawn automation runner in background
300
+ const runnerScriptPath = path.join(__dirname, 'automation-run-due.js');
301
+ if (fs.existsSync(runnerScriptPath)) {
302
+ spawnBackground('node', [runnerScriptPath], { cwd: rootDir });
303
+ lines.push('Running in background...');
304
+ }
305
+
306
+ addWarning('automations', lines);
307
+ }
308
+ }
309
+ } catch (e) {}
310
+
311
+ // === US-0356: DEFERRED SESSION REGISTRATION ===
312
+ // Full session-manager registration (lock write, branch/story update, stale cleanup)
313
+ // was deferred from Phase 1 for startup performance.
314
+ if (flags['run-session-register']) {
315
+ try {
316
+ const sm = require('./session-manager.js');
317
+ if (sm && sm.fullStatus) {
318
+ sm.fullStatus();
319
+ }
320
+ } catch (e) {
321
+ // Session registration failed, non-critical
322
+ }
323
+ }
324
+
325
+ // === US-0356: DEFERRED EXPERTISE SCAN ===
326
+ // Cache expertise count in session-state for next session's fast display
327
+ if (flags['run-expertise-scan']) {
328
+ try {
329
+ const agileflowDir = path.join(rootDir, '.agileflow');
330
+ let expertsDir = path.join(agileflowDir, 'experts');
331
+ if (!fs.existsSync(expertsDir)) {
332
+ expertsDir = path.join(rootDir, 'packages', 'cli', 'src', 'core', 'experts');
333
+ }
334
+ if (fs.existsSync(expertsDir)) {
335
+ const domains = fs
336
+ .readdirSync(expertsDir, { withFileTypes: true })
337
+ .filter(d => d.isDirectory() && d.name !== 'templates');
338
+ const total = domains.length;
339
+ let passed = 0,
340
+ warnings_count = 0,
341
+ failed = 0;
342
+ const issues = [];
343
+ for (const domain of domains) {
344
+ const filePath = path.join(expertsDir, domain.name, 'expertise.yaml');
345
+ if (!fs.existsSync(filePath)) {
346
+ failed++;
347
+ issues.push(`${domain.name}: missing file`);
348
+ } else if (passed < 3) {
349
+ try {
350
+ const content = fs.readFileSync(filePath, 'utf8');
351
+ const m = content.match(/^last_updated:\s*['"]?(\d{4}-\d{2}-\d{2})/m);
352
+ if (m) {
353
+ const days = Math.floor((Date.now() - new Date(m[1]).getTime()) / 86400000);
354
+ if (days > 30) {
355
+ warnings_count++;
356
+ issues.push(`${domain.name}: stale (${days}d)`);
357
+ } else passed++;
358
+ } else passed++;
359
+ } catch (e) {
360
+ passed++;
361
+ }
362
+ } else {
363
+ passed++;
364
+ }
365
+ }
366
+ stateMutations.expertise_count = {
367
+ total,
368
+ passed,
369
+ warnings: warnings_count,
370
+ failed,
371
+ issues,
372
+ };
373
+ }
374
+ } catch (e) {
375
+ // Expertise scan failed, non-critical
376
+ }
377
+ }
378
+
379
+ // === US-0356: DEFERRED CONFIG STALENESS CHECK ===
380
+ // Cache config staleness result in session-state for next session's fast display.
381
+ // Uses a simplified check: just count unconfigured options in metadata.
382
+ if (flags['run-config-staleness']) {
383
+ try {
384
+ const metadata = safeReadJSON(getMetadataPath(rootDir));
385
+ if (metadata) {
386
+ const configOptions = metadata.agileflow?.config_options || {};
387
+ let unconfigured = 0;
388
+ const newOptions = [];
389
+ for (const [name, option] of Object.entries(configOptions)) {
390
+ if (option.configured === false) {
391
+ unconfigured++;
392
+ newOptions.push({ name, description: option.description || name });
393
+ }
394
+ }
395
+ stateMutations.config_staleness = {
396
+ outdated: unconfigured > 0,
397
+ newOptionsCount: unconfigured,
398
+ newOptions: newOptions.slice(0, 5),
399
+ cached_at: new Date().toISOString(),
400
+ };
401
+ }
402
+ } catch (e) {
403
+ // Config staleness check failed, non-critical
404
+ }
405
+ }
406
+
407
+ // === SINGLE CONSOLIDATED WRITE TO SESSION STATE ===
408
+ // Read once, apply all mutations, write once. Avoids race conditions.
409
+ try {
410
+ const state = safeReadJSON(sessionStatePath) || {};
411
+
412
+ // Apply staged mutations
413
+ for (const [key, value] of Object.entries(stateMutations)) {
414
+ state[key] = value;
415
+ }
416
+
417
+ // Save collected warnings
418
+ if (warnings.length > 0) {
419
+ state.deferred_warnings = warnings;
420
+ }
421
+
422
+ const dir = path.dirname(sessionStatePath);
423
+ if (!fs.existsSync(dir)) {
424
+ fs.mkdirSync(dir, { recursive: true });
425
+ }
426
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
427
+ } catch (e) {
428
+ // Write failed, warnings will be lost for this session
429
+ }
430
+
431
+ clearTimeout(timeoutId);
432
+ }
433
+
434
+ main().catch(() => {
435
+ clearTimeout(timeoutId);
436
+ process.exit(0);
437
+ });