agileflow 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/feature-flags.js +32 -4
- package/lib/skill-loader.js +0 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +81 -0
- package/scripts/babysit-clear-restore.js +154 -0
- package/scripts/claude-tmux.sh +120 -24
- package/scripts/claude-watchdog.sh +225 -0
- package/scripts/generators/agent-registry.js +14 -1
- package/scripts/generators/inject-babysit.js +22 -9
- package/scripts/generators/inject-help.js +19 -9
- package/scripts/lib/README-portable-tasks.md +424 -0
- package/scripts/lib/audit-cleanup.js +250 -0
- package/scripts/lib/audit-registry.js +248 -0
- package/scripts/lib/configure-detect.js +20 -0
- package/scripts/lib/feature-catalog.js +13 -2
- package/scripts/lib/gate-enforcer.js +295 -0
- package/scripts/lib/model-profiles.js +98 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/skill-catalog.js +557 -0
- package/scripts/lib/skill-recommender.js +311 -0
- package/scripts/lib/tdd-phase-manager.js +455 -0
- package/scripts/lib/team-events.js +76 -8
- package/scripts/lib/tmux-group-colors.js +113 -0
- package/scripts/messaging-bridge.js +209 -1
- package/scripts/spawn-audit-sessions.js +549 -0
- package/scripts/team-manager.js +37 -16
- package/scripts/tmux-close-windows.sh +180 -0
- package/scripts/tmux-restore-window.sh +67 -0
- package/scripts/tmux-save-closed-window.sh +35 -0
- package/src/core/agents/ads-audit-budget.md +181 -0
- package/src/core/agents/ads-audit-compliance.md +169 -0
- package/src/core/agents/ads-audit-creative.md +164 -0
- package/src/core/agents/ads-audit-google.md +226 -0
- package/src/core/agents/ads-audit-meta.md +183 -0
- package/src/core/agents/ads-audit-tracking.md +197 -0
- package/src/core/agents/ads-consensus.md +322 -0
- package/src/core/agents/brainstorm-analyzer-features.md +169 -0
- package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
- package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
- package/src/core/agents/brainstorm-analyzer-market.md +147 -0
- package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
- package/src/core/agents/brainstorm-consensus.md +237 -0
- package/src/core/agents/completeness-analyzer-api.md +190 -0
- package/src/core/agents/completeness-analyzer-conditional.md +201 -0
- package/src/core/agents/completeness-analyzer-handlers.md +159 -0
- package/src/core/agents/completeness-analyzer-imports.md +159 -0
- package/src/core/agents/completeness-analyzer-routes.md +182 -0
- package/src/core/agents/completeness-analyzer-state.md +188 -0
- package/src/core/agents/completeness-analyzer-stubs.md +198 -0
- package/src/core/agents/completeness-consensus.md +286 -0
- package/src/core/agents/perf-consensus.md +2 -2
- package/src/core/agents/security-consensus.md +2 -2
- package/src/core/agents/seo-analyzer-content.md +167 -0
- package/src/core/agents/seo-analyzer-images.md +187 -0
- package/src/core/agents/seo-analyzer-performance.md +206 -0
- package/src/core/agents/seo-analyzer-schema.md +176 -0
- package/src/core/agents/seo-analyzer-sitemap.md +172 -0
- package/src/core/agents/seo-analyzer-technical.md +144 -0
- package/src/core/agents/seo-consensus.md +289 -0
- package/src/core/agents/test-consensus.md +2 -2
- package/src/core/commands/ads/audit.md +375 -0
- package/src/core/commands/ads/budget.md +97 -0
- package/src/core/commands/ads/competitor.md +112 -0
- package/src/core/commands/ads/creative.md +85 -0
- package/src/core/commands/ads/google.md +112 -0
- package/src/core/commands/ads/landing.md +119 -0
- package/src/core/commands/ads/linkedin.md +112 -0
- package/src/core/commands/ads/meta.md +91 -0
- package/src/core/commands/ads/microsoft.md +115 -0
- package/src/core/commands/ads/plan.md +321 -0
- package/src/core/commands/ads/tiktok.md +129 -0
- package/src/core/commands/ads/youtube.md +124 -0
- package/src/core/commands/ads.md +128 -0
- package/src/core/commands/babysit.md +250 -1344
- package/src/core/commands/code/completeness.md +466 -0
- package/src/core/commands/{audit → code}/legal.md +26 -16
- package/src/core/commands/{audit → code}/logic.md +27 -16
- package/src/core/commands/{audit → code}/performance.md +30 -20
- package/src/core/commands/{audit → code}/security.md +32 -19
- package/src/core/commands/{audit → code}/test.md +30 -20
- package/src/core/commands/{discovery → ideate}/brief.md +12 -12
- package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
- package/src/core/commands/ideate/features.md +435 -0
- package/src/core/commands/seo/audit.md +373 -0
- package/src/core/commands/seo/competitor.md +174 -0
- package/src/core/commands/seo/content.md +107 -0
- package/src/core/commands/seo/geo.md +229 -0
- package/src/core/commands/seo/hreflang.md +140 -0
- package/src/core/commands/seo/images.md +96 -0
- package/src/core/commands/seo/page.md +198 -0
- package/src/core/commands/seo/plan.md +163 -0
- package/src/core/commands/seo/programmatic.md +131 -0
- package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
- package/src/core/commands/seo/references/eeat-framework.md +110 -0
- package/src/core/commands/seo/references/quality-gates.md +91 -0
- package/src/core/commands/seo/references/schema-types.md +102 -0
- package/src/core/commands/seo/schema.md +183 -0
- package/src/core/commands/seo/sitemap.md +97 -0
- package/src/core/commands/seo/technical.md +100 -0
- package/src/core/commands/seo.md +107 -0
- package/src/core/commands/skill/list.md +68 -212
- package/src/core/commands/skill/recommend.md +216 -0
- package/src/core/commands/tdd-next.md +238 -0
- package/src/core/commands/tdd.md +210 -0
- package/src/core/experts/_core-expertise.yaml +105 -0
- package/src/core/experts/analytics/expertise.yaml +5 -99
- package/src/core/experts/codebase-query/expertise.yaml +3 -72
- package/src/core/experts/compliance/expertise.yaml +6 -72
- package/src/core/experts/database/expertise.yaml +9 -52
- package/src/core/experts/documentation/expertise.yaml +7 -140
- package/src/core/experts/integrations/expertise.yaml +7 -127
- package/src/core/experts/mentor/expertise.yaml +8 -35
- package/src/core/experts/monitoring/expertise.yaml +7 -49
- package/src/core/experts/performance/expertise.yaml +1 -26
- package/src/core/experts/security/expertise.yaml +9 -34
- package/src/core/experts/ui/expertise.yaml +6 -36
- package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
- package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
- package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
- package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
- package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
- package/src/core/templates/agileflow-metadata.json +15 -1
- package/tools/cli/installers/ide/_base-ide.js +42 -5
- package/tools/cli/installers/ide/claude-code.js +13 -4
- package/tools/cli/lib/content-injector.js +160 -12
- package/tools/cli/lib/docs-setup.js +1 -1
- package/src/core/commands/skill/create.md +0 -698
- package/src/core/commands/skill/delete.md +0 -316
- package/src/core/commands/skill/edit.md +0 -359
- package/src/core/commands/skill/test.md +0 -394
- package/src/core/commands/skill/upgrade.md +0 -552
- package/src/core/templates/skill-template.md +0 -117
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tdd-phase-manager.js - TDD Phase Tracking for AgileFlow
|
|
3
|
+
*
|
|
4
|
+
* Manages RED→GREEN→REFACTOR phase transitions for TDD workflow.
|
|
5
|
+
* Phase state is stored in status.json story entries under `tdd_phase`.
|
|
6
|
+
*
|
|
7
|
+
* Phases:
|
|
8
|
+
* - red: Write failing tests first (no implementation code allowed)
|
|
9
|
+
* - green: Write minimal code to make tests pass
|
|
10
|
+
* - refactor: Clean up code while keeping tests green
|
|
11
|
+
* - complete: TDD cycle done, ready for commit
|
|
12
|
+
*
|
|
13
|
+
* Transitions:
|
|
14
|
+
* - red → green: Requires test_status = "failing" (tests exist and fail)
|
|
15
|
+
* - green → refactor: Requires test_status = "passing" (tests pass)
|
|
16
|
+
* - refactor → red: Start new cycle (tests must still pass)
|
|
17
|
+
* - refactor → complete: TDD done (tests must pass)
|
|
18
|
+
* - any → cancelled: Exit TDD workflow
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* const { startTDD, advancePhase, getPhaseInstructions } = require('./tdd-phase-manager');
|
|
22
|
+
* const result = startTDD(statusData, 'US-0042');
|
|
23
|
+
* const advance = advancePhase(statusData, 'US-0042', testStatus);
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Constants
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const PHASES = {
|
|
36
|
+
RED: 'red',
|
|
37
|
+
GREEN: 'green',
|
|
38
|
+
REFACTOR: 'refactor',
|
|
39
|
+
COMPLETE: 'complete',
|
|
40
|
+
CANCELLED: 'cancelled',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const VALID_TRANSITIONS = {
|
|
44
|
+
[PHASES.RED]: [PHASES.GREEN, PHASES.CANCELLED],
|
|
45
|
+
[PHASES.GREEN]: [PHASES.REFACTOR, PHASES.CANCELLED],
|
|
46
|
+
[PHASES.REFACTOR]: [PHASES.RED, PHASES.COMPLETE, PHASES.CANCELLED],
|
|
47
|
+
[PHASES.COMPLETE]: [], // Terminal
|
|
48
|
+
[PHASES.CANCELLED]: [], // Terminal
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Conditions required for each transition
|
|
53
|
+
*/
|
|
54
|
+
const TRANSITION_CONDITIONS = {
|
|
55
|
+
[`${PHASES.RED}->${PHASES.GREEN}`]: {
|
|
56
|
+
requires: 'test_status_failing',
|
|
57
|
+
message: 'Tests must exist and be FAILING before moving to GREEN phase',
|
|
58
|
+
hint: 'Write your failing tests first, then run /agileflow:verify to confirm they fail',
|
|
59
|
+
},
|
|
60
|
+
[`${PHASES.GREEN}->${PHASES.REFACTOR}`]: {
|
|
61
|
+
requires: 'test_status_passing',
|
|
62
|
+
message: 'Tests must be PASSING before moving to REFACTOR phase',
|
|
63
|
+
hint: 'Write minimal code to make tests pass, then run /agileflow:verify',
|
|
64
|
+
},
|
|
65
|
+
[`${PHASES.REFACTOR}->${PHASES.RED}`]: {
|
|
66
|
+
requires: 'test_status_passing',
|
|
67
|
+
message: 'Tests must still be PASSING before starting a new RED cycle',
|
|
68
|
+
hint: 'Ensure refactoring did not break tests',
|
|
69
|
+
},
|
|
70
|
+
[`${PHASES.REFACTOR}->${PHASES.COMPLETE}`]: {
|
|
71
|
+
requires: 'test_status_passing',
|
|
72
|
+
message: 'Tests must be PASSING to complete TDD workflow',
|
|
73
|
+
hint: 'Run /agileflow:verify to confirm all tests pass',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Phase-specific instructions for the AI agent
|
|
79
|
+
*/
|
|
80
|
+
const PHASE_INSTRUCTIONS = {
|
|
81
|
+
[PHASES.RED]: {
|
|
82
|
+
emoji: '🔴',
|
|
83
|
+
title: 'RED Phase - Write Failing Tests',
|
|
84
|
+
rules: [
|
|
85
|
+
'Write test files ONLY - do NOT write implementation code yet',
|
|
86
|
+
'Tests should cover the acceptance criteria for this story',
|
|
87
|
+
'Tests MUST fail when run (they test code that does not exist yet)',
|
|
88
|
+
'Focus on the public API/interface - what should the code DO?',
|
|
89
|
+
'Use `.skip()` for tests you plan to implement later in this cycle',
|
|
90
|
+
],
|
|
91
|
+
allowed_file_patterns: [
|
|
92
|
+
'**/*.test.*',
|
|
93
|
+
'**/*.spec.*',
|
|
94
|
+
'**/test_*',
|
|
95
|
+
'**/*_test.*',
|
|
96
|
+
'**/tests/**',
|
|
97
|
+
'**/__tests__/**',
|
|
98
|
+
'**/test/**',
|
|
99
|
+
'**/spec/**',
|
|
100
|
+
'**/fixtures/**',
|
|
101
|
+
'**/mocks/**',
|
|
102
|
+
'**/helpers/**',
|
|
103
|
+
],
|
|
104
|
+
next_action: 'Run /agileflow:verify to confirm tests FAIL, then /agileflow:tdd-next to advance',
|
|
105
|
+
},
|
|
106
|
+
[PHASES.GREEN]: {
|
|
107
|
+
emoji: '🟢',
|
|
108
|
+
title: 'GREEN Phase - Make Tests Pass',
|
|
109
|
+
rules: [
|
|
110
|
+
'Write MINIMAL implementation code to make failing tests pass',
|
|
111
|
+
'Do NOT refactor yet - focus only on making tests green',
|
|
112
|
+
'Do NOT add features beyond what tests require',
|
|
113
|
+
'Do NOT modify test files (except removing .skip())',
|
|
114
|
+
'Simple, direct solutions - even if ugly',
|
|
115
|
+
],
|
|
116
|
+
next_action: 'Run /agileflow:verify to confirm tests PASS, then /agileflow:tdd-next to advance',
|
|
117
|
+
},
|
|
118
|
+
[PHASES.REFACTOR]: {
|
|
119
|
+
emoji: '🔵',
|
|
120
|
+
title: 'REFACTOR Phase - Clean Up',
|
|
121
|
+
rules: [
|
|
122
|
+
'Improve code quality while keeping ALL tests green',
|
|
123
|
+
'Extract functions, rename variables, reduce duplication',
|
|
124
|
+
'Run tests frequently - any failure means you broke something',
|
|
125
|
+
'Do NOT add new features or change behavior',
|
|
126
|
+
'When satisfied, use /agileflow:tdd-next to either start new RED cycle or complete',
|
|
127
|
+
],
|
|
128
|
+
next_action:
|
|
129
|
+
'Run /agileflow:verify, then /agileflow:tdd-next (choose "complete" or "new cycle")',
|
|
130
|
+
},
|
|
131
|
+
[PHASES.COMPLETE]: {
|
|
132
|
+
emoji: '✅',
|
|
133
|
+
title: 'TDD Complete',
|
|
134
|
+
rules: ['All tests pass', 'Code is clean', 'Ready for code review and commit'],
|
|
135
|
+
next_action: 'Run code review, then commit',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Phase Management
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Start TDD workflow for a story
|
|
145
|
+
* @param {Object} statusData - Full status.json data
|
|
146
|
+
* @param {string} storyId - Story ID (e.g., 'US-0042')
|
|
147
|
+
* @returns {{ success: boolean, phase: string, message: string, instructions: Object }}
|
|
148
|
+
*/
|
|
149
|
+
function startTDD(statusData, storyId) {
|
|
150
|
+
if (!statusData || typeof statusData !== 'object') {
|
|
151
|
+
return { success: false, phase: null, message: 'Invalid status data', instructions: null };
|
|
152
|
+
}
|
|
153
|
+
if (!storyId || typeof storyId !== 'string') {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
phase: null,
|
|
157
|
+
message: `Invalid story ID: ${storyId}`,
|
|
158
|
+
instructions: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const story = statusData.stories && statusData.stories[storyId];
|
|
162
|
+
if (!story) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
phase: null,
|
|
166
|
+
message: `Story ${storyId} not found in status.json`,
|
|
167
|
+
instructions: null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if already in TDD
|
|
172
|
+
if (
|
|
173
|
+
story.tdd_phase &&
|
|
174
|
+
story.tdd_phase !== PHASES.COMPLETE &&
|
|
175
|
+
story.tdd_phase !== PHASES.CANCELLED
|
|
176
|
+
) {
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
phase: story.tdd_phase,
|
|
180
|
+
message: `Story ${storyId} already in TDD ${story.tdd_phase.toUpperCase()} phase - resuming`,
|
|
181
|
+
instructions: PHASE_INSTRUCTIONS[story.tdd_phase],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Set RED phase
|
|
186
|
+
story.tdd_phase = PHASES.RED;
|
|
187
|
+
story.tdd_started_at = new Date().toISOString();
|
|
188
|
+
story.tdd_cycles = (story.tdd_cycles || 0) + 1;
|
|
189
|
+
statusData.updated_at = new Date().toISOString();
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
phase: PHASES.RED,
|
|
194
|
+
message: `TDD started for ${storyId} - entering RED phase (cycle ${story.tdd_cycles})`,
|
|
195
|
+
instructions: PHASE_INSTRUCTIONS[PHASES.RED],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Advance to the next TDD phase
|
|
201
|
+
* @param {Object} statusData - Full status.json data
|
|
202
|
+
* @param {string} storyId - Story ID
|
|
203
|
+
* @param {string} targetPhase - Desired next phase
|
|
204
|
+
* @param {Object} context - Current context
|
|
205
|
+
* @param {string} context.test_status - 'passing' | 'failing' | null
|
|
206
|
+
* @returns {{ success: boolean, phase: string, message: string, instructions: Object }}
|
|
207
|
+
*/
|
|
208
|
+
function advancePhase(statusData, storyId, targetPhase, context = {}) {
|
|
209
|
+
if (!statusData || typeof statusData !== 'object') {
|
|
210
|
+
return { success: false, phase: null, message: 'Invalid status data', instructions: null };
|
|
211
|
+
}
|
|
212
|
+
if (!storyId || typeof storyId !== 'string') {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
phase: null,
|
|
216
|
+
message: `Invalid story ID: ${storyId}`,
|
|
217
|
+
instructions: null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (!targetPhase || typeof targetPhase !== 'string') {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
phase: null,
|
|
224
|
+
message: `Invalid target phase: ${targetPhase}`,
|
|
225
|
+
instructions: null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Normalize context if null passed explicitly
|
|
229
|
+
if (!context || typeof context !== 'object') {
|
|
230
|
+
context = {};
|
|
231
|
+
}
|
|
232
|
+
const story = statusData.stories && statusData.stories[storyId];
|
|
233
|
+
if (!story) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
phase: null,
|
|
237
|
+
message: `Story ${storyId} not found`,
|
|
238
|
+
instructions: null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const currentPhase = story.tdd_phase;
|
|
243
|
+
if (!currentPhase) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
phase: null,
|
|
247
|
+
message: `Story ${storyId} is not in TDD mode. Start with /agileflow:tdd ${storyId}`,
|
|
248
|
+
instructions: null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Validate currentPhase is a known phase (catch corrupted data)
|
|
253
|
+
if (!Object.values(PHASES).includes(currentPhase)) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
phase: currentPhase,
|
|
257
|
+
message: `Story ${storyId} has invalid TDD phase: "${currentPhase}". Valid: ${Object.values(PHASES).join(', ')}`,
|
|
258
|
+
instructions: null,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if transition is valid
|
|
263
|
+
const validTargets = VALID_TRANSITIONS[currentPhase] || [];
|
|
264
|
+
if (!validTargets.includes(targetPhase)) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
phase: currentPhase,
|
|
268
|
+
message: `Cannot transition from ${currentPhase.toUpperCase()} to ${targetPhase.toUpperCase()}. Valid: ${validTargets.join(', ') || 'none'}`,
|
|
269
|
+
instructions: PHASE_INSTRUCTIONS[currentPhase],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Cancel is always allowed
|
|
274
|
+
if (targetPhase === PHASES.CANCELLED) {
|
|
275
|
+
story.tdd_phase = PHASES.CANCELLED;
|
|
276
|
+
story.tdd_cancelled_at = new Date().toISOString();
|
|
277
|
+
statusData.updated_at = new Date().toISOString();
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
phase: PHASES.CANCELLED,
|
|
281
|
+
message: `TDD cancelled for ${storyId}`,
|
|
282
|
+
instructions: null,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check transition conditions
|
|
287
|
+
const conditionKey = `${currentPhase}->${targetPhase}`;
|
|
288
|
+
const condition = TRANSITION_CONDITIONS[conditionKey];
|
|
289
|
+
|
|
290
|
+
if (condition) {
|
|
291
|
+
const { test_status } = context;
|
|
292
|
+
|
|
293
|
+
if (condition.requires === 'test_status_failing' && test_status !== 'failing') {
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
phase: currentPhase,
|
|
297
|
+
message: `🚫 ${condition.message}`,
|
|
298
|
+
hint: condition.hint,
|
|
299
|
+
instructions: PHASE_INSTRUCTIONS[currentPhase],
|
|
300
|
+
gate_blocked: true,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (condition.requires === 'test_status_passing' && test_status !== 'passing') {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
phase: currentPhase,
|
|
308
|
+
message: `🚫 ${condition.message}`,
|
|
309
|
+
hint: condition.hint,
|
|
310
|
+
instructions: PHASE_INSTRUCTIONS[currentPhase],
|
|
311
|
+
gate_blocked: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Transition
|
|
317
|
+
const previousPhase = currentPhase;
|
|
318
|
+
story.tdd_phase = targetPhase;
|
|
319
|
+
story.tdd_last_transition = new Date().toISOString();
|
|
320
|
+
|
|
321
|
+
// Track cycles
|
|
322
|
+
if (targetPhase === PHASES.RED && previousPhase === PHASES.REFACTOR) {
|
|
323
|
+
story.tdd_cycles = (story.tdd_cycles || 0) + 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (targetPhase === PHASES.COMPLETE) {
|
|
327
|
+
story.tdd_completed_at = new Date().toISOString();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
statusData.updated_at = new Date().toISOString();
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
phase: targetPhase,
|
|
335
|
+
message: `${previousPhase.toUpperCase()} → ${targetPhase.toUpperCase()} for ${storyId}`,
|
|
336
|
+
instructions: PHASE_INSTRUCTIONS[targetPhase] || null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get current phase info for a story
|
|
342
|
+
* @param {Object} statusData - Full status.json data
|
|
343
|
+
* @param {string} storyId - Story ID
|
|
344
|
+
* @returns {{ phase: string|null, instructions: Object|null, active: boolean }}
|
|
345
|
+
*/
|
|
346
|
+
function getPhaseInfo(statusData, storyId) {
|
|
347
|
+
if (!statusData || typeof statusData !== 'object') {
|
|
348
|
+
return { phase: null, instructions: null, active: false };
|
|
349
|
+
}
|
|
350
|
+
const story = statusData.stories && statusData.stories[storyId];
|
|
351
|
+
if (!story || !story.tdd_phase) {
|
|
352
|
+
return { phase: null, instructions: null, active: false };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const active = story.tdd_phase !== PHASES.COMPLETE && story.tdd_phase !== PHASES.CANCELLED;
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
phase: story.tdd_phase,
|
|
359
|
+
instructions: PHASE_INSTRUCTIONS[story.tdd_phase] || null,
|
|
360
|
+
active,
|
|
361
|
+
cycles: story.tdd_cycles || 0,
|
|
362
|
+
started_at: story.tdd_started_at || null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get the next valid phases from current phase
|
|
368
|
+
* @param {string} currentPhase - Current TDD phase
|
|
369
|
+
* @returns {string[]} Valid next phases
|
|
370
|
+
*/
|
|
371
|
+
function getNextPhases(currentPhase) {
|
|
372
|
+
return VALID_TRANSITIONS[currentPhase] || [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Format phase status for display
|
|
377
|
+
* @param {Object} statusData - Full status.json data
|
|
378
|
+
* @param {string} storyId - Story ID
|
|
379
|
+
* @returns {string} Formatted status string
|
|
380
|
+
*/
|
|
381
|
+
function formatPhaseStatus(statusData, storyId) {
|
|
382
|
+
const info = getPhaseInfo(statusData, storyId);
|
|
383
|
+
|
|
384
|
+
if (!info.phase) {
|
|
385
|
+
return `${storyId}: No TDD workflow active`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const inst = info.instructions;
|
|
389
|
+
const lines = [
|
|
390
|
+
`${inst ? inst.emoji : '?'} ${storyId}: TDD ${info.phase.toUpperCase()} phase (cycle ${info.cycles})`,
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
if (inst) {
|
|
394
|
+
lines.push(` ${inst.title}`);
|
|
395
|
+
if (inst.next_action) {
|
|
396
|
+
lines.push(` Next: ${inst.next_action}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return lines.join('\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// Status.json Helpers
|
|
405
|
+
// ============================================================================
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Load status.json from the standard path
|
|
409
|
+
* @param {string} projectRoot - Project root directory
|
|
410
|
+
* @returns {Object|null} Parsed status data or null
|
|
411
|
+
*/
|
|
412
|
+
function loadStatusData(projectRoot) {
|
|
413
|
+
const statusPath = path.join(projectRoot, 'docs', '09-agents', 'status.json');
|
|
414
|
+
try {
|
|
415
|
+
const content = fs.readFileSync(statusPath, 'utf8');
|
|
416
|
+
return JSON.parse(content);
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Save status.json to the standard path
|
|
424
|
+
* @param {string} projectRoot - Project root directory
|
|
425
|
+
* @param {Object} statusData - Status data to save
|
|
426
|
+
*/
|
|
427
|
+
function saveStatusData(projectRoot, statusData) {
|
|
428
|
+
const statusPath = path.join(projectRoot, 'docs', '09-agents', 'status.json');
|
|
429
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2) + '\n', 'utf8');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ============================================================================
|
|
433
|
+
// Exports
|
|
434
|
+
// ============================================================================
|
|
435
|
+
|
|
436
|
+
module.exports = {
|
|
437
|
+
// Constants
|
|
438
|
+
PHASES,
|
|
439
|
+
VALID_TRANSITIONS,
|
|
440
|
+
TRANSITION_CONDITIONS,
|
|
441
|
+
PHASE_INSTRUCTIONS,
|
|
442
|
+
|
|
443
|
+
// Phase management
|
|
444
|
+
startTDD,
|
|
445
|
+
advancePhase,
|
|
446
|
+
getPhaseInfo,
|
|
447
|
+
getNextPhases,
|
|
448
|
+
|
|
449
|
+
// Display
|
|
450
|
+
formatPhaseStatus,
|
|
451
|
+
|
|
452
|
+
// Status.json helpers
|
|
453
|
+
loadStatusData,
|
|
454
|
+
saveStatusData,
|
|
455
|
+
};
|
|
@@ -73,6 +73,8 @@ function getPaths() {
|
|
|
73
73
|
const EVENT_TYPES = [
|
|
74
74
|
'team_created',
|
|
75
75
|
'team_stopped',
|
|
76
|
+
'team_completed',
|
|
77
|
+
'team_message',
|
|
76
78
|
'task_assigned',
|
|
77
79
|
'task_completed',
|
|
78
80
|
'agent_error',
|
|
@@ -98,6 +100,27 @@ const MODEL_PRICING = {
|
|
|
98
100
|
|
|
99
101
|
const DEFAULT_COST_THRESHOLD_USD = 5.0;
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Get list of files modified since a git reference.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} rootDir - Project root directory (git working tree)
|
|
107
|
+
* @param {string} [sinceRef='HEAD'] - Git ref to diff against
|
|
108
|
+
* @returns {string[]} Sorted, deduplicated list of modified file paths
|
|
109
|
+
*/
|
|
110
|
+
function getModifiedFiles(rootDir, sinceRef) {
|
|
111
|
+
try {
|
|
112
|
+
const { execFileSync } = require('child_process');
|
|
113
|
+
const output = execFileSync('git', ['diff', '--name-only', sinceRef || 'HEAD'], {
|
|
114
|
+
cwd: rootDir,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
}).trim();
|
|
118
|
+
return output ? [...new Set(output.split('\n'))].sort() : [];
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return []; // fail-open
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
/**
|
|
102
125
|
* Compute estimated cost for an agent's token usage.
|
|
103
126
|
*
|
|
@@ -123,13 +146,14 @@ function computeAgentCost(inputTokens, outputTokens, model) {
|
|
|
123
146
|
* @returns {boolean} True if threshold was exceeded
|
|
124
147
|
*/
|
|
125
148
|
function checkCostThreshold(rootDir, traceId, totalCostUsd, threshold) {
|
|
126
|
-
const limit = threshold
|
|
127
|
-
|
|
149
|
+
const limit = typeof threshold === 'number' ? threshold : DEFAULT_COST_THRESHOLD_USD;
|
|
150
|
+
const cost = typeof totalCostUsd === 'number' && isFinite(totalCostUsd) ? totalCostUsd : 0;
|
|
151
|
+
if (cost > limit) {
|
|
128
152
|
trackEvent(rootDir, 'cost_warning', {
|
|
129
153
|
trace_id: traceId,
|
|
130
|
-
total_cost_usd:
|
|
154
|
+
total_cost_usd: cost,
|
|
131
155
|
threshold_usd: limit,
|
|
132
|
-
message: `Team cost $${
|
|
156
|
+
message: `Team cost $${cost.toFixed(4)} exceeds threshold $${limit.toFixed(2)}`,
|
|
133
157
|
});
|
|
134
158
|
return true;
|
|
135
159
|
}
|
|
@@ -149,9 +173,19 @@ function checkCostThreshold(rootDir, traceId, totalCostUsd, threshold) {
|
|
|
149
173
|
* @returns {{ ok: boolean, error?: string }}
|
|
150
174
|
*/
|
|
151
175
|
function trackEvent(rootDir, eventType, data = {}) {
|
|
176
|
+
// Detect native Agent Teams mode for metrics equivalence (AC4)
|
|
177
|
+
let isNative = false;
|
|
178
|
+
try {
|
|
179
|
+
const ff = require('../../lib/feature-flags');
|
|
180
|
+
isNative = ff.isAgentTeamsEnabled({ rootDir });
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Non-critical - default to false
|
|
183
|
+
}
|
|
184
|
+
|
|
152
185
|
const event = {
|
|
153
186
|
type: eventType,
|
|
154
187
|
at: new Date().toISOString(),
|
|
188
|
+
agent_teams: isNative,
|
|
155
189
|
...data,
|
|
156
190
|
};
|
|
157
191
|
|
|
@@ -294,6 +328,7 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
294
328
|
input_tokens: 0,
|
|
295
329
|
output_tokens: 0,
|
|
296
330
|
cost_usd: 0,
|
|
331
|
+
files_modified: [],
|
|
297
332
|
};
|
|
298
333
|
}
|
|
299
334
|
};
|
|
@@ -309,6 +344,9 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
309
344
|
perAgent[e.agent].input_tokens += e.input_tokens || 0;
|
|
310
345
|
perAgent[e.agent].output_tokens += e.output_tokens || 0;
|
|
311
346
|
if (e.model) agentModels[e.agent] = e.model;
|
|
347
|
+
if (Array.isArray(e.files_modified)) {
|
|
348
|
+
perAgent[e.agent].files_modified.push(...e.files_modified);
|
|
349
|
+
}
|
|
312
350
|
}
|
|
313
351
|
if (e.type === 'agent_error' && e.agent) {
|
|
314
352
|
ensureAgent(e.agent);
|
|
@@ -320,7 +358,8 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
320
358
|
}
|
|
321
359
|
}
|
|
322
360
|
|
|
323
|
-
// Compute per-agent costs
|
|
361
|
+
// Compute per-agent costs (uses last-seen model for all tokens; acceptable
|
|
362
|
+
// approximation since agents typically use a single model throughout)
|
|
324
363
|
for (const [agent, metrics] of Object.entries(perAgent)) {
|
|
325
364
|
metrics.cost_usd = computeAgentCost(
|
|
326
365
|
metrics.input_tokens,
|
|
@@ -330,6 +369,14 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
330
369
|
}
|
|
331
370
|
const totalCostUsd = Object.values(perAgent).reduce((sum, a) => sum + a.cost_usd, 0);
|
|
332
371
|
|
|
372
|
+
// Deduplicate and sort per-agent files_modified, compute union
|
|
373
|
+
const allFilesSet = new Set();
|
|
374
|
+
for (const metrics of Object.values(perAgent)) {
|
|
375
|
+
metrics.files_modified = [...new Set(metrics.files_modified)].sort();
|
|
376
|
+
metrics.files_modified.forEach(f => allFilesSet.add(f));
|
|
377
|
+
}
|
|
378
|
+
const allFilesModified = [...allFilesSet].sort();
|
|
379
|
+
|
|
333
380
|
// Per-gate metrics from gate_passed, gate_failed
|
|
334
381
|
const perGate = {};
|
|
335
382
|
for (const e of events) {
|
|
@@ -345,12 +392,30 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
345
392
|
perGate[gate].pass_rate = total > 0 ? perGate[gate].passed / total : 0;
|
|
346
393
|
}
|
|
347
394
|
|
|
348
|
-
//
|
|
395
|
+
// Count team_message events per agent for message-level observability
|
|
396
|
+
const messagesSent = {};
|
|
397
|
+
let totalMessagesSent = 0;
|
|
398
|
+
for (const e of events) {
|
|
399
|
+
if (e.type === 'team_message' && e.from) {
|
|
400
|
+
if (!messagesSent[e.from]) messagesSent[e.from] = 0;
|
|
401
|
+
messagesSent[e.from]++;
|
|
402
|
+
totalMessagesSent++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Merge messages_sent into per-agent metrics
|
|
406
|
+
for (const [agent, count] of Object.entries(messagesSent)) {
|
|
407
|
+
ensureAgent(agent);
|
|
408
|
+
perAgent[agent].messages_sent = count;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Team completion time from team_created → team_completed (or team_stopped as fallback)
|
|
349
412
|
let teamCompletionMs = null;
|
|
350
413
|
const created = events.find(e => e.type === 'team_created');
|
|
414
|
+
const completed = events.find(e => e.type === 'team_completed');
|
|
351
415
|
const stopped = events.find(e => e.type === 'team_stopped');
|
|
352
|
-
|
|
353
|
-
|
|
416
|
+
const endEvent = completed || stopped;
|
|
417
|
+
if (created && endEvent) {
|
|
418
|
+
teamCompletionMs = new Date(endEvent.at).getTime() - new Date(created.at).getTime();
|
|
354
419
|
}
|
|
355
420
|
|
|
356
421
|
return {
|
|
@@ -358,7 +423,9 @@ function aggregateTeamMetrics(rootDir, traceId) {
|
|
|
358
423
|
trace_id: traceId,
|
|
359
424
|
per_agent: perAgent,
|
|
360
425
|
per_gate: perGate,
|
|
426
|
+
all_files_modified: allFilesModified,
|
|
361
427
|
team_completion_ms: teamCompletionMs,
|
|
428
|
+
total_messages_sent: totalMessagesSent,
|
|
362
429
|
total_cost_usd: Math.round(totalCostUsd * 1_000_000) / 1_000_000,
|
|
363
430
|
computed_at: new Date().toISOString(),
|
|
364
431
|
};
|
|
@@ -438,5 +505,6 @@ module.exports = {
|
|
|
438
505
|
saveAggregatedMetrics,
|
|
439
506
|
computeAgentCost,
|
|
440
507
|
checkCostThreshold,
|
|
508
|
+
getModifiedFiles,
|
|
441
509
|
teamMetricsEmitter,
|
|
442
510
|
};
|