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.
- package/CHANGELOG.md +10 -0
- package/README.md +58 -86
- 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/feedback.js +9 -2
- package/lib/lazy-require.js +59 -0
- package/lib/logger.js +106 -0
- package/package.json +4 -2
- package/scripts/agileflow-configure.js +14 -2
- package/scripts/agileflow-welcome.js +450 -459
- package/scripts/claude-tmux.sh +113 -5
- package/scripts/context-loader.js +4 -9
- package/scripts/lib/command-prereqs.js +280 -0
- package/scripts/lib/configure-detect.js +92 -2
- package/scripts/lib/configure-features.js +411 -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 +90 -0
- package/scripts/welcome-deferred.js +437 -0
- package/src/core/agents/legal-analyzer-a11y.md +110 -0
- package/src/core/agents/legal-analyzer-ai.md +117 -0
- package/src/core/agents/legal-analyzer-consumer.md +108 -0
- package/src/core/agents/legal-analyzer-content.md +113 -0
- package/src/core/agents/legal-analyzer-international.md +115 -0
- package/src/core/agents/legal-analyzer-licensing.md +115 -0
- package/src/core/agents/legal-analyzer-privacy.md +108 -0
- package/src/core/agents/legal-analyzer-security.md +112 -0
- package/src/core/agents/legal-analyzer-terms.md +111 -0
- package/src/core/agents/legal-consensus.md +242 -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/team-lead.md +50 -13
- 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/audit/legal.md +446 -0
- 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/configure.md +18 -33
- 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/team/start.md +36 -7
- package/src/core/commands/team/stop.md +5 -2
- 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/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 -67
- 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 -75
- package/scripts/tmux-task-watcher.sh +0 -177
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
|
@@ -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
|
+
});
|