agileflow 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/scripts/agileflow-welcome.js +79 -0
- package/scripts/claude-tmux.sh +12 -36
- package/scripts/lib/ac-test-matcher.js +452 -0
- package/scripts/lib/audit-registry.js +58 -2
- package/scripts/lib/configure-features.js +35 -0
- package/scripts/lib/model-profiles.js +25 -5
- package/scripts/lib/quality-gates.js +163 -0
- package/scripts/lib/signal-detectors.js +43 -0
- package/scripts/lib/status-writer.js +255 -0
- package/scripts/lib/story-claiming.js +128 -45
- package/scripts/lib/task-sync.js +32 -38
- package/scripts/lib/tmux-audit-monitor.js +611 -0
- package/scripts/lib/tool-registry.yaml +241 -0
- package/scripts/lib/tool-shed.js +441 -0
- package/scripts/native-team-observer.js +219 -0
- package/scripts/obtain-context.js +14 -0
- package/scripts/ralph-loop.js +30 -5
- package/scripts/smart-detect.js +21 -0
- package/scripts/spawn-audit-sessions.js +372 -44
- package/scripts/team-manager.js +19 -0
- package/src/core/agents/a11y-analyzer-aria.md +155 -0
- package/src/core/agents/a11y-analyzer-forms.md +162 -0
- package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
- package/src/core/agents/a11y-analyzer-semantic.md +153 -0
- package/src/core/agents/a11y-analyzer-visual.md +158 -0
- package/src/core/agents/a11y-consensus.md +248 -0
- package/src/core/agents/ads-consensus.md +74 -0
- package/src/core/agents/ads-generate.md +145 -0
- package/src/core/agents/ads-performance-tracker.md +197 -0
- package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
- package/src/core/agents/api-quality-analyzer-docs.md +176 -0
- package/src/core/agents/api-quality-analyzer-errors.md +183 -0
- package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
- package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
- package/src/core/agents/api-quality-consensus.md +214 -0
- package/src/core/agents/arch-analyzer-circular.md +148 -0
- package/src/core/agents/arch-analyzer-complexity.md +171 -0
- package/src/core/agents/arch-analyzer-coupling.md +146 -0
- package/src/core/agents/arch-analyzer-layering.md +151 -0
- package/src/core/agents/arch-analyzer-patterns.md +162 -0
- package/src/core/agents/arch-consensus.md +227 -0
- package/src/core/commands/adr.md +1 -0
- package/src/core/commands/ads/generate.md +238 -0
- package/src/core/commands/ads/health.md +327 -0
- package/src/core/commands/ads/test-plan.md +317 -0
- package/src/core/commands/ads/track.md +288 -0
- package/src/core/commands/ads.md +28 -16
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/audit.md +43 -6
- package/src/core/commands/babysit.md +90 -6
- package/src/core/commands/baseline.md +1 -0
- package/src/core/commands/blockers.md +1 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +1 -0
- package/src/core/commands/choose.md +1 -0
- package/src/core/commands/ci.md +1 -0
- package/src/core/commands/code/accessibility.md +347 -0
- package/src/core/commands/code/api.md +297 -0
- package/src/core/commands/code/architecture.md +297 -0
- package/src/core/commands/code/completeness.md +43 -6
- package/src/core/commands/code/legal.md +43 -6
- package/src/core/commands/code/logic.md +43 -6
- package/src/core/commands/code/performance.md +43 -6
- package/src/core/commands/code/security.md +43 -6
- package/src/core/commands/code/test.md +43 -6
- package/src/core/commands/configure.md +1 -0
- package/src/core/commands/council.md +1 -0
- package/src/core/commands/deploy.md +1 -0
- package/src/core/commands/diagnose.md +1 -0
- package/src/core/commands/docs.md +1 -0
- package/src/core/commands/epic/edit.md +213 -0
- package/src/core/commands/epic.md +1 -0
- package/src/core/commands/export.md +238 -0
- package/src/core/commands/help.md +16 -1
- package/src/core/commands/ideate/discover.md +7 -3
- package/src/core/commands/ideate/features.md +65 -4
- package/src/core/commands/ideate/new.md +158 -124
- package/src/core/commands/impact.md +1 -0
- package/src/core/commands/learn/explain.md +118 -0
- package/src/core/commands/learn/glossary.md +135 -0
- package/src/core/commands/learn/patterns.md +138 -0
- package/src/core/commands/learn/tour.md +126 -0
- package/src/core/commands/migrate/codemods.md +151 -0
- package/src/core/commands/migrate/plan.md +131 -0
- package/src/core/commands/migrate/scan.md +114 -0
- package/src/core/commands/migrate/validate.md +119 -0
- package/src/core/commands/multi-expert.md +1 -0
- package/src/core/commands/pr.md +1 -0
- package/src/core/commands/review.md +1 -0
- package/src/core/commands/sprint.md +1 -0
- package/src/core/commands/status/undo.md +191 -0
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story/edit.md +204 -0
- package/src/core/commands/story/view.md +29 -7
- package/src/core/commands/story-validate.md +1 -0
- package/src/core/commands/story.md +1 -0
- package/src/core/commands/tdd.md +1 -0
- package/src/core/commands/team/start.md +10 -6
- package/src/core/commands/tests.md +1 -0
- package/src/core/commands/verify.md +27 -1
- package/src/core/commands/workflow.md +2 -0
- package/src/core/teams/backend.json +41 -0
- package/src/core/teams/frontend.json +41 -0
- package/src/core/teams/qa.json +41 -0
- package/src/core/teams/solo.json +35 -0
- package/src/core/templates/agileflow-metadata.json +5 -0
- package/tools/cli/commands/setup.js +85 -3
- package/tools/cli/commands/update.js +42 -0
- package/tools/cli/installers/ide/claude-code.js +68 -0
|
@@ -132,6 +132,62 @@ const AUDIT_TYPES = {
|
|
|
132
132
|
deep_analyzers: ['handlers', 'routes', 'api', 'stubs', 'state', 'imports', 'conditional'],
|
|
133
133
|
},
|
|
134
134
|
|
|
135
|
+
brainstorm: {
|
|
136
|
+
name: 'Feature Brainstorm',
|
|
137
|
+
prefix: 'Brain',
|
|
138
|
+
color: '#c0caf5', // lavender
|
|
139
|
+
command: 'ideate/features',
|
|
140
|
+
analyzers: {
|
|
141
|
+
features: { subagent_type: 'brainstorm-analyzer-features', label: 'Feature Gaps' },
|
|
142
|
+
ux: { subagent_type: 'brainstorm-analyzer-ux', label: 'UX Improvements' },
|
|
143
|
+
market: { subagent_type: 'brainstorm-analyzer-market', label: 'Market Features' },
|
|
144
|
+
growth: { subagent_type: 'brainstorm-analyzer-growth', label: 'Growth & Engagement' },
|
|
145
|
+
integration: { subagent_type: 'brainstorm-analyzer-integration', label: 'Integrations' },
|
|
146
|
+
},
|
|
147
|
+
consensus: { subagent_type: 'brainstorm-consensus', label: 'Brainstorm Consensus' },
|
|
148
|
+
quick_analyzers: ['features', 'ux', 'market'],
|
|
149
|
+
deep_analyzers: ['features', 'ux', 'market', 'growth', 'integration'],
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
ideate: {
|
|
153
|
+
name: 'Ideation',
|
|
154
|
+
prefix: 'Idea',
|
|
155
|
+
color: '#ff9e64', // orange
|
|
156
|
+
command: 'ideate/new',
|
|
157
|
+
analyzers: {
|
|
158
|
+
security: { subagent_type: 'agileflow-security', label: 'Security' },
|
|
159
|
+
performance: { subagent_type: 'agileflow-performance', label: 'Performance' },
|
|
160
|
+
refactor: { subagent_type: 'agileflow-refactor', label: 'Code Quality' },
|
|
161
|
+
ui: { subagent_type: 'agileflow-ui', label: 'UX/Design' },
|
|
162
|
+
testing: { subagent_type: 'agileflow-testing', label: 'Testing' },
|
|
163
|
+
api: { subagent_type: 'agileflow-api', label: 'API/Architecture' },
|
|
164
|
+
accessibility: { subagent_type: 'agileflow-accessibility', label: 'Accessibility' },
|
|
165
|
+
compliance: { subagent_type: 'agileflow-compliance', label: 'Compliance' },
|
|
166
|
+
database: { subagent_type: 'agileflow-database', label: 'Database' },
|
|
167
|
+
monitoring: { subagent_type: 'agileflow-monitoring', label: 'Monitoring' },
|
|
168
|
+
qa: { subagent_type: 'agileflow-qa', label: 'QA' },
|
|
169
|
+
analytics: { subagent_type: 'agileflow-analytics', label: 'Analytics' },
|
|
170
|
+
documentation: { subagent_type: 'agileflow-documentation', label: 'Documentation' },
|
|
171
|
+
},
|
|
172
|
+
consensus: null, // ideation does its own synthesis (no consensus coordinator)
|
|
173
|
+
quick_analyzers: ['security', 'performance', 'refactor', 'ui', 'testing', 'api'],
|
|
174
|
+
deep_analyzers: [
|
|
175
|
+
'security',
|
|
176
|
+
'performance',
|
|
177
|
+
'refactor',
|
|
178
|
+
'ui',
|
|
179
|
+
'testing',
|
|
180
|
+
'api',
|
|
181
|
+
'accessibility',
|
|
182
|
+
'compliance',
|
|
183
|
+
'database',
|
|
184
|
+
'monitoring',
|
|
185
|
+
'qa',
|
|
186
|
+
'analytics',
|
|
187
|
+
'documentation',
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
|
|
135
191
|
legal: {
|
|
136
192
|
name: 'Legal Risk',
|
|
137
193
|
prefix: 'Legal',
|
|
@@ -167,7 +223,7 @@ const AUDIT_TYPES = {
|
|
|
167
223
|
/**
|
|
168
224
|
* Get audit type configuration.
|
|
169
225
|
*
|
|
170
|
-
* @param {string} type - Audit type key (logic, security, performance, test, completeness, legal)
|
|
226
|
+
* @param {string} type - Audit type key (logic, security, performance, test, completeness, legal, ideate)
|
|
171
227
|
* @returns {object|null} Audit type config or null if invalid
|
|
172
228
|
*/
|
|
173
229
|
function getAuditType(type) {
|
|
@@ -195,7 +251,7 @@ function getAnalyzersForAudit(type, depth, focus) {
|
|
|
195
251
|
const audit = AUDIT_TYPES[type];
|
|
196
252
|
if (!audit) return null;
|
|
197
253
|
|
|
198
|
-
const effectiveDepth = depth === 'ultradeep' ? 'deep' : depth || 'quick';
|
|
254
|
+
const effectiveDepth = depth === 'ultradeep' || depth === 'extreme' ? 'deep' : depth || 'quick';
|
|
199
255
|
const analyzerKeys = effectiveDepth === 'deep' ? audit.deep_analyzers : audit.quick_analyzers;
|
|
200
256
|
|
|
201
257
|
// Filter by focus if specified
|
|
@@ -393,6 +393,25 @@ function enableFeature(feature, options = {}, version) {
|
|
|
393
393
|
if (feature === 'agentteams') {
|
|
394
394
|
settings.env = settings.env || {};
|
|
395
395
|
settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
|
396
|
+
|
|
397
|
+
// Register PostToolUse hooks for native team observability
|
|
398
|
+
if (!settings.hooks) settings.hooks = {};
|
|
399
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
400
|
+
const observerCmd = 'node $CLAUDE_PROJECT_DIR/.agileflow/scripts/native-team-observer.js';
|
|
401
|
+
for (const matcher of ['TeamCreate', 'SendMessage', 'ListTeams']) {
|
|
402
|
+
const exists = settings.hooks.PostToolUse.some(
|
|
403
|
+
h =>
|
|
404
|
+
h.matcher === matcher &&
|
|
405
|
+
h.hooks?.some(hk => hk.command && hk.command.includes('native-team-observer'))
|
|
406
|
+
);
|
|
407
|
+
if (!exists) {
|
|
408
|
+
settings.hooks.PostToolUse.push({
|
|
409
|
+
matcher,
|
|
410
|
+
hooks: [{ type: 'command', command: observerCmd, timeout: 5000 }],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
396
415
|
writeJSON('.claude/settings.json', settings);
|
|
397
416
|
updateMetadata(
|
|
398
417
|
{
|
|
@@ -408,6 +427,7 @@ function enableFeature(feature, options = {}, version) {
|
|
|
408
427
|
);
|
|
409
428
|
success('Native Agent Teams enabled');
|
|
410
429
|
info('Set CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 in .claude/settings.json');
|
|
430
|
+
info('Registered PostToolUse hooks for native team observability');
|
|
411
431
|
info('Claude Code will use native TeamCreate/SendMessage tools');
|
|
412
432
|
info('Fallback: subagent mode (Task/TaskOutput) when native is unavailable');
|
|
413
433
|
return true;
|
|
@@ -954,6 +974,20 @@ function disableFeature(feature, version) {
|
|
|
954
974
|
delete settings.env;
|
|
955
975
|
}
|
|
956
976
|
}
|
|
977
|
+
|
|
978
|
+
// Remove PostToolUse hooks for native team observer
|
|
979
|
+
if (settings.hooks?.PostToolUse) {
|
|
980
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
981
|
+
h => !h.hooks?.some(hk => hk.command && hk.command.includes('native-team-observer'))
|
|
982
|
+
);
|
|
983
|
+
if (settings.hooks.PostToolUse.length === 0) {
|
|
984
|
+
delete settings.hooks.PostToolUse;
|
|
985
|
+
}
|
|
986
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
987
|
+
delete settings.hooks;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
957
991
|
writeJSON('.claude/settings.json', settings);
|
|
958
992
|
updateMetadata(
|
|
959
993
|
{
|
|
@@ -969,6 +1003,7 @@ function disableFeature(feature, version) {
|
|
|
969
1003
|
);
|
|
970
1004
|
success('Native Agent Teams disabled');
|
|
971
1005
|
info('Removed CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS from .claude/settings.json');
|
|
1006
|
+
info('Removed PostToolUse hooks for native team observer');
|
|
972
1007
|
info('AgileFlow will use subagent mode (Task/TaskOutput) for multi-agent orchestration');
|
|
973
1008
|
return true;
|
|
974
1009
|
}
|
|
@@ -60,9 +60,10 @@ function isValidModel(model) {
|
|
|
60
60
|
*
|
|
61
61
|
* @param {string} model - Model name
|
|
62
62
|
* @param {number} [analyzerCount=5] - Number of analyzers
|
|
63
|
-
* @
|
|
63
|
+
* @param {number} [partitions=1] - Number of partitions (extreme mode)
|
|
64
|
+
* @returns {{ multiplier: number, model: string, perAnalyzerCost: string, totalEstimate: string, partitions?: number, totalSessions?: number }}
|
|
64
65
|
*/
|
|
65
|
-
function estimateCost(model, analyzerCount) {
|
|
66
|
+
function estimateCost(model, analyzerCount, partitions) {
|
|
66
67
|
let MODEL_PRICING;
|
|
67
68
|
try {
|
|
68
69
|
MODEL_PRICING = require('./team-events').MODEL_PRICING;
|
|
@@ -75,19 +76,38 @@ function estimateCost(model, analyzerCount) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
const count = analyzerCount || 5;
|
|
79
|
+
const partCount = typeof partitions === 'number' && partitions > 1 ? partitions : 1;
|
|
78
80
|
const resolved = resolveModel(model);
|
|
79
81
|
const pricing = MODEL_PRICING[resolved] || MODEL_PRICING.haiku;
|
|
80
82
|
const haikuPricing = MODEL_PRICING.haiku;
|
|
81
83
|
|
|
82
84
|
const multiplier = pricing.output / haikuPricing.output;
|
|
83
|
-
const
|
|
85
|
+
const perAnalyzerCostNum =
|
|
86
|
+
(pricing.input * 50000) / 1_000_000 + (pricing.output * 10000) / 1_000_000;
|
|
87
|
+
const perAnalyzer = `$${perAnalyzerCostNum.toFixed(3)}`;
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
// For extreme mode: each partition has a coordinator + all analyzers as sub-agents
|
|
90
|
+
// Estimated cost per partition coordinator session in USD (~10k input + 2k output at haiku rates)
|
|
91
|
+
const coordinatorCostUSD = 0.05;
|
|
92
|
+
const totalSessions = partCount * count;
|
|
93
|
+
const totalCost =
|
|
94
|
+
partCount > 1
|
|
95
|
+
? partCount * coordinatorCostUSD + totalSessions * perAnalyzerCostNum
|
|
96
|
+
: count * perAnalyzerCostNum;
|
|
97
|
+
|
|
98
|
+
const result = {
|
|
86
99
|
multiplier: Math.round(multiplier * 100) / 100,
|
|
87
100
|
model: resolved,
|
|
88
101
|
perAnalyzerCost: perAnalyzer,
|
|
89
|
-
totalEstimate: `~$${
|
|
102
|
+
totalEstimate: `~$${totalCost.toFixed(2)}`,
|
|
90
103
|
};
|
|
104
|
+
|
|
105
|
+
if (partCount > 1) {
|
|
106
|
+
result.partitions = partCount;
|
|
107
|
+
result.totalSessions = totalSessions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
module.exports = {
|
|
@@ -561,6 +561,164 @@ function createValidationReport(gateResults, options = {}) {
|
|
|
561
561
|
return lines.join('\n');
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
// ============================================================================
|
|
565
|
+
// CI Feedback Loop
|
|
566
|
+
// ============================================================================
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Default CI feedback loop configuration
|
|
570
|
+
*/
|
|
571
|
+
const CI_FEEDBACK_DEFAULTS = {
|
|
572
|
+
enabled: true,
|
|
573
|
+
max_rounds: 3,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Load CI feedback loop config from agileflow-metadata.json
|
|
578
|
+
* @param {string} projectRoot - Project root directory
|
|
579
|
+
* @returns {Object} CI feedback loop config
|
|
580
|
+
*/
|
|
581
|
+
function loadCIFeedbackConfig(projectRoot) {
|
|
582
|
+
const metadataPath = path.join(projectRoot, 'docs', '00-meta', 'agileflow-metadata.json');
|
|
583
|
+
try {
|
|
584
|
+
if (fs.existsSync(metadataPath)) {
|
|
585
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
586
|
+
if (metadata.ci_feedback_loops) {
|
|
587
|
+
return {
|
|
588
|
+
...CI_FEEDBACK_DEFAULTS,
|
|
589
|
+
...metadata.ci_feedback_loops,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
} catch {
|
|
594
|
+
// Fall through to defaults
|
|
595
|
+
}
|
|
596
|
+
return { ...CI_FEEDBACK_DEFAULTS };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Execute a CI feedback loop - runs gates, and if they fail, returns
|
|
601
|
+
* structured feedback for the agent to retry (up to max_rounds).
|
|
602
|
+
*
|
|
603
|
+
* This implements the Stripe "Minions" pattern: deterministic CI check
|
|
604
|
+
* followed by agent retry, with a hard iteration limit.
|
|
605
|
+
*
|
|
606
|
+
* @param {Object[]} gates - Quality gate definitions to check
|
|
607
|
+
* @param {Object} options - Loop options
|
|
608
|
+
* @param {string} [options.projectRoot] - Project root directory
|
|
609
|
+
* @param {number} [options.maxRounds] - Override max retry rounds (default: from config)
|
|
610
|
+
* @param {number} [options.currentRound] - Current round number (1-based, default: 1)
|
|
611
|
+
* @param {string} [options.cwd] - Working directory for gate execution
|
|
612
|
+
* @returns {Object} Loop result with status and agent feedback
|
|
613
|
+
*/
|
|
614
|
+
function executeCIFeedbackLoop(gates, options = {}) {
|
|
615
|
+
const { projectRoot = process.cwd(), maxRounds, currentRound = 1, cwd } = options;
|
|
616
|
+
|
|
617
|
+
const config = loadCIFeedbackConfig(projectRoot);
|
|
618
|
+
|
|
619
|
+
if (!config.enabled) {
|
|
620
|
+
return {
|
|
621
|
+
status: 'disabled',
|
|
622
|
+
message: 'CI feedback loops are disabled in agileflow-metadata.json',
|
|
623
|
+
should_retry: false,
|
|
624
|
+
round: currentRound,
|
|
625
|
+
max_rounds: 0,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const effectiveMaxRounds = maxRounds || config.max_rounds || CI_FEEDBACK_DEFAULTS.max_rounds;
|
|
630
|
+
|
|
631
|
+
// Execute all gates
|
|
632
|
+
const gateResults = executeGates(gates, { cwd, stopOnFailure: false });
|
|
633
|
+
|
|
634
|
+
if (gateResults.passed) {
|
|
635
|
+
return {
|
|
636
|
+
status: 'passed',
|
|
637
|
+
message: `All ${gateResults.passed_count} gates passed on round ${currentRound}`,
|
|
638
|
+
should_retry: false,
|
|
639
|
+
round: currentRound,
|
|
640
|
+
max_rounds: effectiveMaxRounds,
|
|
641
|
+
gate_results: gateResults,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Gates failed - determine if we should retry
|
|
646
|
+
const hasRoundsLeft = currentRound < effectiveMaxRounds;
|
|
647
|
+
|
|
648
|
+
if (!hasRoundsLeft) {
|
|
649
|
+
return {
|
|
650
|
+
status: 'exhausted',
|
|
651
|
+
message: `Gates failed after ${currentRound}/${effectiveMaxRounds} rounds. Escalating to human.`,
|
|
652
|
+
should_retry: false,
|
|
653
|
+
round: currentRound,
|
|
654
|
+
max_rounds: effectiveMaxRounds,
|
|
655
|
+
gate_results: gateResults,
|
|
656
|
+
failures: gateResults.results
|
|
657
|
+
.filter(r => r.status === GATE_STATUS.FAILED || r.status === GATE_STATUS.ERROR)
|
|
658
|
+
.map(r => ({
|
|
659
|
+
gate: r.gate,
|
|
660
|
+
message: r.message,
|
|
661
|
+
output: r.output,
|
|
662
|
+
error: r.error,
|
|
663
|
+
})),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Build structured feedback for agent retry
|
|
668
|
+
const failures = gateResults.results.filter(
|
|
669
|
+
r => r.status === GATE_STATUS.FAILED || r.status === GATE_STATUS.ERROR
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const feedbackLines = [
|
|
673
|
+
`## CI Feedback Loop - Round ${currentRound}/${effectiveMaxRounds}`,
|
|
674
|
+
'',
|
|
675
|
+
`**${failures.length} gate(s) failed.** ${effectiveMaxRounds - currentRound} retry round(s) remaining.`,
|
|
676
|
+
'',
|
|
677
|
+
'### Failures',
|
|
678
|
+
'',
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
for (const failure of failures) {
|
|
682
|
+
feedbackLines.push(`#### ${failure.gate} (${failure.type})`);
|
|
683
|
+
feedbackLines.push(`- **Status**: ${failure.status}`);
|
|
684
|
+
feedbackLines.push(`- **Message**: ${failure.message}`);
|
|
685
|
+
if (failure.output) {
|
|
686
|
+
feedbackLines.push('- **Output**:');
|
|
687
|
+
feedbackLines.push('```');
|
|
688
|
+
feedbackLines.push(failure.output);
|
|
689
|
+
feedbackLines.push('```');
|
|
690
|
+
}
|
|
691
|
+
if (failure.error) {
|
|
692
|
+
feedbackLines.push('- **Error**:');
|
|
693
|
+
feedbackLines.push('```');
|
|
694
|
+
feedbackLines.push(failure.error);
|
|
695
|
+
feedbackLines.push('```');
|
|
696
|
+
}
|
|
697
|
+
feedbackLines.push('');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
feedbackLines.push('### Action Required');
|
|
701
|
+
feedbackLines.push('');
|
|
702
|
+
feedbackLines.push('Fix the failing gates above, then re-run verification.');
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
status: 'retry',
|
|
706
|
+
message: `Round ${currentRound}/${effectiveMaxRounds} failed. Agent should fix and retry.`,
|
|
707
|
+
should_retry: true,
|
|
708
|
+
round: currentRound,
|
|
709
|
+
max_rounds: effectiveMaxRounds,
|
|
710
|
+
next_round: currentRound + 1,
|
|
711
|
+
gate_results: gateResults,
|
|
712
|
+
agent_feedback: feedbackLines.join('\n'),
|
|
713
|
+
failures: failures.map(r => ({
|
|
714
|
+
gate: r.gate,
|
|
715
|
+
message: r.message,
|
|
716
|
+
output: r.output,
|
|
717
|
+
error: r.error,
|
|
718
|
+
})),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
564
722
|
// ============================================================================
|
|
565
723
|
// Exports
|
|
566
724
|
// ============================================================================
|
|
@@ -595,4 +753,9 @@ module.exports = {
|
|
|
595
753
|
|
|
596
754
|
// Reporting
|
|
597
755
|
createValidationReport,
|
|
756
|
+
|
|
757
|
+
// CI Feedback Loop
|
|
758
|
+
CI_FEEDBACK_DEFAULTS,
|
|
759
|
+
loadCIFeedbackConfig,
|
|
760
|
+
executeCIFeedbackLoop,
|
|
598
761
|
};
|
|
@@ -233,6 +233,24 @@ const FEATURE_DETECTORS = {
|
|
|
233
233
|
});
|
|
234
234
|
},
|
|
235
235
|
|
|
236
|
+
'scale-adaptive': signals => {
|
|
237
|
+
const { scale } = signals;
|
|
238
|
+
if (!scale || !scale.tier) return null;
|
|
239
|
+
// Only trigger when scale info provides actionable guidance
|
|
240
|
+
const rec = scale.recommendations;
|
|
241
|
+
if (!rec) return null;
|
|
242
|
+
// Suggest scale-adaptive workflow when project is not medium (the default)
|
|
243
|
+
if (scale.tier === 'medium') return null;
|
|
244
|
+
const label = scale.tier.charAt(0).toUpperCase() + scale.tier.slice(1);
|
|
245
|
+
return recommend('scale-adaptive', {
|
|
246
|
+
priority: scale.tier === 'enterprise' || scale.tier === 'large' ? 'medium' : 'low',
|
|
247
|
+
trigger: `${label} project detected (${scale.metrics.files} files, ${scale.metrics.stories} stories) — ${rec.description}`,
|
|
248
|
+
action: 'suggest',
|
|
249
|
+
command: '/agileflow:workflow',
|
|
250
|
+
phase: 'pre-story',
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
|
|
236
254
|
// =========================================================================
|
|
237
255
|
// PLANNING PHASE
|
|
238
256
|
// =========================================================================
|
|
@@ -517,6 +535,29 @@ const FEATURE_DETECTORS = {
|
|
|
517
535
|
});
|
|
518
536
|
},
|
|
519
537
|
|
|
538
|
+
'ac-verify': signals => {
|
|
539
|
+
const { story, tests } = signals;
|
|
540
|
+
if (!story || story.status !== 'in-progress') return null;
|
|
541
|
+
if (!tests || tests.passing !== true) return null; // Only after tests pass
|
|
542
|
+
if (!storyHasAC(story)) return null;
|
|
543
|
+
// Check if AC already verified (count by index to avoid extra keys)
|
|
544
|
+
const acStatus = story.ac_status || {};
|
|
545
|
+
const acList = story.acceptance_criteria || story.ac || [];
|
|
546
|
+
const verifiedCount = acList.filter(
|
|
547
|
+
(_, i) =>
|
|
548
|
+
acStatus[i] === 'verified' || acStatus[i] === 'auto-verified' || acStatus[i] === true
|
|
549
|
+
).length;
|
|
550
|
+
if (verifiedCount === acList.length) return null;
|
|
551
|
+
const unverifiedCount = acList.length - verifiedCount;
|
|
552
|
+
return recommend('ac-verify', {
|
|
553
|
+
priority: 'high',
|
|
554
|
+
trigger: `Tests pass but ${unverifiedCount}/${acList.length} AC unverified`,
|
|
555
|
+
action: 'suggest',
|
|
556
|
+
command: '/agileflow:audit',
|
|
557
|
+
phase: 'post-impl',
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
|
|
520
561
|
// =========================================================================
|
|
521
562
|
// POST-IMPLEMENTATION PHASE
|
|
522
563
|
// =========================================================================
|
|
@@ -704,6 +745,7 @@ const PHASE_MAP = {
|
|
|
704
745
|
'workflow',
|
|
705
746
|
'template',
|
|
706
747
|
'configure',
|
|
748
|
+
'scale-adaptive',
|
|
707
749
|
],
|
|
708
750
|
planning: [
|
|
709
751
|
'impact',
|
|
@@ -728,6 +770,7 @@ const PHASE_MAP = {
|
|
|
728
770
|
'serve',
|
|
729
771
|
],
|
|
730
772
|
'post-impl': [
|
|
773
|
+
'ac-verify',
|
|
731
774
|
'review',
|
|
732
775
|
'logic-audit',
|
|
733
776
|
'docs',
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-writer.js - Canonical write module for status.json mutations
|
|
3
|
+
*
|
|
4
|
+
* ALL status.json story updates should go through this module to ensure:
|
|
5
|
+
* 1. Atomic read-modify-write via file-lock.js
|
|
6
|
+
* 2. State machine validation on status transitions
|
|
7
|
+
* 3. Automatic dependency resolution when stories complete
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { updateStory, readStory } = require('./status-writer');
|
|
11
|
+
* updateStory(rootDir, 'US-0042', { status: 'completed' });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Lazy-load file-lock for atomic writes
|
|
20
|
+
let _fileLock;
|
|
21
|
+
function getFileLock() {
|
|
22
|
+
if (_fileLock === undefined) {
|
|
23
|
+
try {
|
|
24
|
+
_fileLock = require('./file-lock');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
_fileLock = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return _fileLock;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Lazy-load story-state-machine for transition validation
|
|
33
|
+
let _stateMachine;
|
|
34
|
+
function getStateMachine() {
|
|
35
|
+
if (_stateMachine === undefined) {
|
|
36
|
+
try {
|
|
37
|
+
_stateMachine = require('./story-state-machine');
|
|
38
|
+
} catch (e) {
|
|
39
|
+
_stateMachine = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return _stateMachine;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Lazy-load paths module
|
|
46
|
+
let _paths;
|
|
47
|
+
function getPaths() {
|
|
48
|
+
if (_paths === undefined) {
|
|
49
|
+
try {
|
|
50
|
+
_paths = require('../../lib/paths');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
_paths = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return _paths;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the status.json file path for a given project root.
|
|
60
|
+
* @param {string} rootDir - Project root directory
|
|
61
|
+
* @returns {string} Absolute path to status.json
|
|
62
|
+
*/
|
|
63
|
+
function getStatusFilePath(rootDir) {
|
|
64
|
+
const paths = getPaths();
|
|
65
|
+
if (paths && typeof paths.getStatusPath === 'function') {
|
|
66
|
+
return paths.getStatusPath(rootDir);
|
|
67
|
+
}
|
|
68
|
+
return path.join(rootDir, 'docs', '09-agents', 'status.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read a single story from status.json.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} rootDir - Project root directory
|
|
75
|
+
* @param {string} storyId - Story ID (e.g., 'US-0042')
|
|
76
|
+
* @returns {{ ok: boolean, story?: object, error?: string }}
|
|
77
|
+
*/
|
|
78
|
+
function readStory(rootDir, storyId) {
|
|
79
|
+
try {
|
|
80
|
+
const statusPath = getStatusFilePath(rootDir);
|
|
81
|
+
if (!fs.existsSync(statusPath)) {
|
|
82
|
+
return { ok: false, error: 'status.json not found' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
86
|
+
if (!data.stories || !data.stories[storyId]) {
|
|
87
|
+
return { ok: false, error: `Story ${storyId} not found` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { ok: true, story: data.stories[storyId] };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { ok: false, error: e.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve dependencies when a story transitions to completed/done.
|
|
98
|
+
* Pure in-memory operation — mutates `data` in place.
|
|
99
|
+
*
|
|
100
|
+
* Iterates all stories, finds those with `depends_on` or `blocked_by`
|
|
101
|
+
* containing `completedStoryId`. If all dependencies are now
|
|
102
|
+
* completed/done, transitions the story from `blocked` → `ready`.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} data - Full status.json data object (mutated in place)
|
|
105
|
+
* @param {string} completedStoryId - The story that just completed
|
|
106
|
+
* @returns {{ unblocked: string[] }} List of story IDs that were unblocked
|
|
107
|
+
*/
|
|
108
|
+
function resolveDependencies(data, completedStoryId) {
|
|
109
|
+
const unblocked = [];
|
|
110
|
+
if (!data || !data.stories) return { unblocked };
|
|
111
|
+
|
|
112
|
+
const sm = getStateMachine();
|
|
113
|
+
const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
|
|
114
|
+
|
|
115
|
+
// Helper: check if a story ID is in a completed/done state
|
|
116
|
+
function isCompleted(sid) {
|
|
117
|
+
const s = data.stories[sid];
|
|
118
|
+
if (!s) return false;
|
|
119
|
+
return completedStatuses.includes(s.status) || s.status === 'done';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [storyId, story] of Object.entries(data.stories)) {
|
|
123
|
+
// Only consider blocked stories
|
|
124
|
+
if (story.status !== 'blocked') continue;
|
|
125
|
+
|
|
126
|
+
// Collect dependency IDs from both fields
|
|
127
|
+
const deps = [];
|
|
128
|
+
if (Array.isArray(story.depends_on)) deps.push(...story.depends_on);
|
|
129
|
+
if (Array.isArray(story.blocked_by)) deps.push(...story.blocked_by);
|
|
130
|
+
|
|
131
|
+
// Skip stories that don't depend on the completed story
|
|
132
|
+
if (!deps.includes(completedStoryId)) continue;
|
|
133
|
+
|
|
134
|
+
// Check if ALL dependencies are now completed/done
|
|
135
|
+
const allMet = deps.every(depId => isCompleted(depId));
|
|
136
|
+
if (!allMet) continue;
|
|
137
|
+
|
|
138
|
+
// Transition blocked → ready
|
|
139
|
+
if (sm) {
|
|
140
|
+
const result = sm.transition({ id: storyId, status: 'blocked' }, 'ready', {
|
|
141
|
+
actor: 'status-writer',
|
|
142
|
+
reason: `Dependencies resolved (${completedStoryId} completed)`,
|
|
143
|
+
});
|
|
144
|
+
if (result.success) {
|
|
145
|
+
story.status = 'ready';
|
|
146
|
+
story.updated_at = new Date().toISOString();
|
|
147
|
+
unblocked.push(storyId);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// No state machine available — direct transition
|
|
151
|
+
story.status = 'ready';
|
|
152
|
+
story.updated_at = new Date().toISOString();
|
|
153
|
+
unblocked.push(storyId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { unblocked };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Update a single story in status.json using atomic read-modify-write.
|
|
162
|
+
*
|
|
163
|
+
* When `updates.status` is provided and differs from the current status,
|
|
164
|
+
* validates the transition via story-state-machine. When transitioning
|
|
165
|
+
* to completed/done, triggers resolveDependencies() automatically.
|
|
166
|
+
*
|
|
167
|
+
* @param {string} rootDir - Project root directory
|
|
168
|
+
* @param {string} storyId - Story ID (e.g., 'US-0042')
|
|
169
|
+
* @param {object} updates - Fields to update (e.g., { status: 'completed', assigned_to: 'AG-API' })
|
|
170
|
+
* @param {object} [options={}] - Options
|
|
171
|
+
* @param {boolean} [options.skipValidation=false] - Skip state machine validation
|
|
172
|
+
* @returns {{ ok: boolean, unblocked?: string[], error?: string }}
|
|
173
|
+
*/
|
|
174
|
+
function updateStory(rootDir, storyId, updates, options = {}) {
|
|
175
|
+
const { skipValidation = false } = options;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const statusPath = getStatusFilePath(rootDir);
|
|
179
|
+
if (!fs.existsSync(statusPath)) {
|
|
180
|
+
return { ok: false, error: 'status.json not found' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const fileLock = getFileLock();
|
|
184
|
+
|
|
185
|
+
// Mutation function applied inside the lock
|
|
186
|
+
let resultMeta = { unblocked: [] };
|
|
187
|
+
|
|
188
|
+
const modifyFn = data => {
|
|
189
|
+
if (!data.stories) data.stories = {};
|
|
190
|
+
if (!data.stories[storyId]) {
|
|
191
|
+
throw new Error(`Story ${storyId} not found`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const story = data.stories[storyId];
|
|
195
|
+
|
|
196
|
+
// Validate status transition if status is changing
|
|
197
|
+
if (updates.status && updates.status !== story.status && !skipValidation) {
|
|
198
|
+
const sm = getStateMachine();
|
|
199
|
+
if (sm) {
|
|
200
|
+
const valid = sm.isValidTransition(story.status, updates.status);
|
|
201
|
+
if (!valid) {
|
|
202
|
+
const validTargets = sm.getValidTransitions(story.status);
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Invalid transition: ${story.status} → ${updates.status}. ` +
|
|
205
|
+
`Valid transitions: ${validTargets.join(', ') || 'none'}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Apply updates (null values delete the field)
|
|
212
|
+
Object.assign(story, updates);
|
|
213
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
214
|
+
if (val === null) delete story[key];
|
|
215
|
+
}
|
|
216
|
+
story.updated_at = new Date().toISOString();
|
|
217
|
+
|
|
218
|
+
// Trigger dependency resolution on completion
|
|
219
|
+
const sm = getStateMachine();
|
|
220
|
+
const completedStatuses = sm ? sm.COMPLETED_STATUSES : ['completed', 'archived'];
|
|
221
|
+
if (
|
|
222
|
+
updates.status &&
|
|
223
|
+
(completedStatuses.includes(updates.status) || updates.status === 'done')
|
|
224
|
+
) {
|
|
225
|
+
const resolved = resolveDependencies(data, storyId);
|
|
226
|
+
resultMeta.unblocked = resolved.unblocked;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return data;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (fileLock && typeof fileLock.atomicReadModifyWrite === 'function') {
|
|
233
|
+
const result = fileLock.atomicReadModifyWrite(statusPath, modifyFn);
|
|
234
|
+
if (!result.success) {
|
|
235
|
+
return { ok: false, error: result.error || 'Atomic write failed' };
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// Fallback: direct read-modify-write (no lock)
|
|
239
|
+
const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
|
|
240
|
+
const modified = modifyFn(data);
|
|
241
|
+
fs.writeFileSync(statusPath, JSON.stringify(modified, null, 2) + '\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { ok: true, unblocked: resultMeta.unblocked };
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return { ok: false, error: e.message };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
updateStory,
|
|
252
|
+
readStory,
|
|
253
|
+
resolveDependencies,
|
|
254
|
+
getStatusFilePath,
|
|
255
|
+
};
|