agileflow 3.3.0 → 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 (121) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +6 -6
  3. package/lib/skill-loader.js +0 -1
  4. package/package.json +1 -1
  5. package/scripts/agileflow-statusline.sh +81 -0
  6. package/scripts/claude-tmux.sh +113 -22
  7. package/scripts/claude-watchdog.sh +225 -0
  8. package/scripts/generators/agent-registry.js +14 -1
  9. package/scripts/generators/inject-babysit.js +22 -9
  10. package/scripts/generators/inject-help.js +19 -9
  11. package/scripts/lib/audit-cleanup.js +250 -0
  12. package/scripts/lib/audit-registry.js +248 -0
  13. package/scripts/lib/feature-catalog.js +3 -3
  14. package/scripts/lib/gate-enforcer.js +295 -0
  15. package/scripts/lib/model-profiles.js +98 -0
  16. package/scripts/lib/signal-detectors.js +1 -1
  17. package/scripts/lib/skill-catalog.js +557 -0
  18. package/scripts/lib/skill-recommender.js +311 -0
  19. package/scripts/lib/tdd-phase-manager.js +455 -0
  20. package/scripts/lib/team-events.js +34 -3
  21. package/scripts/lib/tmux-group-colors.js +113 -0
  22. package/scripts/messaging-bridge.js +209 -1
  23. package/scripts/spawn-audit-sessions.js +549 -0
  24. package/scripts/team-manager.js +37 -16
  25. package/scripts/tmux-close-windows.sh +180 -0
  26. package/src/core/agents/ads-audit-budget.md +181 -0
  27. package/src/core/agents/ads-audit-compliance.md +169 -0
  28. package/src/core/agents/ads-audit-creative.md +164 -0
  29. package/src/core/agents/ads-audit-google.md +226 -0
  30. package/src/core/agents/ads-audit-meta.md +183 -0
  31. package/src/core/agents/ads-audit-tracking.md +197 -0
  32. package/src/core/agents/ads-consensus.md +322 -0
  33. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  34. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  35. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  36. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  37. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  38. package/src/core/agents/brainstorm-consensus.md +237 -0
  39. package/src/core/agents/completeness-consensus.md +5 -5
  40. package/src/core/agents/perf-consensus.md +2 -2
  41. package/src/core/agents/security-consensus.md +2 -2
  42. package/src/core/agents/seo-analyzer-content.md +167 -0
  43. package/src/core/agents/seo-analyzer-images.md +187 -0
  44. package/src/core/agents/seo-analyzer-performance.md +206 -0
  45. package/src/core/agents/seo-analyzer-schema.md +176 -0
  46. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  47. package/src/core/agents/seo-analyzer-technical.md +144 -0
  48. package/src/core/agents/seo-consensus.md +289 -0
  49. package/src/core/agents/test-consensus.md +2 -2
  50. package/src/core/commands/ads/audit.md +375 -0
  51. package/src/core/commands/ads/budget.md +97 -0
  52. package/src/core/commands/ads/competitor.md +112 -0
  53. package/src/core/commands/ads/creative.md +85 -0
  54. package/src/core/commands/ads/google.md +112 -0
  55. package/src/core/commands/ads/landing.md +119 -0
  56. package/src/core/commands/ads/linkedin.md +112 -0
  57. package/src/core/commands/ads/meta.md +91 -0
  58. package/src/core/commands/ads/microsoft.md +115 -0
  59. package/src/core/commands/ads/plan.md +321 -0
  60. package/src/core/commands/ads/tiktok.md +129 -0
  61. package/src/core/commands/ads/youtube.md +124 -0
  62. package/src/core/commands/ads.md +128 -0
  63. package/src/core/commands/babysit.md +249 -1284
  64. package/src/core/commands/{audit → code}/completeness.md +35 -25
  65. package/src/core/commands/{audit → code}/legal.md +26 -16
  66. package/src/core/commands/{audit → code}/logic.md +27 -16
  67. package/src/core/commands/{audit → code}/performance.md +30 -20
  68. package/src/core/commands/{audit → code}/security.md +32 -19
  69. package/src/core/commands/{audit → code}/test.md +30 -20
  70. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  71. package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
  72. package/src/core/commands/ideate/features.md +435 -0
  73. package/src/core/commands/seo/audit.md +373 -0
  74. package/src/core/commands/seo/competitor.md +174 -0
  75. package/src/core/commands/seo/content.md +107 -0
  76. package/src/core/commands/seo/geo.md +229 -0
  77. package/src/core/commands/seo/hreflang.md +140 -0
  78. package/src/core/commands/seo/images.md +96 -0
  79. package/src/core/commands/seo/page.md +198 -0
  80. package/src/core/commands/seo/plan.md +163 -0
  81. package/src/core/commands/seo/programmatic.md +131 -0
  82. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  83. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  84. package/src/core/commands/seo/references/quality-gates.md +91 -0
  85. package/src/core/commands/seo/references/schema-types.md +102 -0
  86. package/src/core/commands/seo/schema.md +183 -0
  87. package/src/core/commands/seo/sitemap.md +97 -0
  88. package/src/core/commands/seo/technical.md +100 -0
  89. package/src/core/commands/seo.md +107 -0
  90. package/src/core/commands/skill/list.md +68 -212
  91. package/src/core/commands/skill/recommend.md +216 -0
  92. package/src/core/commands/tdd-next.md +238 -0
  93. package/src/core/commands/tdd.md +210 -0
  94. package/src/core/experts/_core-expertise.yaml +105 -0
  95. package/src/core/experts/analytics/expertise.yaml +5 -99
  96. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  97. package/src/core/experts/compliance/expertise.yaml +6 -72
  98. package/src/core/experts/database/expertise.yaml +9 -52
  99. package/src/core/experts/documentation/expertise.yaml +7 -140
  100. package/src/core/experts/integrations/expertise.yaml +7 -127
  101. package/src/core/experts/mentor/expertise.yaml +8 -35
  102. package/src/core/experts/monitoring/expertise.yaml +7 -49
  103. package/src/core/experts/performance/expertise.yaml +1 -26
  104. package/src/core/experts/security/expertise.yaml +9 -34
  105. package/src/core/experts/ui/expertise.yaml +6 -36
  106. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  107. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  108. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  109. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  110. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  111. package/src/core/templates/agileflow-metadata.json +15 -1
  112. package/tools/cli/installers/ide/_base-ide.js +42 -5
  113. package/tools/cli/installers/ide/claude-code.js +3 -3
  114. package/tools/cli/lib/content-injector.js +160 -12
  115. package/tools/cli/lib/docs-setup.js +1 -1
  116. package/src/core/commands/skill/create.md +0 -698
  117. package/src/core/commands/skill/delete.md +0 -316
  118. package/src/core/commands/skill/edit.md +0 -359
  119. package/src/core/commands/skill/test.md +0 -394
  120. package/src/core/commands/skill/upgrade.md +0 -552
  121. 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',
