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.
Files changed (134) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/feature-flags.js +32 -4
  4. package/lib/skill-loader.js +0 -1
  5. package/package.json +1 -1
  6. package/scripts/agileflow-statusline.sh +81 -0
  7. package/scripts/babysit-clear-restore.js +154 -0
  8. package/scripts/claude-tmux.sh +120 -24
  9. package/scripts/claude-watchdog.sh +225 -0
  10. package/scripts/generators/agent-registry.js +14 -1
  11. package/scripts/generators/inject-babysit.js +22 -9
  12. package/scripts/generators/inject-help.js +19 -9
  13. package/scripts/lib/README-portable-tasks.md +424 -0
  14. package/scripts/lib/audit-cleanup.js +250 -0
  15. package/scripts/lib/audit-registry.js +248 -0
  16. package/scripts/lib/configure-detect.js +20 -0
  17. package/scripts/lib/feature-catalog.js +13 -2
  18. package/scripts/lib/gate-enforcer.js +295 -0
  19. package/scripts/lib/model-profiles.js +98 -0
  20. package/scripts/lib/signal-detectors.js +1 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/tdd-phase-manager.js +455 -0
  24. package/scripts/lib/team-events.js +76 -8
  25. package/scripts/lib/tmux-group-colors.js +113 -0
  26. package/scripts/messaging-bridge.js +209 -1
  27. package/scripts/spawn-audit-sessions.js +549 -0
  28. package/scripts/team-manager.js +37 -16
  29. package/scripts/tmux-close-windows.sh +180 -0
  30. package/scripts/tmux-restore-window.sh +67 -0
  31. package/scripts/tmux-save-closed-window.sh +35 -0
  32. package/src/core/agents/ads-audit-budget.md +181 -0
  33. package/src/core/agents/ads-audit-compliance.md +169 -0
  34. package/src/core/agents/ads-audit-creative.md +164 -0
  35. package/src/core/agents/ads-audit-google.md +226 -0
  36. package/src/core/agents/ads-audit-meta.md +183 -0
  37. package/src/core/agents/ads-audit-tracking.md +197 -0
  38. package/src/core/agents/ads-consensus.md +322 -0
  39. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  40. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  41. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  42. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  43. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  44. package/src/core/agents/brainstorm-consensus.md +237 -0
  45. package/src/core/agents/completeness-analyzer-api.md +190 -0
  46. package/src/core/agents/completeness-analyzer-conditional.md +201 -0
  47. package/src/core/agents/completeness-analyzer-handlers.md +159 -0
  48. package/src/core/agents/completeness-analyzer-imports.md +159 -0
  49. package/src/core/agents/completeness-analyzer-routes.md +182 -0
  50. package/src/core/agents/completeness-analyzer-state.md +188 -0
  51. package/src/core/agents/completeness-analyzer-stubs.md +198 -0
  52. package/src/core/agents/completeness-consensus.md +286 -0
  53. package/src/core/agents/perf-consensus.md +2 -2
  54. package/src/core/agents/security-consensus.md +2 -2
  55. package/src/core/agents/seo-analyzer-content.md +167 -0
  56. package/src/core/agents/seo-analyzer-images.md +187 -0
  57. package/src/core/agents/seo-analyzer-performance.md +206 -0
  58. package/src/core/agents/seo-analyzer-schema.md +176 -0
  59. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  60. package/src/core/agents/seo-analyzer-technical.md +144 -0
  61. package/src/core/agents/seo-consensus.md +289 -0
  62. package/src/core/agents/test-consensus.md +2 -2
  63. package/src/core/commands/ads/audit.md +375 -0
  64. package/src/core/commands/ads/budget.md +97 -0
  65. package/src/core/commands/ads/competitor.md +112 -0
  66. package/src/core/commands/ads/creative.md +85 -0
  67. package/src/core/commands/ads/google.md +112 -0
  68. package/src/core/commands/ads/landing.md +119 -0
  69. package/src/core/commands/ads/linkedin.md +112 -0
  70. package/src/core/commands/ads/meta.md +91 -0
  71. package/src/core/commands/ads/microsoft.md +115 -0
  72. package/src/core/commands/ads/plan.md +321 -0
  73. package/src/core/commands/ads/tiktok.md +129 -0
  74. package/src/core/commands/ads/youtube.md +124 -0
  75. package/src/core/commands/ads.md +128 -0
  76. package/src/core/commands/babysit.md +250 -1344
  77. package/src/core/commands/code/completeness.md +466 -0
  78. package/src/core/commands/{audit → code}/legal.md +26 -16
  79. package/src/core/commands/{audit → code}/logic.md +27 -16
  80. package/src/core/commands/{audit → code}/performance.md +30 -20
  81. package/src/core/commands/{audit → code}/security.md +32 -19
  82. package/src/core/commands/{audit → code}/test.md +30 -20
  83. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  84. package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
  85. package/src/core/commands/ideate/features.md +435 -0
  86. package/src/core/commands/seo/audit.md +373 -0
  87. package/src/core/commands/seo/competitor.md +174 -0
  88. package/src/core/commands/seo/content.md +107 -0
  89. package/src/core/commands/seo/geo.md +229 -0
  90. package/src/core/commands/seo/hreflang.md +140 -0
  91. package/src/core/commands/seo/images.md +96 -0
  92. package/src/core/commands/seo/page.md +198 -0
  93. package/src/core/commands/seo/plan.md +163 -0
  94. package/src/core/commands/seo/programmatic.md +131 -0
  95. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  96. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  97. package/src/core/commands/seo/references/quality-gates.md +91 -0
  98. package/src/core/commands/seo/references/schema-types.md +102 -0
  99. package/src/core/commands/seo/schema.md +183 -0
  100. package/src/core/commands/seo/sitemap.md +97 -0
  101. package/src/core/commands/seo/technical.md +100 -0
  102. package/src/core/commands/seo.md +107 -0
  103. package/src/core/commands/skill/list.md +68 -212
  104. package/src/core/commands/skill/recommend.md +216 -0
  105. package/src/core/commands/tdd-next.md +238 -0
  106. package/src/core/commands/tdd.md +210 -0
  107. package/src/core/experts/_core-expertise.yaml +105 -0
  108. package/src/core/experts/analytics/expertise.yaml +5 -99
  109. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  110. package/src/core/experts/compliance/expertise.yaml +6 -72
  111. package/src/core/experts/database/expertise.yaml +9 -52
  112. package/src/core/experts/documentation/expertise.yaml +7 -140
  113. package/src/core/experts/integrations/expertise.yaml +7 -127
  114. package/src/core/experts/mentor/expertise.yaml +8 -35
  115. package/src/core/experts/monitoring/expertise.yaml +7 -49
  116. package/src/core/experts/performance/expertise.yaml +1 -26
  117. package/src/core/experts/security/expertise.yaml +9 -34
  118. package/src/core/experts/ui/expertise.yaml +6 -36
  119. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  120. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  121. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  122. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  123. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  124. package/src/core/templates/agileflow-metadata.json +15 -1
  125. package/tools/cli/installers/ide/_base-ide.js +42 -5
  126. package/tools/cli/installers/ide/claude-code.js +13 -4
  127. package/tools/cli/lib/content-injector.js +160 -12
  128. package/tools/cli/lib/docs-setup.js +1 -1
  129. package/src/core/commands/skill/create.md +0 -698
  130. package/src/core/commands/skill/delete.md +0 -316
  131. package/src/core/commands/skill/edit.md +0 -359
  132. package/src/core/commands/skill/test.md +0 -394
  133. package/src/core/commands/skill/upgrade.md +0 -552
  134. 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 || DEFAULT_COST_THRESHOLD_USD;
127
- if (totalCostUsd > limit) {
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: totalCostUsd,
154
+ total_cost_usd: cost,
131
155
  threshold_usd: limit,
132
- message: `Team cost $${totalCostUsd.toFixed(4)} exceeds threshold $${limit.toFixed(2)}`,
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
- // Team completion time from team_created team_stopped
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
- if (created && stopped) {
353
- teamCompletionMs = new Date(stopped.at).getTime() - new Date(created.at).getTime();
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
  };