agileflow 3.1.0 → 3.2.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 +10 -0
- package/README.md +57 -85
- package/lib/dashboard-automations.js +130 -0
- package/lib/dashboard-git.js +254 -0
- package/lib/dashboard-inbox.js +64 -0
- package/lib/dashboard-protocol.js +1 -0
- package/lib/dashboard-server.js +114 -924
- package/lib/dashboard-session.js +136 -0
- package/lib/dashboard-status.js +72 -0
- package/lib/dashboard-terminal.js +354 -0
- package/lib/dashboard-websocket.js +88 -0
- package/lib/drivers/codex-driver.ts +4 -4
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +409 -434
- package/scripts/claude-tmux.sh +80 -2
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/browser-qa-evidence.js +409 -0
- package/scripts/lib/browser-qa-status.js +192 -0
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +295 -1
- package/scripts/lib/context-formatter.js +468 -233
- package/scripts/lib/context-loader.js +27 -15
- package/scripts/lib/damage-control-utils.js +8 -1
- package/scripts/lib/feature-catalog.js +321 -0
- package/scripts/lib/portable-tasks-cli.js +274 -0
- package/scripts/lib/portable-tasks.js +479 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/team-events.js +86 -1
- package/scripts/obtain-context.js +28 -4
- package/scripts/smart-detect.js +17 -0
- package/scripts/strip-ai-attribution.js +63 -0
- package/scripts/team-manager.js +7 -2
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/browser-qa.md +328 -0
- package/src/core/agents/perf-analyzer-assets.md +174 -0
- package/src/core/agents/perf-analyzer-bundle.md +165 -0
- package/src/core/agents/perf-analyzer-caching.md +160 -0
- package/src/core/agents/perf-analyzer-compute.md +165 -0
- package/src/core/agents/perf-analyzer-memory.md +182 -0
- package/src/core/agents/perf-analyzer-network.md +157 -0
- package/src/core/agents/perf-analyzer-queries.md +155 -0
- package/src/core/agents/perf-analyzer-rendering.md +156 -0
- package/src/core/agents/perf-consensus.md +280 -0
- package/src/core/agents/security-analyzer-api.md +199 -0
- package/src/core/agents/security-analyzer-auth.md +160 -0
- package/src/core/agents/security-analyzer-authz.md +168 -0
- package/src/core/agents/security-analyzer-deps.md +147 -0
- package/src/core/agents/security-analyzer-infra.md +176 -0
- package/src/core/agents/security-analyzer-injection.md +148 -0
- package/src/core/agents/security-analyzer-input.md +191 -0
- package/src/core/agents/security-analyzer-secrets.md +175 -0
- package/src/core/agents/security-consensus.md +276 -0
- package/src/core/agents/test-analyzer-assertions.md +181 -0
- package/src/core/agents/test-analyzer-coverage.md +183 -0
- package/src/core/agents/test-analyzer-fragility.md +185 -0
- package/src/core/agents/test-analyzer-integration.md +155 -0
- package/src/core/agents/test-analyzer-maintenance.md +173 -0
- package/src/core/agents/test-analyzer-mocking.md +178 -0
- package/src/core/agents/test-analyzer-patterns.md +189 -0
- package/src/core/agents/test-analyzer-structure.md +177 -0
- package/src/core/agents/test-consensus.md +294 -0
- package/src/core/commands/{legal/audit.md → audit/legal.md} +13 -13
- package/src/core/commands/{logic/audit.md → audit/logic.md} +12 -12
- package/src/core/commands/audit/performance.md +443 -0
- package/src/core/commands/audit/security.md +443 -0
- package/src/core/commands/audit/test.md +442 -0
- package/src/core/commands/babysit.md +505 -463
- package/src/core/commands/browser-qa.md +240 -0
- package/src/core/commands/configure.md +8 -8
- package/src/core/commands/research/ask.md +42 -9
- package/src/core/commands/research/import.md +14 -8
- package/src/core/commands/research/list.md +17 -16
- package/src/core/commands/research/synthesize.md +8 -8
- package/src/core/commands/research/view.md +28 -4
- package/src/core/commands/whats-new.md +2 -2
- package/src/core/experts/devops/expertise.yaml +13 -2
- package/src/core/experts/documentation/expertise.yaml +26 -4
- package/src/core/profiles/COMPARISON.md +170 -0
- package/src/core/profiles/README.md +178 -0
- package/src/core/profiles/claude-code.yaml +111 -0
- package/src/core/profiles/codex.yaml +103 -0
- package/src/core/profiles/cursor.yaml +134 -0
- package/src/core/profiles/examples.js +250 -0
- package/src/core/profiles/loader.js +235 -0
- package/src/core/profiles/windsurf.yaml +159 -0
- package/src/core/teams/logic-audit.json +6 -0
- package/src/core/teams/perf-audit.json +71 -0
- package/src/core/teams/security-audit.json +71 -0
- package/src/core/teams/test-audit.json +71 -0
- package/src/core/templates/browser-qa-spec.yaml +94 -0
- package/src/core/templates/command-prerequisites.yaml +169 -0
- package/src/core/templates/damage-control-patterns.yaml +9 -0
- package/tools/cli/installers/ide/_base-ide.js +33 -3
- package/tools/cli/installers/ide/claude-code.js +2 -69
- package/tools/cli/installers/ide/codex.js +9 -9
- package/tools/cli/installers/ide/cursor.js +165 -4
- package/tools/cli/installers/ide/windsurf.js +237 -6
- package/tools/cli/lib/content-transformer.js +234 -9
- package/tools/cli/lib/docs-setup.js +1 -1
- package/tools/cli/lib/ide-generator.js +357 -0
- package/tools/cli/lib/ide-registry.js +2 -2
- package/scripts/tmux-task-name.sh +0 -105
- package/scripts/tmux-task-watcher.sh +0 -344
package/scripts/smart-detect.js
CHANGED
|
@@ -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);
|
package/scripts/team-manager.js
CHANGED
|
@@ -137,7 +137,10 @@ function validateTemplateName(name) {
|
|
|
137
137
|
return { valid: false, error: 'Template name must be 1-255 characters' };
|
|
138
138
|
}
|
|
139
139
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
140
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
valid: false,
|
|
142
|
+
error: 'Template name must contain only alphanumeric characters, hyphens, and underscores',
|
|
143
|
+
};
|
|
141
144
|
}
|
|
142
145
|
return { valid: true };
|
|
143
146
|
}
|
|
@@ -221,7 +224,9 @@ function buildTeammatePrompt(teammate, template) {
|
|
|
221
224
|
} else if (teammate.description) {
|
|
222
225
|
parts.push(teammate.description);
|
|
223
226
|
} else {
|
|
224
|
-
parts.push(
|
|
227
|
+
parts.push(
|
|
228
|
+
`You are the ${teammate.role || 'teammate'} agent responsible for the ${teammate.domain || 'general'} domain.`
|
|
229
|
+
);
|
|
225
230
|
}
|
|
226
231
|
parts.push('');
|
|
227
232
|
|
|
@@ -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
|
+
});
|