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
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* signal-detectors.js
|
|
4
|
+
*
|
|
5
|
+
* Registry of feature detector functions for contextual feature routing.
|
|
6
|
+
* Each detector analyzes project signals and returns a recommendation
|
|
7
|
+
* (or null if not triggered).
|
|
8
|
+
*
|
|
9
|
+
* Pattern follows DISCRETION_CONDITIONS from ralph-loop.js:
|
|
10
|
+
* name -> (signals) => result | null
|
|
11
|
+
*
|
|
12
|
+
* Organized by lifecycle phase:
|
|
13
|
+
* pre-story, planning, implementation, post-impl, pre-pr
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Detector Result Helpers
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a recommendation result.
|
|
24
|
+
* @param {string} feature - Feature/command name
|
|
25
|
+
* @param {Object} opts
|
|
26
|
+
* @param {'high'|'medium'|'low'} opts.priority
|
|
27
|
+
* @param {string} opts.trigger - Why this was triggered
|
|
28
|
+
* @param {'auto'|'suggest'|'offer'} opts.action - How to present it
|
|
29
|
+
* @param {string} opts.command - AgileFlow command to run
|
|
30
|
+
* @param {string} opts.phase - Lifecycle phase this belongs to
|
|
31
|
+
* @returns {Object} Recommendation object
|
|
32
|
+
*/
|
|
33
|
+
function recommend(feature, opts) {
|
|
34
|
+
return {
|
|
35
|
+
feature,
|
|
36
|
+
priority: opts.priority || 'medium',
|
|
37
|
+
trigger: opts.trigger,
|
|
38
|
+
action: opts.action || 'suggest',
|
|
39
|
+
command: opts.command || `/agileflow:${feature}`,
|
|
40
|
+
phase: opts.phase,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Signal Extraction Helpers
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
function getStoriesByStatus(statusJson, status) {
|
|
49
|
+
if (!statusJson || !statusJson.stories) return [];
|
|
50
|
+
return Object.entries(statusJson.stories)
|
|
51
|
+
.filter(([, s]) => s.status === status)
|
|
52
|
+
.map(([id, s]) => ({ id, ...s }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStoriesForEpic(statusJson, epicId) {
|
|
56
|
+
if (!statusJson || !statusJson.stories) return [];
|
|
57
|
+
return Object.entries(statusJson.stories)
|
|
58
|
+
.filter(([, s]) => s.epic === epicId)
|
|
59
|
+
.map(([id, s]) => ({ id, ...s }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasPackageScript(packageJson, scriptName) {
|
|
63
|
+
return !!(packageJson && packageJson.scripts && packageJson.scripts[scriptName]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function storyHasAC(story) {
|
|
67
|
+
return !!(
|
|
68
|
+
story &&
|
|
69
|
+
story.acceptance_criteria &&
|
|
70
|
+
Array.isArray(story.acceptance_criteria) &&
|
|
71
|
+
story.acceptance_criteria.length > 0
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function storyMentions(story, keywords) {
|
|
76
|
+
if (!story) return false;
|
|
77
|
+
const text = `${story.title || ''} ${story.description || ''}`.toLowerCase();
|
|
78
|
+
return keywords.some(kw => text.includes(kw.toLowerCase()));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// FEATURE DETECTORS - Organized by Lifecycle Phase
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {Object} Signals
|
|
87
|
+
* @property {Object} statusJson - Parsed status.json
|
|
88
|
+
* @property {Object} sessionState - Parsed session-state.json
|
|
89
|
+
* @property {Object} metadata - Parsed agileflow-metadata.json
|
|
90
|
+
* @property {Object} git - Git signals { branch, filesChanged, isClean, onFeatureBranch, diffStats }
|
|
91
|
+
* @property {Object} packageJson - Parsed package.json
|
|
92
|
+
* @property {Object} story - Current story { id, status, title, owner, epic, acceptance_criteria }
|
|
93
|
+
* @property {Object} files - File existence checks { tsconfig, eslintrc, coverage, playwright, screenshots }
|
|
94
|
+
* @property {number} storyCount - Total stories in status.json
|
|
95
|
+
* @property {Object} counts - Story counts by status { ready, 'in-progress', blocked, done }
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
const FEATURE_DETECTORS = {
|
|
99
|
+
// =========================================================================
|
|
100
|
+
// PRE-STORY PHASE
|
|
101
|
+
// =========================================================================
|
|
102
|
+
|
|
103
|
+
'story-validate': (signals) => {
|
|
104
|
+
const { story } = signals;
|
|
105
|
+
if (!story || !story.id) return null;
|
|
106
|
+
if (story.status !== 'ready' && story.status !== 'in-progress') return null;
|
|
107
|
+
if (!storyHasAC(story)) {
|
|
108
|
+
return recommend('story-validate', {
|
|
109
|
+
priority: 'high',
|
|
110
|
+
trigger: `Story ${story.id} missing acceptance criteria`,
|
|
111
|
+
action: 'suggest',
|
|
112
|
+
phase: 'pre-story',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
'blockers': (signals) => {
|
|
119
|
+
const blocked = getStoriesByStatus(signals.statusJson, 'blocked');
|
|
120
|
+
if (blocked.length === 0) return null;
|
|
121
|
+
return recommend('blockers', {
|
|
122
|
+
priority: 'high',
|
|
123
|
+
trigger: `${blocked.length} blocked story(ies)`,
|
|
124
|
+
action: 'suggest',
|
|
125
|
+
phase: 'pre-story',
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
'choose': (signals) => {
|
|
130
|
+
const { story, counts } = signals;
|
|
131
|
+
if (story && story.id) return null; // Already have a story
|
|
132
|
+
if ((counts.ready || 0) < 2) return null;
|
|
133
|
+
return recommend('choose', {
|
|
134
|
+
priority: 'medium',
|
|
135
|
+
trigger: `${counts.ready} ready stories - use AI to pick the best one`,
|
|
136
|
+
action: 'offer',
|
|
137
|
+
phase: 'pre-story',
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
'assign': (signals) => {
|
|
142
|
+
const ready = getStoriesByStatus(signals.statusJson, 'ready');
|
|
143
|
+
const unassigned = ready.filter(s => !s.owner);
|
|
144
|
+
if (unassigned.length === 0) return null;
|
|
145
|
+
return recommend('assign', {
|
|
146
|
+
priority: 'low',
|
|
147
|
+
trigger: `${unassigned.length} ready stories without owner`,
|
|
148
|
+
action: 'offer',
|
|
149
|
+
phase: 'pre-story',
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
'board': (signals) => {
|
|
154
|
+
const { storyCount } = signals;
|
|
155
|
+
if (!storyCount || storyCount < 5) return null;
|
|
156
|
+
return recommend('board', {
|
|
157
|
+
priority: 'low',
|
|
158
|
+
trigger: `${storyCount} stories tracked - visual board available`,
|
|
159
|
+
action: 'offer',
|
|
160
|
+
phase: 'pre-story',
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
'sprint': (signals) => {
|
|
165
|
+
const { counts } = signals;
|
|
166
|
+
if ((counts.ready || 0) < 3) return null;
|
|
167
|
+
return recommend('sprint', {
|
|
168
|
+
priority: 'low',
|
|
169
|
+
trigger: `${counts.ready} ready stories - sprint planning available`,
|
|
170
|
+
action: 'offer',
|
|
171
|
+
phase: 'pre-story',
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
'batch': (signals) => {
|
|
176
|
+
const ready = getStoriesByStatus(signals.statusJson, 'ready');
|
|
177
|
+
if (ready.length < 5) return null;
|
|
178
|
+
// Check if stories share same epic (good batch candidate)
|
|
179
|
+
const epicGroups = {};
|
|
180
|
+
ready.forEach(s => {
|
|
181
|
+
const ep = s.epic || 'none';
|
|
182
|
+
epicGroups[ep] = (epicGroups[ep] || 0) + 1;
|
|
183
|
+
});
|
|
184
|
+
const epicGroupCounts = Object.values(epicGroups);
|
|
185
|
+
if (epicGroupCounts.length === 0) return null;
|
|
186
|
+
const maxGroup = Math.max(...epicGroupCounts);
|
|
187
|
+
if (maxGroup < 3) return null;
|
|
188
|
+
return recommend('batch', {
|
|
189
|
+
priority: 'medium',
|
|
190
|
+
trigger: `${maxGroup} ready stories in same epic - batch processing available`,
|
|
191
|
+
action: 'offer',
|
|
192
|
+
phase: 'pre-story',
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
'workflow': (signals) => {
|
|
197
|
+
const { metadata } = signals;
|
|
198
|
+
const workflows = metadata?.workflows;
|
|
199
|
+
if (!workflows || Object.keys(workflows).length === 0) return null;
|
|
200
|
+
return recommend('workflow', {
|
|
201
|
+
priority: 'low',
|
|
202
|
+
trigger: `${Object.keys(workflows).length} workflow template(s) configured`,
|
|
203
|
+
action: 'offer',
|
|
204
|
+
phase: 'pre-story',
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
'template': (signals) => {
|
|
209
|
+
const { story } = signals;
|
|
210
|
+
if (!story || story.status !== 'ready') return null;
|
|
211
|
+
if (!story.title) return null;
|
|
212
|
+
// Suggest template if story is a new doc/pattern type
|
|
213
|
+
if (storyMentions(story, ['template', 'boilerplate', 'scaffold', 'generator'])) {
|
|
214
|
+
return recommend('template', {
|
|
215
|
+
priority: 'low',
|
|
216
|
+
trigger: `Story mentions template/scaffold patterns`,
|
|
217
|
+
action: 'offer',
|
|
218
|
+
phase: 'pre-story',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
'configure': (signals) => {
|
|
225
|
+
const { metadata } = signals;
|
|
226
|
+
// Only suggest if metadata is minimal/missing
|
|
227
|
+
if (metadata && Object.keys(metadata).length > 3) return null;
|
|
228
|
+
return recommend('configure', {
|
|
229
|
+
priority: 'low',
|
|
230
|
+
trigger: 'Minimal AgileFlow configuration detected',
|
|
231
|
+
action: 'offer',
|
|
232
|
+
phase: 'pre-story',
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
// =========================================================================
|
|
237
|
+
// PLANNING PHASE
|
|
238
|
+
// =========================================================================
|
|
239
|
+
|
|
240
|
+
'impact': (signals) => {
|
|
241
|
+
const { git, story } = signals;
|
|
242
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
243
|
+
// Suggest impact analysis if touching core/shared files
|
|
244
|
+
const coreFilesChanged = (git.changedFiles || []).filter(f =>
|
|
245
|
+
/^(src\/(core|lib|shared)|lib\/|packages\/.*\/src\/)/.test(f)
|
|
246
|
+
).length;
|
|
247
|
+
if (coreFilesChanged < (signals.thresholds?.impact_min_files || 3)) return null;
|
|
248
|
+
return recommend('impact', {
|
|
249
|
+
priority: 'high',
|
|
250
|
+
trigger: `${coreFilesChanged} core/shared files being modified`,
|
|
251
|
+
action: 'suggest',
|
|
252
|
+
phase: 'planning',
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
'adr': (signals) => {
|
|
257
|
+
const { story } = signals;
|
|
258
|
+
if (!story || !story.id) return null;
|
|
259
|
+
if (storyMentions(story, ['architecture', 'redesign', 'migrate', 'replace', 'new system', 'framework'])) {
|
|
260
|
+
return recommend('adr', {
|
|
261
|
+
priority: 'medium',
|
|
262
|
+
trigger: 'Story involves architectural decisions',
|
|
263
|
+
action: 'suggest',
|
|
264
|
+
command: '/agileflow:adr',
|
|
265
|
+
phase: 'planning',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
'research': (signals) => {
|
|
272
|
+
const { story } = signals;
|
|
273
|
+
if (!story || !story.id) return null;
|
|
274
|
+
if (storyMentions(story, ['research', 'investigate', 'evaluate', 'compare', 'POC', 'proof of concept', 'spike'])) {
|
|
275
|
+
return recommend('research', {
|
|
276
|
+
priority: 'medium',
|
|
277
|
+
trigger: 'Story involves research/investigation',
|
|
278
|
+
action: 'suggest',
|
|
279
|
+
command: '/agileflow:research:ask',
|
|
280
|
+
phase: 'planning',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
'baseline': (signals) => {
|
|
287
|
+
const { story, files } = signals;
|
|
288
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
289
|
+
if (!files.coverage) return null;
|
|
290
|
+
// Only suggest baseline at start of work (planning phase)
|
|
291
|
+
return recommend('baseline', {
|
|
292
|
+
priority: 'medium',
|
|
293
|
+
trigger: 'Coverage data exists - mark baseline before changes',
|
|
294
|
+
action: 'offer',
|
|
295
|
+
phase: 'planning',
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
'council': (signals) => {
|
|
300
|
+
const { story } = signals;
|
|
301
|
+
if (!story || !story.id) return null;
|
|
302
|
+
if (storyMentions(story, ['strategic', 'trade-off', 'decision', 'approach', 'architecture'])) {
|
|
303
|
+
return recommend('council', {
|
|
304
|
+
priority: 'low',
|
|
305
|
+
trigger: 'Story involves strategic decision-making',
|
|
306
|
+
action: 'offer',
|
|
307
|
+
phase: 'planning',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
'multi-expert': (signals) => {
|
|
314
|
+
const { story } = signals;
|
|
315
|
+
if (!story || !story.id) return null;
|
|
316
|
+
if (storyMentions(story, ['complex', 'cross-cutting', 'full-stack', 'multi-domain'])) {
|
|
317
|
+
return recommend('multi-expert', {
|
|
318
|
+
priority: 'low',
|
|
319
|
+
trigger: 'Story involves multiple domains',
|
|
320
|
+
action: 'offer',
|
|
321
|
+
phase: 'planning',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
'validate-expertise': (signals) => {
|
|
328
|
+
const { files } = signals;
|
|
329
|
+
if (!files.expertiseDir) return null;
|
|
330
|
+
return recommend('validate-expertise', {
|
|
331
|
+
priority: 'low',
|
|
332
|
+
trigger: 'Expertise files exist - validate for drift',
|
|
333
|
+
action: 'offer',
|
|
334
|
+
phase: 'planning',
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// =========================================================================
|
|
339
|
+
// IMPLEMENTATION PHASE
|
|
340
|
+
// =========================================================================
|
|
341
|
+
|
|
342
|
+
'verify': (signals) => {
|
|
343
|
+
const { story, tests, git } = signals;
|
|
344
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
345
|
+
if ((git.filesChanged || 0) === 0) return null;
|
|
346
|
+
if (tests.passing === false) {
|
|
347
|
+
return recommend('verify', {
|
|
348
|
+
priority: 'high',
|
|
349
|
+
trigger: 'Tests are failing',
|
|
350
|
+
action: 'suggest',
|
|
351
|
+
phase: 'implementation',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
'tests': (signals) => {
|
|
358
|
+
const { story, files, packageJson } = signals;
|
|
359
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
360
|
+
if (!hasPackageScript(packageJson, 'test')) {
|
|
361
|
+
return recommend('tests', {
|
|
362
|
+
priority: 'medium',
|
|
363
|
+
trigger: 'No test script found - set up testing infrastructure',
|
|
364
|
+
action: 'suggest',
|
|
365
|
+
phase: 'implementation',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
'audit': (signals) => {
|
|
372
|
+
const { story, git } = signals;
|
|
373
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
374
|
+
if ((git.filesChanged || 0) < 5) return null;
|
|
375
|
+
return recommend('audit', {
|
|
376
|
+
priority: 'medium',
|
|
377
|
+
trigger: `${git.filesChanged} files changed - audit story completion`,
|
|
378
|
+
action: 'offer',
|
|
379
|
+
command: '/agileflow:audit',
|
|
380
|
+
phase: 'implementation',
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
'ci': (signals) => {
|
|
385
|
+
const { files } = signals;
|
|
386
|
+
if (files.ciConfig) return null; // Already has CI
|
|
387
|
+
return recommend('ci', {
|
|
388
|
+
priority: 'low',
|
|
389
|
+
trigger: 'No CI configuration detected',
|
|
390
|
+
action: 'offer',
|
|
391
|
+
phase: 'implementation',
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
'deps': (signals) => {
|
|
396
|
+
const { packageJson } = signals;
|
|
397
|
+
if (!packageJson) return null;
|
|
398
|
+
// Check for outdated or vulnerable deps signal
|
|
399
|
+
const depCount = Object.keys(packageJson.dependencies || {}).length +
|
|
400
|
+
Object.keys(packageJson.devDependencies || {}).length;
|
|
401
|
+
if (depCount < 10) return null;
|
|
402
|
+
return recommend('deps', {
|
|
403
|
+
priority: 'low',
|
|
404
|
+
trigger: `${depCount} dependencies - dependency graph available`,
|
|
405
|
+
action: 'offer',
|
|
406
|
+
phase: 'implementation',
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
'diagnose': (signals) => {
|
|
411
|
+
const { sessionState } = signals;
|
|
412
|
+
// Detect if there have been recent errors or stuck patterns
|
|
413
|
+
const failCount = sessionState?.failure_count || 0;
|
|
414
|
+
if (failCount < 2) return null;
|
|
415
|
+
return recommend('diagnose', {
|
|
416
|
+
priority: 'high',
|
|
417
|
+
trigger: `${failCount} recent failures detected - run diagnostics`,
|
|
418
|
+
action: 'suggest',
|
|
419
|
+
phase: 'implementation',
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
'debt': (signals) => {
|
|
424
|
+
const { story } = signals;
|
|
425
|
+
if (!story || !story.id) return null;
|
|
426
|
+
if (storyMentions(story, ['refactor', 'cleanup', 'tech debt', 'legacy', 'deprecat'])) {
|
|
427
|
+
return recommend('debt', {
|
|
428
|
+
priority: 'medium',
|
|
429
|
+
trigger: 'Story involves technical debt work',
|
|
430
|
+
action: 'offer',
|
|
431
|
+
phase: 'implementation',
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
'maintain': (signals) => {
|
|
438
|
+
const { story } = signals;
|
|
439
|
+
if (!story || !story.id) return null;
|
|
440
|
+
if (storyMentions(story, ['maintenance', 'update', 'upgrade', 'patch', 'housekeeping'])) {
|
|
441
|
+
return recommend('maintain', {
|
|
442
|
+
priority: 'low',
|
|
443
|
+
trigger: 'Story involves maintenance work',
|
|
444
|
+
action: 'offer',
|
|
445
|
+
phase: 'implementation',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
'packages': (signals) => {
|
|
452
|
+
const { story } = signals;
|
|
453
|
+
if (!story || !story.id) return null;
|
|
454
|
+
if (storyMentions(story, ['dependency', 'dependencies', 'package', 'upgrade', 'npm', 'vulnerability'])) {
|
|
455
|
+
return recommend('packages', {
|
|
456
|
+
priority: 'medium',
|
|
457
|
+
trigger: 'Story involves dependency management',
|
|
458
|
+
action: 'offer',
|
|
459
|
+
phase: 'implementation',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
'deploy': (signals) => {
|
|
466
|
+
const { story } = signals;
|
|
467
|
+
if (!story || !story.id) return null;
|
|
468
|
+
if (storyMentions(story, ['deploy', 'deployment', 'CD', 'pipeline', 'staging', 'production'])) {
|
|
469
|
+
return recommend('deploy', {
|
|
470
|
+
priority: 'medium',
|
|
471
|
+
trigger: 'Story involves deployment',
|
|
472
|
+
action: 'offer',
|
|
473
|
+
phase: 'implementation',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
'serve': (signals) => {
|
|
480
|
+
const { metadata } = signals;
|
|
481
|
+
const dashboardEnabled = metadata?.features?.dashboard?.enabled;
|
|
482
|
+
if (!dashboardEnabled) return null;
|
|
483
|
+
return recommend('serve', {
|
|
484
|
+
priority: 'low',
|
|
485
|
+
trigger: 'Dashboard server available',
|
|
486
|
+
action: 'offer',
|
|
487
|
+
phase: 'implementation',
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// =========================================================================
|
|
492
|
+
// POST-IMPLEMENTATION PHASE
|
|
493
|
+
// =========================================================================
|
|
494
|
+
|
|
495
|
+
'review': (signals) => {
|
|
496
|
+
const { git, story } = signals;
|
|
497
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
498
|
+
const linesChanged = (git.diffStats?.insertions || 0) + (git.diffStats?.deletions || 0);
|
|
499
|
+
if (linesChanged < (signals.thresholds?.review_min_lines || 100)) return null;
|
|
500
|
+
return recommend('review', {
|
|
501
|
+
priority: 'high',
|
|
502
|
+
trigger: `${linesChanged} lines changed - code review recommended`,
|
|
503
|
+
action: 'suggest',
|
|
504
|
+
phase: 'post-impl',
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
'logic-audit': (signals) => {
|
|
509
|
+
const { git, story } = signals;
|
|
510
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
511
|
+
// Suggest logic audit for complex changes
|
|
512
|
+
const coreFiles = (git.changedFiles || []).filter(f =>
|
|
513
|
+
/\.(js|ts|jsx|tsx|py|go|rs)$/.test(f)
|
|
514
|
+
).length;
|
|
515
|
+
if (coreFiles < 3) return null;
|
|
516
|
+
return recommend('logic-audit', {
|
|
517
|
+
priority: 'medium',
|
|
518
|
+
trigger: `${coreFiles} source files modified - logic audit available`,
|
|
519
|
+
action: 'offer',
|
|
520
|
+
command: '/agileflow:logic:audit',
|
|
521
|
+
phase: 'post-impl',
|
|
522
|
+
});
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
'docs': (signals) => {
|
|
526
|
+
const { git, story } = signals;
|
|
527
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
528
|
+
// Detect API or public interface changes
|
|
529
|
+
const apiFiles = (git.changedFiles || []).filter(f =>
|
|
530
|
+
/\b(api|route|endpoint|handler|controller|schema)\b/i.test(f)
|
|
531
|
+
).length;
|
|
532
|
+
if (apiFiles === 0) return null;
|
|
533
|
+
return recommend('docs', {
|
|
534
|
+
priority: 'medium',
|
|
535
|
+
trigger: `${apiFiles} API/interface files changed - docs sync recommended`,
|
|
536
|
+
action: 'suggest',
|
|
537
|
+
phase: 'post-impl',
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
'changelog': (signals) => {
|
|
542
|
+
const { git } = signals;
|
|
543
|
+
// Suggest changelog if there are multiple commits on feature branch
|
|
544
|
+
if (!git.onFeatureBranch) return null;
|
|
545
|
+
if ((git.commitCount || 0) < 3) return null;
|
|
546
|
+
return recommend('changelog', {
|
|
547
|
+
priority: 'low',
|
|
548
|
+
trigger: `${git.commitCount} commits on feature branch - changelog entry recommended`,
|
|
549
|
+
action: 'offer',
|
|
550
|
+
phase: 'post-impl',
|
|
551
|
+
});
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
'metrics': (signals) => {
|
|
555
|
+
const { statusJson } = signals;
|
|
556
|
+
if (!statusJson || !statusJson.stories) return null;
|
|
557
|
+
const doneCount = getStoriesByStatus(statusJson, 'done').length;
|
|
558
|
+
if (doneCount < 5) return null;
|
|
559
|
+
return recommend('metrics', {
|
|
560
|
+
priority: 'low',
|
|
561
|
+
trigger: `${doneCount} completed stories - metrics dashboard available`,
|
|
562
|
+
action: 'offer',
|
|
563
|
+
phase: 'post-impl',
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
'retro': (signals) => {
|
|
568
|
+
const { statusJson } = signals;
|
|
569
|
+
if (!statusJson || !statusJson.epics) return null;
|
|
570
|
+
// Suggest retro when an epic is mostly complete
|
|
571
|
+
const epics = statusJson.epics || {};
|
|
572
|
+
for (const [epId, ep] of Object.entries(epics)) {
|
|
573
|
+
if (!ep) continue;
|
|
574
|
+
if (ep.status === 'done' || ep.progress >= 90) {
|
|
575
|
+
return recommend('retro', {
|
|
576
|
+
priority: 'medium',
|
|
577
|
+
trigger: `Epic ${epId} is ${ep.status === 'done' ? 'complete' : `${ep.progress ?? 0}% done`} - retrospective recommended`,
|
|
578
|
+
action: 'offer',
|
|
579
|
+
phase: 'post-impl',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
'velocity': (signals) => {
|
|
587
|
+
const { statusJson } = signals;
|
|
588
|
+
if (!statusJson || !statusJson.stories) return null;
|
|
589
|
+
const doneCount = getStoriesByStatus(statusJson, 'done').length;
|
|
590
|
+
if (doneCount < 10) return null;
|
|
591
|
+
return recommend('velocity', {
|
|
592
|
+
priority: 'low',
|
|
593
|
+
trigger: `${doneCount} completed stories - velocity tracking available`,
|
|
594
|
+
action: 'offer',
|
|
595
|
+
phase: 'post-impl',
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
'readme-sync': (signals) => {
|
|
600
|
+
const { git } = signals;
|
|
601
|
+
// Check if any README files were potentially affected
|
|
602
|
+
const readmeAffected = (git.changedFiles || []).some(f =>
|
|
603
|
+
/readme/i.test(f) || /^(src|packages|apps)\/[^/]+\//.test(f)
|
|
604
|
+
);
|
|
605
|
+
if (!readmeAffected) return null;
|
|
606
|
+
return recommend('readme-sync', {
|
|
607
|
+
priority: 'low',
|
|
608
|
+
trigger: 'Structural changes detected - README sync available',
|
|
609
|
+
action: 'offer',
|
|
610
|
+
phase: 'post-impl',
|
|
611
|
+
});
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
'feedback': (signals) => {
|
|
615
|
+
const { sessionState } = signals;
|
|
616
|
+
// Suggest feedback collection after extended sessions
|
|
617
|
+
const sessionDuration = sessionState?.current_session?.started_at
|
|
618
|
+
? Math.round((Date.now() - new Date(sessionState.current_session.started_at).getTime()) / 60000)
|
|
619
|
+
: 0;
|
|
620
|
+
if (isNaN(sessionDuration) || sessionDuration < 30) return null;
|
|
621
|
+
return recommend('feedback', {
|
|
622
|
+
priority: 'low',
|
|
623
|
+
trigger: `${sessionDuration}min session - consider capturing feedback`,
|
|
624
|
+
action: 'offer',
|
|
625
|
+
phase: 'post-impl',
|
|
626
|
+
});
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
// =========================================================================
|
|
630
|
+
// PRE-PR PHASE
|
|
631
|
+
// =========================================================================
|
|
632
|
+
|
|
633
|
+
'pr': (signals) => {
|
|
634
|
+
const { git, tests, story } = signals;
|
|
635
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
636
|
+
if (!git.onFeatureBranch) return null;
|
|
637
|
+
if (tests.passing !== true) return null;
|
|
638
|
+
return recommend('pr', {
|
|
639
|
+
priority: 'high',
|
|
640
|
+
trigger: 'Tests passing on feature branch - ready for PR',
|
|
641
|
+
action: 'suggest',
|
|
642
|
+
phase: 'pre-pr',
|
|
643
|
+
});
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
'compress': (signals) => {
|
|
647
|
+
const { statusJson } = signals;
|
|
648
|
+
if (!statusJson || !statusJson.stories) return null;
|
|
649
|
+
const totalStories = Object.keys(statusJson.stories).length;
|
|
650
|
+
if (totalStories < (signals.thresholds?.compress_min_stories || 100)) return null;
|
|
651
|
+
return recommend('compress', {
|
|
652
|
+
priority: 'medium',
|
|
653
|
+
trigger: `${totalStories} stories in status.json - compression recommended`,
|
|
654
|
+
action: 'suggest',
|
|
655
|
+
phase: 'pre-pr',
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// =============================================================================
|
|
661
|
+
// Phase Mapping (which detectors belong to which phase)
|
|
662
|
+
// =============================================================================
|
|
663
|
+
|
|
664
|
+
const PHASE_MAP = {
|
|
665
|
+
'pre-story': [
|
|
666
|
+
'story-validate', 'blockers', 'choose', 'assign', 'board',
|
|
667
|
+
'sprint', 'batch', 'workflow', 'template', 'configure',
|
|
668
|
+
],
|
|
669
|
+
'planning': [
|
|
670
|
+
'impact', 'adr', 'research', 'baseline', 'council',
|
|
671
|
+
'multi-expert', 'validate-expertise',
|
|
672
|
+
],
|
|
673
|
+
'implementation': [
|
|
674
|
+
'verify', 'tests', 'audit', 'ci', 'deps',
|
|
675
|
+
'diagnose', 'debt', 'maintain', 'packages', 'deploy', 'serve',
|
|
676
|
+
],
|
|
677
|
+
'post-impl': [
|
|
678
|
+
'review', 'logic-audit', 'docs', 'changelog', 'metrics',
|
|
679
|
+
'retro', 'velocity', 'readme-sync', 'feedback',
|
|
680
|
+
],
|
|
681
|
+
'pre-pr': [
|
|
682
|
+
'pr', 'compress',
|
|
683
|
+
],
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Get all detector names.
|
|
688
|
+
* @returns {string[]}
|
|
689
|
+
*/
|
|
690
|
+
function getDetectorNames() {
|
|
691
|
+
return Object.keys(FEATURE_DETECTORS);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Get detectors for a specific phase.
|
|
696
|
+
* @param {string} phase
|
|
697
|
+
* @returns {string[]}
|
|
698
|
+
*/
|
|
699
|
+
function getDetectorsForPhase(phase) {
|
|
700
|
+
return PHASE_MAP[phase] || [];
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Run a single detector by name.
|
|
705
|
+
* @param {string} name - Detector name
|
|
706
|
+
* @param {Signals} signals - Project signals
|
|
707
|
+
* @returns {Object|null} Recommendation or null
|
|
708
|
+
*/
|
|
709
|
+
function runDetector(name, signals) {
|
|
710
|
+
const detector = FEATURE_DETECTORS[name];
|
|
711
|
+
if (!detector) return null;
|
|
712
|
+
try {
|
|
713
|
+
return detector(signals);
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Run all detectors for given phases.
|
|
721
|
+
* @param {string[]} phases - Array of phase names
|
|
722
|
+
* @param {Signals} signals - Project signals
|
|
723
|
+
* @returns {Object[]} Array of recommendations
|
|
724
|
+
*/
|
|
725
|
+
function runDetectorsForPhases(phases, signals) {
|
|
726
|
+
const results = [];
|
|
727
|
+
const seen = new Set();
|
|
728
|
+
|
|
729
|
+
for (const phase of phases) {
|
|
730
|
+
const detectorNames = PHASE_MAP[phase] || [];
|
|
731
|
+
for (const name of detectorNames) {
|
|
732
|
+
if (seen.has(name)) continue;
|
|
733
|
+
seen.add(name);
|
|
734
|
+
const result = runDetector(name, signals);
|
|
735
|
+
if (result) {
|
|
736
|
+
results.push(result);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return results;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Run all detectors (all phases).
|
|
746
|
+
* @param {Signals} signals - Project signals
|
|
747
|
+
* @returns {Object[]} Array of all triggered recommendations
|
|
748
|
+
*/
|
|
749
|
+
function runAllDetectors(signals) {
|
|
750
|
+
const results = [];
|
|
751
|
+
for (const [name, detector] of Object.entries(FEATURE_DETECTORS)) {
|
|
752
|
+
try {
|
|
753
|
+
const result = detector(signals);
|
|
754
|
+
if (result) {
|
|
755
|
+
results.push(result);
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
// Skip failed detectors
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return results;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
module.exports = {
|
|
765
|
+
FEATURE_DETECTORS,
|
|
766
|
+
PHASE_MAP,
|
|
767
|
+
recommend,
|
|
768
|
+
getDetectorNames,
|
|
769
|
+
getDetectorsForPhase,
|
|
770
|
+
runDetector,
|
|
771
|
+
runDetectorsForPhases,
|
|
772
|
+
runAllDetectors,
|
|
773
|
+
// Helpers exported for testing
|
|
774
|
+
getStoriesByStatus,
|
|
775
|
+
getStoriesForEpic,
|
|
776
|
+
hasPackageScript,
|
|
777
|
+
storyHasAC,
|
|
778
|
+
storyMentions,
|
|
779
|
+
};
|