@@ -171,9 +173,19 @@ function checkCostThreshold(rootDir, traceId, totalCostUsd, threshold) {
171
173
  * @returns {{ ok: boolean, error?: string }}
172
174
  */
173
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
+
174
185
  const event = {
175
186
  type: eventType,
176
187
  at: new Date().toISOString(),
188
+ agent_teams: isNative,
177
189
  ...data,
178
190
  };
179
191
 
@@ -380,12 +392,30 @@ function aggregateTeamMetrics(rootDir, traceId) {
380
392
  perGate[gate].pass_rate = total > 0 ? perGate[gate].passed / total : 0;
381
393
  }
382
394
 
383
- // 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)
384
412
  let teamCompletionMs = null;
385
413
  const created = events.find(e => e.type === 'team_created');
414
+ const completed = events.find(e => e.type === 'team_completed');
386
415
  const stopped = events.find(e => e.type === 'team_stopped');
387
- if (created && stopped) {
388
- 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();
389
419
  }
390
420
 
391
421
  return {
@@ -395,6 +425,7 @@ function aggregateTeamMetrics(rootDir, traceId) {
395
425
  per_gate: perGate,
396
426
  all_files_modified: allFilesModified,
397
427
  team_completion_ms: teamCompletionMs,
428
+ total_messages_sent: totalMessagesSent,
398
429
  total_cost_usd: Math.round(totalCostUsd * 1_000_000) / 1_000_000,
399
430
  computed_at: new Date().toISOString(),
400
431
  };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * tmux-group-colors.js - Color palette for tmux tab groups
3
+ *
4
+ * Provides a curated palette of 8 colors optimized for dark terminal
5
+ * backgrounds (Tokyo Night compatible). Supports color assignment by
6
+ * audit type and random selection with avoidance of in-use colors.
7
+ *
8
+ * Usage:
9
+ * const { getColorForAudit, pickGroupColor } = require('./tmux-group-colors');
10
+ * const color = getColorForAudit('security'); // '#f7768e'
11
+ * const random = pickGroupColor(['#f7768e']); // random excluding coral
12
+ */
13
+
14
+ /**
15
+ * Curated palette for dark backgrounds.
16
+ * Each color has sufficient contrast against #1a1b26 (Tokyo Night bg)
17
+ * and #2d2f3a (tab bg).
18
+ */
19
+ const GROUP_PALETTE = [
20
+ { name: 'coral', hex: '#f7768e', audit: 'security' },
21
+ { name: 'sky', hex: '#7aa2f7', audit: 'logic' },
22
+ { name: 'mint', hex: '#73daca', audit: 'performance' },
23
+ { name: 'amber', hex: '#e0af68', audit: 'test' },
24
+ { name: 'violet', hex: '#bb9af7', audit: 'completeness' },
25
+ { name: 'lime', hex: '#9ece6a', audit: 'legal' },
26
+ { name: 'rose', hex: '#ff9e64', audit: null },
27
+ { name: 'ice', hex: '#89ddff', audit: null },
28
+ ];
29
+
30
+ /**
31
+ * Map audit type to its assigned color.
32
+ */
33
+ const AUDIT_COLOR_MAP = {};
34
+ for (const entry of GROUP_PALETTE) {
35
+ if (entry.audit) {
36
+ AUDIT_COLOR_MAP[entry.audit] = entry.hex;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get the assigned color for an audit type.
42
+ *
43
+ * @param {string} auditType - Audit type key (security, logic, etc.)
44
+ * @returns {string} Hex color string
45
+ */
46
+ function getColorForAudit(auditType) {
47
+ return AUDIT_COLOR_MAP[auditType] || pickGroupColor([]);
48
+ }
49
+
50
+ /**
51
+ * Pick a group color, avoiding colors currently in use.
52
+ *
53
+ * @param {string[]} [inUseColors] - Array of hex colors currently in use
54
+ * @returns {string} Hex color string not in the in-use list
55
+ */
56
+ function pickGroupColor(inUseColors) {
57
+ const avoid = new Set((inUseColors || []).map(c => c.toLowerCase()));
58
+ const available = GROUP_PALETTE.filter(entry => !avoid.has(entry.hex.toLowerCase()));
59
+
60
+ if (available.length === 0) {
61
+ // All colors in use, pick first from full palette
62
+ return GROUP_PALETTE[0].hex;
63
+ }
64
+
65
+ // Deterministic pick: first available for consistency
66
+ return available[0].hex;
67
+ }
68
+
69
+ /**
70
+ * Get a palette entry by name.
71
+ *
72
+ * @param {string} name - Color name (e.g. 'coral', 'sky')
73
+ * @returns {object|null} Palette entry or null
74
+ */
75
+ function getColorByName(name) {
76
+ return GROUP_PALETTE.find(entry => entry.name === name) || null;
77
+ }
78
+
79
+ /**
80
+ * Build tmux window format string with group color.
81
+ *
82
+ * @param {string} groupColor - Hex color for the group
83
+ * @param {string} prefix - Short prefix (e.g. 'Sec', 'Logic')
84
+ * @param {boolean} [isActive=false] - Whether this is the active window
85
+ * @returns {string} tmux format string
86
+ */
87
+ function buildGroupWindowFormat(groupColor, prefix, isActive) {
88
+ if (isActive) {
89
+ // Active: colored bg for index, dark bg for name
90
+ return `#[fg=#1a1b26 bg=${groupColor} bold] #I #[fg=${groupColor} bg=#2d2f3a]#[fg=#e0e0e0] ${prefix}:#{window_name} #[bg=#1a1b26 fg=#2d2f3a]`;
91
+ }
92
+ // Inactive: colored dot prefix + gray text
93
+ return `#[fg=${groupColor}]#[fg=#8a8a8a] #I:${prefix}:#{window_name} `;
94
+ }
95
+
96
+ /**
97
+ * Get all palette colors.
98
+ *
99
+ * @returns {Array<{ name: string, hex: string, audit: string|null }>}
100
+ */
101
+ function getAllColors() {
102
+ return [...GROUP_PALETTE];
103
+ }
104
+
105
+ module.exports = {
106
+ GROUP_PALETTE,
107
+ AUDIT_COLOR_MAP,
108
+ getColorForAudit,
109
+ pickGroupColor,
110
+ getColorByName,
111
+ buildGroupWindowFormat,
112
+ getAllColors,
113
+ };