agileflow 2.99.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/lib/cache-provider.js +155 -0
- package/lib/codebase-indexer.js +1 -1
- package/lib/content-sanitizer.js +1 -0
- package/lib/dashboard-protocol.js +25 -0
- package/lib/dashboard-server.js +184 -133
- package/lib/errors.js +18 -0
- package/lib/file-cache.js +1 -1
- package/lib/flag-detection.js +11 -20
- package/lib/git-operations.js +15 -33
- package/lib/merge-operations.js +40 -34
- package/lib/process-executor.js +199 -0
- package/lib/registry-cache.js +13 -47
- package/lib/skill-loader.js +206 -0
- package/lib/smart-json-file.js +2 -4
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -12
- package/scripts/agileflow-statusline.sh +30 -0
- package/scripts/agileflow-welcome.js +181 -212
- package/scripts/auto-self-improve.js +3 -3
- package/scripts/claude-smart.sh +67 -0
- package/scripts/claude-tmux.sh +248 -161
- package/scripts/damage-control-multi-agent.js +227 -0
- package/scripts/lib/bus-utils.js +471 -0
- package/scripts/lib/configure-detect.js +5 -6
- package/scripts/lib/configure-features.js +44 -0
- package/scripts/lib/configure-repair.js +5 -6
- package/scripts/lib/configure-utils.js +2 -3
- package/scripts/lib/context-formatter.js +87 -8
- package/scripts/lib/damage-control-utils.js +37 -3
- package/scripts/lib/file-lock.js +392 -0
- package/scripts/lib/ideation-index.js +2 -5
- package/scripts/lib/lifecycle-detector.js +123 -0
- package/scripts/lib/process-cleanup.js +55 -81
- package/scripts/lib/scale-detector.js +357 -0
- package/scripts/lib/signal-detectors.js +779 -0
- package/scripts/lib/story-state-machine.js +1 -1
- package/scripts/lib/sync-ideation-status.js +2 -3
- package/scripts/lib/task-registry.js +7 -1
- package/scripts/lib/team-events.js +357 -0
- package/scripts/messaging-bridge.js +79 -36
- package/scripts/migrate-ideation-index.js +37 -14
- package/scripts/obtain-context.js +37 -19
- package/scripts/ralph-loop.js +3 -4
- package/scripts/smart-detect.js +390 -0
- package/scripts/team-manager.js +174 -30
- package/src/core/commands/audit.md +13 -11
- package/src/core/commands/babysit.md +162 -115
- package/src/core/commands/changelog.md +21 -4
- package/src/core/commands/configure.md +105 -2
- package/src/core/commands/debt.md +12 -2
- package/src/core/commands/feedback.md +7 -6
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +5 -5
- package/src/core/commands/logic/audit.md +2 -2
- package/src/core/commands/pr.md +7 -6
- package/src/core/commands/research/analyze.md +28 -20
- package/src/core/commands/research/ask.md +43 -0
- package/src/core/commands/research/import.md +29 -21
- package/src/core/commands/research/list.md +8 -7
- package/src/core/commands/research/synthesize.md +356 -20
- package/src/core/commands/research/view.md +8 -5
- package/src/core/commands/review.md +24 -6
- package/src/core/commands/skill/create.md +34 -0
- package/tools/cli/lib/docs-setup.js +4 -0
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
|
-
const {
|
|
24
|
+
const { executeCommandSync } = require('../lib/process-executor');
|
|
25
25
|
|
|
26
26
|
// Import loader and formatter modules
|
|
27
27
|
const {
|
|
@@ -35,6 +35,14 @@ const {
|
|
|
35
35
|
|
|
36
36
|
const { generateSummary, generateFullContent } = require('./lib/context-formatter');
|
|
37
37
|
|
|
38
|
+
// Smart detection for contextual feature routing
|
|
39
|
+
let smartDetect;
|
|
40
|
+
try {
|
|
41
|
+
smartDetect = require('./smart-detect');
|
|
42
|
+
} catch {
|
|
43
|
+
// smart-detect not available
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
// Import validation
|
|
39
47
|
let isValidCommandName;
|
|
40
48
|
try {
|
|
@@ -122,28 +130,27 @@ function executeQueryMode(query) {
|
|
|
122
130
|
return null;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
if (result.includes('No files found') || result.trim() === '') {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
133
|
+
const result = executeCommandSync('node', [queryScript, `--query=${query}`, '--budget=15000'], {
|
|
134
|
+
timeout: 30000,
|
|
135
|
+
});
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
query: query,
|
|
138
|
-
results: result.trim(),
|
|
139
|
-
};
|
|
140
|
-
} catch (err) {
|
|
141
|
-
if (err.status === 2) {
|
|
137
|
+
if (!result.ok) {
|
|
138
|
+
if (result.exitCode === 2) {
|
|
142
139
|
return null; // No results, fall back
|
|
143
140
|
}
|
|
144
|
-
console.error(`Query error: ${
|
|
141
|
+
console.error(`Query error: ${result.error}`);
|
|
145
142
|
return null;
|
|
146
143
|
}
|
|
144
|
+
|
|
145
|
+
if (result.data.includes('No files found') || result.data === '') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
mode: 'query',
|
|
151
|
+
query: query,
|
|
152
|
+
results: result.data,
|
|
153
|
+
};
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
// =============================================================================
|
|
@@ -188,8 +195,19 @@ async function main() {
|
|
|
188
195
|
process.stderr.write(`Context loaded in ${(prefetchElapsed / 1000).toFixed(1)}s\n`);
|
|
189
196
|
}
|
|
190
197
|
|
|
198
|
+
// Run smart detection (contextual feature routing)
|
|
199
|
+
let smartDetectResults = null;
|
|
200
|
+
if (smartDetect) {
|
|
201
|
+
try {
|
|
202
|
+
smartDetectResults = smartDetect.analyze(prefetched);
|
|
203
|
+
smartDetect.writeRecommendations(smartDetectResults);
|
|
204
|
+
} catch {
|
|
205
|
+
// Smart detection is optional - don't block context output
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
191
209
|
// Generate formatted output
|
|
192
|
-
const formatOptions = { commandName, activeSections };
|
|
210
|
+
const formatOptions = { commandName, activeSections, smartDetectResults };
|
|
193
211
|
const summary = generateSummary(prefetched, formatOptions);
|
|
194
212
|
const fullContent = generateFullContent(prefetched, formatOptions);
|
|
195
213
|
|
package/scripts/ralph-loop.js
CHANGED
|
@@ -35,7 +35,7 @@ const { execFileSync, spawnSync } = require('child_process');
|
|
|
35
35
|
// Shared utilities
|
|
36
36
|
const { c } = require('../lib/colors');
|
|
37
37
|
const { getProjectRoot, getStatusPath, getSessionStatePath } = require('../lib/paths');
|
|
38
|
-
const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
|
|
38
|
+
const { safeReadJSON, safeWriteJSON, tryOptional } = require('../lib/errors');
|
|
39
39
|
const { isValidEpicId, parseIntBounded } = require('../lib/validate');
|
|
40
40
|
|
|
41
41
|
// Agent Teams integration (lazy-loaded)
|
|
@@ -428,7 +428,7 @@ function runCoverage(rootDir) {
|
|
|
428
428
|
|
|
429
429
|
// Get screenshots directory from metadata or default
|
|
430
430
|
function getScreenshotsDir(rootDir) {
|
|
431
|
-
|
|
431
|
+
return tryOptional(() => {
|
|
432
432
|
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
433
433
|
if (fs.existsSync(metadataPath)) {
|
|
434
434
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
@@ -436,8 +436,7 @@ function getScreenshotsDir(rootDir) {
|
|
|
436
436
|
return metadata.ralph_loop.screenshots_dir;
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
|
-
}
|
|
440
|
-
return './screenshots';
|
|
439
|
+
}, 'metadata read') || './screenshots';
|
|
441
440
|
}
|
|
442
441
|
|
|
443
442
|
// Run screenshot verification (Visual Mode)
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smart-detect.js
|
|
4
|
+
*
|
|
5
|
+
* Orchestrator for contextual feature routing.
|
|
6
|
+
* Gathers signals from prefetched context data, runs feature detectors,
|
|
7
|
+
* filters by lifecycle phase, and outputs recommendations.
|
|
8
|
+
*
|
|
9
|
+
* Called by obtain-context.js after prefetchAllData() returns.
|
|
10
|
+
* Output: docs/09-agents/smart-detect.json
|
|
11
|
+
*
|
|
12
|
+
* Usage (standalone):
|
|
13
|
+
* node scripts/smart-detect.js
|
|
14
|
+
*
|
|
15
|
+
* Usage (as module):
|
|
16
|
+
* const smartDetect = require('./smart-detect');
|
|
17
|
+
* const result = smartDetect.analyze(prefetched, sessionState, metadata);
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { detectLifecyclePhase, getRelevantPhases } = require('./lib/lifecycle-detector');
|
|
25
|
+
const { runDetectorsForPhases } = require('./lib/signal-detectors');
|
|
26
|
+
|
|
27
|
+
let safeReadJSON, safeWriteJSON, tryOptional;
|
|
28
|
+
try {
|
|
29
|
+
const errors = require('../lib/errors');
|
|
30
|
+
safeReadJSON = errors.safeReadJSON;
|
|
31
|
+
safeWriteJSON = errors.safeWriteJSON;
|
|
32
|
+
tryOptional = errors.tryOptional;
|
|
33
|
+
} catch {
|
|
34
|
+
// Fallback for when running outside package context
|
|
35
|
+
safeReadJSON = (filePath, opts = {}) => {
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
return opts.defaultValue !== undefined
|
|
39
|
+
? { ok: true, data: opts.defaultValue }
|
|
40
|
+
: { ok: false, error: 'not found' };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, data: JSON.parse(fs.readFileSync(filePath, 'utf8')) };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, error: e.message };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
safeWriteJSON = (filePath, data) => {
|
|
48
|
+
try {
|
|
49
|
+
const dir = path.dirname(filePath);
|
|
50
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
51
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
52
|
+
return { ok: true };
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return { ok: false, error: e.message };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
tryOptional = (fn, _label) => { try { return fn(); } catch { return undefined; } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Signal Extraction from Prefetched Data
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract structured signals from prefetched context data.
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} prefetched - Data from obtain-context.js prefetchAllData()
|
|
68
|
+
* @param {Object} sessionState - Parsed session-state.json
|
|
69
|
+
* @param {Object} metadata - Parsed agileflow-metadata.json
|
|
70
|
+
* @returns {Object} Structured signals for detectors
|
|
71
|
+
*/
|
|
72
|
+
function extractSignals(prefetched, sessionState, metadata) {
|
|
73
|
+
const statusJson = prefetched?.json?.statusJson || null;
|
|
74
|
+
const git = prefetched?.git || {};
|
|
75
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
76
|
+
|
|
77
|
+
let packageJson = null;
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
80
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// No package.json or invalid
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Determine current story
|
|
87
|
+
let story = null;
|
|
88
|
+
const currentStoryId = sessionState?.current_session?.current_story;
|
|
89
|
+
if (currentStoryId && statusJson?.stories?.[currentStoryId]) {
|
|
90
|
+
const s = statusJson.stories[currentStoryId];
|
|
91
|
+
story = { id: currentStoryId, ...s };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Count stories by status
|
|
95
|
+
const counts = {};
|
|
96
|
+
let storyCount = 0;
|
|
97
|
+
if (statusJson?.stories) {
|
|
98
|
+
Object.values(statusJson.stories).forEach(s => {
|
|
99
|
+
const status = s.status || 'unknown';
|
|
100
|
+
counts[status] = (counts[status] || 0) + 1;
|
|
101
|
+
storyCount++;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Git signals
|
|
106
|
+
const statusLines = (git.status || '').split('\n').filter(Boolean);
|
|
107
|
+
const changedFiles = statusLines.map(line => line.substring(3).trim());
|
|
108
|
+
const branch = git.branch || '';
|
|
109
|
+
const isClean = statusLines.length === 0;
|
|
110
|
+
const onFeatureBranch = branch !== 'main' && branch !== 'master' && branch !== '';
|
|
111
|
+
|
|
112
|
+
// Parse diff stats if available
|
|
113
|
+
let diffStats = null;
|
|
114
|
+
if (git.diffStat) {
|
|
115
|
+
const match = git.diffStat.match(/(\d+) insertions?.*?(\d+) deletions?/);
|
|
116
|
+
if (match) {
|
|
117
|
+
diffStats = { insertions: parseInt(match[1], 10), deletions: parseInt(match[2], 10) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// File existence checks
|
|
122
|
+
const files = {
|
|
123
|
+
tsconfig: fs.existsSync('tsconfig.json'),
|
|
124
|
+
eslintrc: fs.existsSync('.eslintrc.js') || fs.existsSync('.eslintrc.json') || fs.existsSync('.eslintrc.yml'),
|
|
125
|
+
coverage: fs.existsSync('coverage/coverage-summary.json'),
|
|
126
|
+
playwright: fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js'),
|
|
127
|
+
screenshots: fs.existsSync('screenshots'),
|
|
128
|
+
ciConfig: fs.existsSync('.github/workflows') || fs.existsSync('.gitlab-ci.yml') || fs.existsSync('Jenkinsfile'),
|
|
129
|
+
expertiseDir: fs.existsSync('.agileflow/expertise'),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Test state
|
|
133
|
+
let tests = { passing: null, hasTestSetup: false };
|
|
134
|
+
if (packageJson?.scripts?.test) {
|
|
135
|
+
tests.hasTestSetup = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Plan mode detection
|
|
139
|
+
const planModeActive = (sessionState?.active_commands || []).some(
|
|
140
|
+
c => c.active_sections && c.active_sections.includes('plan-mode')
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Thresholds from metadata
|
|
144
|
+
const thresholds = metadata?.smart_detect?.thresholds || {};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
statusJson,
|
|
148
|
+
sessionState,
|
|
149
|
+
metadata,
|
|
150
|
+
git: {
|
|
151
|
+
branch,
|
|
152
|
+
filesChanged: statusLines.length,
|
|
153
|
+
changedFiles,
|
|
154
|
+
isClean,
|
|
155
|
+
onFeatureBranch,
|
|
156
|
+
diffStats,
|
|
157
|
+
commitCount: git.commitCount || 0,
|
|
158
|
+
},
|
|
159
|
+
packageJson,
|
|
160
|
+
story,
|
|
161
|
+
files,
|
|
162
|
+
tests,
|
|
163
|
+
counts,
|
|
164
|
+
storyCount,
|
|
165
|
+
thresholds,
|
|
166
|
+
session: {
|
|
167
|
+
planModeActive,
|
|
168
|
+
activeCommands: sessionState?.active_commands || [],
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// Recommendation Filtering
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Filter recommendations based on user preferences and session history.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object[]} recommendations - Raw recommendations from detectors
|
|
181
|
+
* @param {Object} metadata - AgileFlow metadata (for disabled features)
|
|
182
|
+
* @param {Object} sessionState - Session state (for already-offered features)
|
|
183
|
+
* @returns {Object} Filtered and categorized recommendations
|
|
184
|
+
*/
|
|
185
|
+
function filterRecommendations(recommendations, metadata, sessionState) {
|
|
186
|
+
const disabledFeatures = new Set(metadata?.smart_detect?.disabled_features || []);
|
|
187
|
+
const offeredFeatures = new Set(sessionState?.smart_detect?.features_offered || []);
|
|
188
|
+
const skippedFeatures = new Set(sessionState?.smart_detect?.features_skipped || []);
|
|
189
|
+
|
|
190
|
+
// Apply priority overrides from metadata
|
|
191
|
+
const priorityOverrides = metadata?.smart_detect?.priority_overrides || {};
|
|
192
|
+
|
|
193
|
+
const filtered = recommendations
|
|
194
|
+
.filter(r => !disabledFeatures.has(r.feature))
|
|
195
|
+
.filter(r => !skippedFeatures.has(r.feature))
|
|
196
|
+
.map(r => {
|
|
197
|
+
if (priorityOverrides[r.feature]) {
|
|
198
|
+
return { ...r, priority: priorityOverrides[r.feature] };
|
|
199
|
+
}
|
|
200
|
+
return r;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Categorize: immediate (high priority, not yet offered), available (rest)
|
|
204
|
+
const immediate = filtered.filter(r => r.priority === 'high' && !offeredFeatures.has(r.feature));
|
|
205
|
+
const available = filtered.filter(r => r.priority !== 'high' || offeredFeatures.has(r.feature));
|
|
206
|
+
|
|
207
|
+
// Sort by priority within each category
|
|
208
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
209
|
+
immediate.sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2));
|
|
210
|
+
available.sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2));
|
|
211
|
+
|
|
212
|
+
return { immediate, available };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// Main Analysis
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Run full smart detection analysis.
|
|
221
|
+
*
|
|
222
|
+
* @param {Object} prefetched - Data from obtain-context.js prefetchAllData()
|
|
223
|
+
* @param {Object} [sessionState] - Parsed session-state.json (optional, extracted from prefetched if omitted)
|
|
224
|
+
* @param {Object} [metadata] - Parsed agileflow-metadata.json (optional, extracted from prefetched if omitted)
|
|
225
|
+
* @returns {Object} Smart detection results
|
|
226
|
+
*/
|
|
227
|
+
function analyze(prefetched, sessionState, metadata) {
|
|
228
|
+
// Extract from prefetched if not provided directly
|
|
229
|
+
if (!sessionState) {
|
|
230
|
+
sessionState = prefetched?.json?.sessionState || {};
|
|
231
|
+
}
|
|
232
|
+
if (!metadata) {
|
|
233
|
+
metadata = prefetched?.json?.metadata || {};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if smart-detect is disabled
|
|
237
|
+
if (metadata?.smart_detect?.enabled === false) {
|
|
238
|
+
return {
|
|
239
|
+
detected_at: new Date().toISOString(),
|
|
240
|
+
lifecycle_phase: 'unknown',
|
|
241
|
+
recommendations: { immediate: [], available: [], auto_enabled: {} },
|
|
242
|
+
signals_summary: {},
|
|
243
|
+
disabled: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Extract signals
|
|
248
|
+
const signals = extractSignals(prefetched, sessionState, metadata);
|
|
249
|
+
|
|
250
|
+
// Detect lifecycle phase
|
|
251
|
+
const phaseResult = detectLifecyclePhase(signals);
|
|
252
|
+
const relevantPhases = getRelevantPhases(phaseResult.phase);
|
|
253
|
+
|
|
254
|
+
// Run detectors for relevant phases
|
|
255
|
+
const rawRecommendations = runDetectorsForPhases(relevantPhases, signals);
|
|
256
|
+
|
|
257
|
+
// Filter and categorize
|
|
258
|
+
const { immediate, available } = filterRecommendations(rawRecommendations, metadata, sessionState);
|
|
259
|
+
|
|
260
|
+
// Auto-enabled features (existing babysit modes)
|
|
261
|
+
const autoEnabled = detectAutoModes(signals);
|
|
262
|
+
|
|
263
|
+
// Build signals summary
|
|
264
|
+
const signalsSummary = {
|
|
265
|
+
story: signals.story
|
|
266
|
+
? `${signals.story.id} (${signals.story.status || 'unknown'}${signals.story.owner ? ', ' + signals.story.owner : ''})`
|
|
267
|
+
: 'none',
|
|
268
|
+
files_changed: signals.git.filesChanged,
|
|
269
|
+
core_files: (signals.git.changedFiles || []).filter(f =>
|
|
270
|
+
/^(src\/(core|lib|shared)|lib\/|packages\/.*\/src\/)/.test(f)
|
|
271
|
+
).length,
|
|
272
|
+
tests_passing: signals.tests.passing,
|
|
273
|
+
on_feature_branch: signals.git.onFeatureBranch,
|
|
274
|
+
story_counts: signals.counts,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
detected_at: new Date().toISOString(),
|
|
279
|
+
lifecycle_phase: phaseResult.phase,
|
|
280
|
+
phase_confidence: phaseResult.confidence,
|
|
281
|
+
phase_reason: phaseResult.reason,
|
|
282
|
+
recommendations: {
|
|
283
|
+
immediate,
|
|
284
|
+
available,
|
|
285
|
+
auto_enabled: autoEnabled,
|
|
286
|
+
},
|
|
287
|
+
signals_summary: signalsSummary,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Detect existing babysit auto-modes (loop, visual, coverage).
|
|
293
|
+
* Preserves backward compatibility with existing Smart Detection.
|
|
294
|
+
*
|
|
295
|
+
* @param {Object} signals
|
|
296
|
+
* @returns {Object} Auto-enabled mode flags
|
|
297
|
+
*/
|
|
298
|
+
function detectAutoModes(signals) {
|
|
299
|
+
const { statusJson, story, files, packageJson } = signals;
|
|
300
|
+
|
|
301
|
+
// Loop mode: 3+ ready stories in same epic + test setup
|
|
302
|
+
let loopMode = false;
|
|
303
|
+
if (story?.epic && statusJson?.stories) {
|
|
304
|
+
const readyInEpic = Object.entries(statusJson.stories).filter(
|
|
305
|
+
([, s]) => s.epic === story.epic && s.status === 'ready'
|
|
306
|
+
).length;
|
|
307
|
+
loopMode = readyInEpic >= 3 && !!(packageJson?.scripts?.test);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Visual mode: UI-related story or visual e2e setup
|
|
311
|
+
const visualMode = !!(files.playwright && files.screenshots);
|
|
312
|
+
|
|
313
|
+
// Coverage mode: coverage data exists
|
|
314
|
+
const coverageMode = !!files.coverage;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
loop_mode: loopMode,
|
|
318
|
+
visual_mode: visualMode,
|
|
319
|
+
coverage_mode: coverageMode,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// =============================================================================
|
|
324
|
+
// Output
|
|
325
|
+
// =============================================================================
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Write recommendations to smart-detect.json.
|
|
329
|
+
*
|
|
330
|
+
* @param {Object} results - Analysis results from analyze()
|
|
331
|
+
* @param {string} [outputPath] - Override output path
|
|
332
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
333
|
+
*/
|
|
334
|
+
function writeRecommendations(results, outputPath) {
|
|
335
|
+
const targetPath = outputPath || path.join(process.cwd(), 'docs/09-agents/smart-detect.json');
|
|
336
|
+
return safeWriteJSON(targetPath, results, { createDir: true });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// =============================================================================
|
|
340
|
+
// CLI Entry Point
|
|
341
|
+
// =============================================================================
|
|
342
|
+
|
|
343
|
+
if (require.main === module) {
|
|
344
|
+
// Run standalone - gather our own data
|
|
345
|
+
const statusJsonResult = safeReadJSON(
|
|
346
|
+
path.join(process.cwd(), 'docs/09-agents/status.json'),
|
|
347
|
+
{ defaultValue: {} }
|
|
348
|
+
);
|
|
349
|
+
const sessionStateResult = safeReadJSON(
|
|
350
|
+
path.join(process.cwd(), 'docs/09-agents/session-state.json'),
|
|
351
|
+
{ defaultValue: {} }
|
|
352
|
+
);
|
|
353
|
+
const metadataResult = safeReadJSON(
|
|
354
|
+
path.join(process.cwd(), 'docs/00-meta/agileflow-metadata.json'),
|
|
355
|
+
{ defaultValue: {} }
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Build minimal prefetched structure
|
|
359
|
+
const { execSync } = require('child_process');
|
|
360
|
+
const gitBranch = tryOptional(() => execSync('git branch --show-current', { encoding: 'utf8' }).trim(), 'git branch') || '';
|
|
361
|
+
const gitStatus = tryOptional(() => execSync('git status --short', { encoding: 'utf8' }).trim(), 'git status') || '';
|
|
362
|
+
|
|
363
|
+
const prefetched = {
|
|
364
|
+
json: {
|
|
365
|
+
statusJson: statusJsonResult.ok ? statusJsonResult.data : {},
|
|
366
|
+
sessionState: sessionStateResult.ok ? sessionStateResult.data : {},
|
|
367
|
+
metadata: metadataResult.ok ? metadataResult.data : {},
|
|
368
|
+
},
|
|
369
|
+
git: {
|
|
370
|
+
branch: gitBranch,
|
|
371
|
+
status: gitStatus,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const results = analyze(prefetched);
|
|
376
|
+
writeRecommendations(results);
|
|
377
|
+
|
|
378
|
+
// Output summary to stderr for visibility
|
|
379
|
+
const { immediate, available } = results.recommendations;
|
|
380
|
+
process.stderr.write(`Smart detect: phase=${results.lifecycle_phase}, `);
|
|
381
|
+
process.stderr.write(`${immediate.length} immediate, ${available.length} available\n`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = {
|
|
385
|
+
analyze,
|
|
386
|
+
writeRecommendations,
|
|
387
|
+
extractSignals,
|
|
388
|
+
filterRecommendations,
|
|
389
|
+
detectAutoModes,
|
|
390
|
+
};
|