@howlil/ez-agents 3.4.1 → 3.5.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 (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Memory Compression — Compress long session transcripts
5
+ *
6
+ * Reduces session size by keeping first N and last M messages
7
+ */
8
+
9
+ const { defaultLogger: logger } = require('./logger.cjs');
10
+
11
+ class MemoryCompression {
12
+ /**
13
+ * Create a MemoryCompression instance
14
+ * @param {Object} sessionManager - SessionManager instance
15
+ */
16
+ constructor(sessionManager) {
17
+ this.sessionManager = sessionManager;
18
+ }
19
+
20
+ /**
21
+ * Compress a session transcript
22
+ * @param {string} sessionId - Session ID
23
+ * @param {Object} options - Compression options
24
+ * @param {number} [options.threshold=50] - Minimum messages before compression
25
+ * @param {number} [options.keepFirst=5] - Messages to keep at start
26
+ * @param {number} [options.keepLast=10] - Messages to keep at end
27
+ * @returns {Object} Compression result
28
+ */
29
+ compress(sessionId, options = {}) {
30
+ const session = this.sessionManager.loadSession(sessionId);
31
+ if (!session) {
32
+ return { compressed: false, reason: 'Session not found' };
33
+ }
34
+
35
+ const {
36
+ threshold = 50,
37
+ keepFirst = 5,
38
+ keepLast = 10
39
+ } = options;
40
+
41
+ const transcript = session.context?.transcript || '';
42
+
43
+ // Handle string transcript (split by newlines or messages)
44
+ let messages = [];
45
+ if (typeof transcript === 'string') {
46
+ // Try to parse as JSON array first
47
+ try {
48
+ messages = JSON.parse(transcript);
49
+ } catch {
50
+ // Split by newlines as fallback
51
+ messages = transcript.split('\n').filter(line => line.trim());
52
+ }
53
+ } else if (Array.isArray(transcript)) {
54
+ messages = transcript;
55
+ }
56
+
57
+ if (messages.length <= threshold) {
58
+ logger.info('Session below compression threshold', { sessionId, messageCount: messages.length });
59
+ return {
60
+ compressed: false,
61
+ reason: 'Below threshold',
62
+ messageCount: messages.length,
63
+ threshold
64
+ };
65
+ }
66
+
67
+ // Create compressed transcript
68
+ const firstMessages = messages.slice(0, keepFirst);
69
+ const lastMessages = messages.slice(-keepLast);
70
+ const compressedCount = messages.length - keepFirst - keepLast;
71
+
72
+ const placeholder = {
73
+ role: 'system',
74
+ content: `... ${compressedCount} messages compressed ...`,
75
+ timestamp: new Date().toISOString(),
76
+ compressed: true
77
+ };
78
+
79
+ const compressedMessages = [
80
+ ...firstMessages,
81
+ placeholder,
82
+ ...lastMessages
83
+ ];
84
+
85
+ // Update session
86
+ const updates = {
87
+ context: {
88
+ transcript: compressedMessages
89
+ },
90
+ metadata: {
91
+ compressed: true,
92
+ compressed_at: new Date().toISOString(),
93
+ compression_stats: {
94
+ original_count: messages.length,
95
+ compressed_count: compressedMessages.length,
96
+ removed_count: compressedCount
97
+ }
98
+ }
99
+ };
100
+
101
+ this.sessionManager.updateSession(sessionId, updates);
102
+
103
+ const reduction = Math.round((1 - compressedMessages.length / messages.length) * 100);
104
+
105
+ logger.info('Session compressed', {
106
+ sessionId,
107
+ originalLength: messages.length,
108
+ newLength: compressedMessages.length,
109
+ reduction
110
+ });
111
+
112
+ return {
113
+ compressed: true,
114
+ originalLength: messages.length,
115
+ newLength: compressedMessages.length,
116
+ reduction
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Get compression stats for a session
122
+ * @param {string} sessionId - Session ID
123
+ * @returns {Object} Compression statistics
124
+ */
125
+ getCompressionStats(sessionId) {
126
+ const session = this.sessionManager.loadSession(sessionId);
127
+ if (!session) {
128
+ return { compressed: false, reason: 'Session not found' };
129
+ }
130
+
131
+ if (!session.metadata?.compressed) {
132
+ return { compressed: false };
133
+ }
134
+
135
+ const originalSize = session.metadata.compression_stats?.original_count || 0;
136
+ const compressedSize = session.metadata.compression_stats?.compressed_count || 0;
137
+ const reduction = session.metadata.compression_stats?.removed_count || 0;
138
+ const reductionPercent = originalSize > 0
139
+ ? Math.round((reduction / originalSize) * 100)
140
+ : 0;
141
+
142
+ return {
143
+ compressed: true,
144
+ original_size: originalSize,
145
+ compressed_size: compressedSize,
146
+ reduction_percent: reductionPercent,
147
+ compressed_at: session.metadata.compressed_at
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Check if session should be compressed
153
+ * @param {string} sessionId - Session ID
154
+ * @param {number} threshold - Message threshold
155
+ * @returns {boolean} True if compression recommended
156
+ */
157
+ shouldCompress(sessionId, threshold = 50) {
158
+ const session = this.sessionManager.loadSession(sessionId);
159
+ if (!session) {
160
+ return false;
161
+ }
162
+
163
+ const transcript = session.context?.transcript || '';
164
+ let messageCount = 0;
165
+
166
+ if (typeof transcript === 'string') {
167
+ try {
168
+ const messages = JSON.parse(transcript);
169
+ messageCount = Array.isArray(messages) ? messages.length : 0;
170
+ } catch {
171
+ messageCount = transcript.split('\n').filter(line => line.trim()).length;
172
+ }
173
+ } else if (Array.isArray(transcript)) {
174
+ messageCount = transcript.length;
175
+ }
176
+
177
+ return messageCount > threshold;
178
+ }
179
+
180
+ /**
181
+ * Compress all sessions exceeding threshold
182
+ * @param {Object} options - Compression options
183
+ * @returns {Object} Batch compression result
184
+ */
185
+ compressAll(options = {}) {
186
+ const sessions = this.sessionManager.listSessions();
187
+ const results = {
188
+ total: sessions.length,
189
+ compressed: 0,
190
+ skipped: 0,
191
+ details: []
192
+ };
193
+
194
+ for (const sessionMeta of sessions) {
195
+ const shouldCompress = this.shouldCompress(sessionMeta.session_id, options.threshold);
196
+
197
+ if (shouldCompress) {
198
+ const result = this.compress(sessionMeta.session_id, options);
199
+ if (result.compressed) {
200
+ results.compressed++;
201
+ results.details.push({
202
+ sessionId: sessionMeta.session_id,
203
+ ...result
204
+ });
205
+ } else {
206
+ results.skipped++;
207
+ }
208
+ } else {
209
+ results.skipped++;
210
+ }
211
+ }
212
+
213
+ logger.info('Batch compression complete', {
214
+ total: results.total,
215
+ compressed: results.compressed,
216
+ skipped: results.skipped
217
+ });
218
+
219
+ return results;
220
+ }
221
+
222
+ /**
223
+ * Decompress a session (restore placeholder info)
224
+ * Note: Cannot restore original messages, only marks as decompressed
225
+ * @param {string} sessionId - Session ID
226
+ * @returns {Object} Decompression result
227
+ */
228
+ decompress(sessionId) {
229
+ const session = this.sessionManager.loadSession(sessionId);
230
+ if (!session) {
231
+ return { decompressed: false, reason: 'Session not found' };
232
+ }
233
+
234
+ if (!session.metadata?.compressed) {
235
+ return { decompressed: false, reason: 'Not compressed' };
236
+ }
237
+
238
+ // Remove compression metadata
239
+ this.sessionManager.updateSession(sessionId, {
240
+ metadata: {
241
+ compressed: false,
242
+ compressed_at: null,
243
+ compression_stats: null
244
+ }
245
+ });
246
+
247
+ logger.info('Session marked as decompressed', { sessionId });
248
+
249
+ return {
250
+ decompressed: true,
251
+ note: 'Original messages cannot be restored'
252
+ };
253
+ }
254
+ }
255
+
256
+ module.exports = MemoryCompression;
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Metrics Tracker — Records and queries EZ Agents success metrics
5
+ *
6
+ * Manages .planning/metrics.json: phase velocity, BDD pass rate,
7
+ * defect density, token cost, and DORA-inspired metrics.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { withLock } = require('./file-lock.cjs');
15
+
16
+ const METRICS_PATH = '.planning/metrics.json';
17
+
18
+ // ─────────────────────────────────────────────
19
+ // Metric Thresholds
20
+ // ─────────────────────────────────────────────
21
+
22
+ const METRIC_THRESHOLDS = {
23
+ deviation_rate: { warn: 0.2, bad: 0.4 }, // DORA: change failure rate
24
+ avg_velocity_min: { warn: 120, bad: 240 }, // warn if avg > 2h, bad if > 4h
25
+ bdd_pass_rate: { warn: 0.8, bad: 0.6 }, // MVP: 60%, Medium: 80%
26
+ test_coverage: { warn: 60, bad: 40 } // warn below 60%
27
+ };
28
+
29
+ function getThresholdStatus(value, thresholds, lowerIsBetter = false) {
30
+ if (lowerIsBetter) {
31
+ if (value >= thresholds.bad) return '🔴';
32
+ if (value >= thresholds.warn) return '🟡';
33
+ return '🟢';
34
+ } else {
35
+ if (value <= thresholds.bad) return '🔴';
36
+ if (value <= thresholds.warn) return '🟡';
37
+ return '🟢';
38
+ }
39
+ }
40
+
41
+ // ─────────────────────────────────────────────
42
+ // Schema defaults
43
+ // ─────────────────────────────────────────────
44
+
45
+ function defaultMetrics(project) {
46
+ return {
47
+ schema_version: '1.0',
48
+ project: project || 'unknown',
49
+ updated: new Date().toISOString(),
50
+ phase_metrics: [],
51
+ project_metrics: {
52
+ requirements_coverage_pct: 0,
53
+ test_coverage_pct: 0,
54
+ bdd_scenarios_total: 0,
55
+ bdd_scenarios_passing: 0,
56
+ bdd_scenarios_must: 0,
57
+ bdd_scenarios_must_passing: 0
58
+ },
59
+ agent_metrics: {
60
+ total_token_cost_usd: 0,
61
+ avg_cost_per_plan: 0,
62
+ deviation_rate: 0,
63
+ avg_plans_per_phase: 0,
64
+ avg_velocity_min_per_plan: 0
65
+ },
66
+ business_metrics: {
67
+ time_to_first_ship_days: null,
68
+ hotfixes_deployed: 0,
69
+ milestones_shipped: 0,
70
+ current_tier: 'mvp',
71
+ phases_total: 0,
72
+ phases_completed: 0
73
+ }
74
+ };
75
+ }
76
+
77
+ // ─────────────────────────────────────────────
78
+ // Load / Save
79
+ // ─────────────────────────────────────────────
80
+
81
+ function loadMetrics(metricsPath = METRICS_PATH) {
82
+ const fullPath = path.resolve(process.cwd(), metricsPath);
83
+ if (!fs.existsSync(fullPath)) {
84
+ return defaultMetrics();
85
+ }
86
+ try {
87
+ return JSON.parse(fs.readFileSync(fullPath, 'utf8'));
88
+ } catch {
89
+ return defaultMetrics();
90
+ }
91
+ }
92
+
93
+ function saveMetrics(metrics, metricsPath = METRICS_PATH) {
94
+ const fullPath = path.resolve(process.cwd(), metricsPath);
95
+ const dir = path.dirname(fullPath);
96
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
97
+ metrics.updated = new Date().toISOString();
98
+ fs.writeFileSync(fullPath, JSON.stringify(metrics, null, 2) + '\n', 'utf8');
99
+ }
100
+
101
+ async function saveMetricsLocked(metrics, metricsPath = METRICS_PATH) {
102
+ const fullPath = path.resolve(process.cwd(), metricsPath);
103
+ return withLock(fullPath, async () => {
104
+ saveMetrics(metrics, metricsPath);
105
+ });
106
+ }
107
+
108
+ // ─────────────────────────────────────────────
109
+ // Phase Metrics
110
+ // ─────────────────────────────────────────────
111
+
112
+ /**
113
+ * Record phase execution metrics (called by ez-executor after plan completion)
114
+ * @param {object} data - { phase, phase_name, plans_total, plans_completed, velocity_min, deviation_count, tasks_total, cost_usd }
115
+ */
116
+ async function recordPlanMetrics(data, metricsPath = METRICS_PATH) {
117
+ const fullPath = path.resolve(process.cwd(), metricsPath);
118
+ return withLock(fullPath, async () => {
119
+ const metrics = loadMetrics(metricsPath); // read INSIDE lock
120
+
121
+ const existing = metrics.phase_metrics.find(m => m.phase === data.phase);
122
+ if (existing) {
123
+ // Update existing phase entry
124
+ Object.assign(existing, {
125
+ plans_completed: data.plans_completed || existing.plans_completed,
126
+ velocity_min: data.velocity_min || existing.velocity_min,
127
+ deviation_count: (existing.deviation_count || 0) + (data.deviation_count || 0),
128
+ defect_density: data.tasks_total > 0
129
+ ? ((existing.deviation_count || 0) + (data.deviation_count || 0)) / data.tasks_total
130
+ : existing.defect_density
131
+ });
132
+ } else {
133
+ metrics.phase_metrics.push({
134
+ phase: data.phase,
135
+ phase_name: data.phase_name || `phase-${data.phase}`,
136
+ plans_total: data.plans_total || 0,
137
+ plans_completed: data.plans_completed || 0,
138
+ velocity_min: data.velocity_min || 0,
139
+ defect_density: data.tasks_total > 0 ? (data.deviation_count || 0) / data.tasks_total : 0,
140
+ bdd_pass_rate: null, // filled by verifier
141
+ bdd_must_passing: null,
142
+ bdd_must_total: null,
143
+ deviation_count: data.deviation_count || 0,
144
+ completed_at: new Date().toISOString()
145
+ });
146
+ }
147
+
148
+ // Update agent metrics
149
+ if (data.cost_usd) {
150
+ metrics.agent_metrics.total_token_cost_usd =
151
+ (metrics.agent_metrics.total_token_cost_usd || 0) + data.cost_usd;
152
+ }
153
+
154
+ // Recalculate averages
155
+ _recalcAverages(metrics);
156
+ saveMetrics(metrics, metricsPath); // write INSIDE lock
157
+ return metrics.phase_metrics.find(m => m.phase === data.phase);
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Record BDD verification results (called by ez-verifier)
163
+ * @param {object} data - { phase, bdd_must_passing, bdd_must_total, bdd_pass_rate, test_coverage_pct }
164
+ */
165
+ async function recordBddMetrics(data, metricsPath = METRICS_PATH) {
166
+ const fullPath = path.resolve(process.cwd(), metricsPath);
167
+ return withLock(fullPath, async () => {
168
+ const metrics = loadMetrics(metricsPath); // read INSIDE lock
169
+
170
+ const phaseEntry = metrics.phase_metrics.find(m => m.phase === data.phase);
171
+ if (phaseEntry) {
172
+ phaseEntry.bdd_pass_rate = data.bdd_pass_rate;
173
+ phaseEntry.bdd_must_passing = data.bdd_must_passing;
174
+ phaseEntry.bdd_must_total = data.bdd_must_total;
175
+ }
176
+
177
+ if (data.test_coverage_pct !== undefined) {
178
+ metrics.project_metrics.test_coverage_pct = data.test_coverage_pct;
179
+ }
180
+ if (data.bdd_must_passing !== undefined) {
181
+ metrics.project_metrics.bdd_scenarios_must_passing = data.bdd_must_passing;
182
+ metrics.project_metrics.bdd_scenarios_must = data.bdd_must_total;
183
+ }
184
+
185
+ saveMetrics(metrics, metricsPath); // write INSIDE lock
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Record release event (called by ez-release-agent)
191
+ * @param {object} data - { tier, version, is_hotfix }
192
+ */
193
+ async function recordRelease(data, metricsPath = METRICS_PATH) {
194
+ const fullPath = path.resolve(process.cwd(), metricsPath);
195
+ return withLock(fullPath, async () => {
196
+ const metrics = loadMetrics(metricsPath); // read INSIDE lock
197
+
198
+ if (data.is_hotfix) {
199
+ metrics.business_metrics.hotfixes_deployed =
200
+ (metrics.business_metrics.hotfixes_deployed || 0) + 1;
201
+ } else {
202
+ metrics.business_metrics.milestones_shipped =
203
+ (metrics.business_metrics.milestones_shipped || 0) + 1;
204
+
205
+ if (!metrics.business_metrics.first_ship_date) {
206
+ metrics.business_metrics.first_ship_date = new Date().toISOString();
207
+ // Calculate time to first ship from project init
208
+ const planningStateFile = '.planning/STATE.md';
209
+ if (fs.existsSync(planningStateFile)) {
210
+ const stat = fs.statSync(planningStateFile);
211
+ const initDate = stat.birthtime || stat.mtime;
212
+ const days = Math.ceil((Date.now() - initDate.getTime()) / (1000 * 60 * 60 * 24));
213
+ metrics.business_metrics.time_to_first_ship_days = days;
214
+ }
215
+ }
216
+ }
217
+
218
+ if (data.tier) {
219
+ metrics.business_metrics.current_tier = data.tier;
220
+ }
221
+
222
+ saveMetrics(metrics, metricsPath); // write INSIDE lock
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Update project-level metrics
228
+ * @param {object} data - { requirements_coverage_pct, phases_total, phases_completed }
229
+ */
230
+ async function updateProjectMetrics(data, metricsPath = METRICS_PATH) {
231
+ const fullPath = path.resolve(process.cwd(), metricsPath);
232
+ return withLock(fullPath, async () => {
233
+ const metrics = loadMetrics(metricsPath); // read INSIDE lock
234
+ Object.assign(metrics.project_metrics, data);
235
+ Object.assign(metrics.business_metrics, {
236
+ phases_total: data.phases_total || metrics.business_metrics.phases_total,
237
+ phases_completed: data.phases_completed || metrics.business_metrics.phases_completed
238
+ });
239
+ saveMetrics(metrics, metricsPath); // write INSIDE lock
240
+ });
241
+ }
242
+
243
+ // ─────────────────────────────────────────────
244
+ // Dashboard
245
+ // ─────────────────────────────────────────────
246
+
247
+ /**
248
+ * Generate enhanced /ez:stats dashboard
249
+ * @param {string} metricsPath
250
+ * @returns {string} Formatted dashboard
251
+ */
252
+ function generateDashboard(metricsPath = METRICS_PATH) {
253
+ const metrics = loadMetrics(metricsPath);
254
+ const pm = metrics.project_metrics;
255
+ const am = metrics.agent_metrics;
256
+ const bm = metrics.business_metrics;
257
+
258
+ // Velocity trend
259
+ const phases = metrics.phase_metrics.slice(-5);
260
+ let velocityTrend = '→ STABLE';
261
+ if (phases.length >= 3) {
262
+ const recent = phases.slice(-2).reduce((sum, p) => sum + (p.velocity_min || 0), 0) / 2;
263
+ const older = phases.slice(0, -2).reduce((sum, p) => sum + (p.velocity_min || 0), 0) / Math.max(1, phases.length - 2);
264
+ if (recent < older * 0.9) velocityTrend = '↑ IMPROVING';
265
+ else if (recent > older * 1.1) velocityTrend = '↓ SLOWING';
266
+ }
267
+
268
+ // BDD status
269
+ const bddRate = pm.bdd_scenarios_must > 0
270
+ ? Math.round((pm.bdd_scenarios_must_passing / pm.bdd_scenarios_must) * 100)
271
+ : null;
272
+
273
+ // Cost estimate remaining
274
+ const remainingPhases = (bm.phases_total || 0) - (bm.phases_completed || 0);
275
+ const costEst = am.avg_cost_per_plan > 0 && am.avg_plans_per_phase > 0
276
+ ? (remainingPhases * am.avg_plans_per_phase * am.avg_cost_per_plan).toFixed(2)
277
+ : null;
278
+
279
+ const lines = [
280
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
281
+ ' EZ ► PROJECT METRICS DASHBOARD',
282
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
283
+ '',
284
+ `PROGRESS: Phase ${bm.phases_completed || '?'}/${bm.phases_total || '?'} (${bm.phases_total ? Math.round((bm.phases_completed / bm.phases_total) * 100) : '?'}%) | Requirements ${pm.requirements_coverage_pct || 0}%${bddRate !== null ? ` | BDD @must ${bddRate}% (est.)` : ''}`,
285
+ `VELOCITY: ${am.avg_velocity_min_per_plan || '?'} min/plan avg | Trend: ${velocityTrend}`,
286
+ ];
287
+
288
+ // Fix 12: QUALITY line with threshold indicators
289
+ const coveragePct = pm.test_coverage_pct || 0;
290
+ const deviationRate = am.deviation_rate || 0;
291
+ const coverageIcon = getThresholdStatus(coveragePct, METRIC_THRESHOLDS.test_coverage);
292
+ const deviationIcon = getThresholdStatus(deviationRate, METRIC_THRESHOLDS.deviation_rate, true);
293
+ const deviationPct = Math.round(deviationRate * 100);
294
+ lines.push(`QUALITY: ${coverageIcon} Coverage ${coveragePct}% | ${deviationIcon} Deviation ${deviationPct}%${deviationRate >= METRIC_THRESHOLDS.deviation_rate.warn ? ` (warn: >${Math.round(METRIC_THRESHOLDS.deviation_rate.warn * 100)}%)` : ''} | Defect density ${am.deviation_rate ? am.deviation_rate.toFixed(2) : '?'}`);
295
+ lines.push(`COSTS: $${(am.total_token_cost_usd || 0).toFixed(2)} total | $${(am.avg_cost_per_plan || 0).toFixed(2)}/plan${costEst ? ` | Est. remaining: ~$${costEst}` : ''}`);
296
+ lines.push(`RELEASE: Tier: ${bm.current_tier || 'mvp'} | Hotfixes: ${bm.hotfixes_deployed || 0} | Ships: ${bm.milestones_shipped || 0}`);
297
+ lines.push('');
298
+
299
+ if (metrics.phase_metrics.length > 0) {
300
+ lines.push('Recent Phases:');
301
+ for (const p of metrics.phase_metrics.slice(-5).reverse()) {
302
+ const bdd = p.bdd_pass_rate !== null && p.bdd_pass_rate !== undefined
303
+ ? ` | BDD ${Math.round(p.bdd_pass_rate * 100)}%`
304
+ : '';
305
+ lines.push(` Phase ${p.phase} ${p.phase_name}: ${p.velocity_min || '?'}min | dev:${(p.defect_density || 0).toFixed(2)}${bdd}`);
306
+ }
307
+ lines.push('');
308
+ }
309
+
310
+ // Fix 12: Cost projection alert
311
+ if (am.total_token_cost_usd > 5) {
312
+ const projectedTotal = costEst ? parseFloat(costEst) + am.total_token_cost_usd : null;
313
+ if (projectedTotal && projectedTotal > 30) {
314
+ lines.push(`⚠ COST ALERT: Projected total ~$${projectedTotal.toFixed(0)}. Consider disabling scrum_master_standup for budget tiers.`);
315
+ }
316
+ }
317
+
318
+ lines.push('─────────────────────────────────────────────────────');
319
+ lines.push('⚠ BDD rates are ESTIMATED (scenario existence, not test runs).');
320
+ lines.push(' For actual pass rates, configure a BDD runner (Jest/Cucumber/Playwright).');
321
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
322
+
323
+ return lines.join('\n');
324
+ }
325
+
326
+ // ─────────────────────────────────────────────
327
+ // Internal helpers
328
+ // ─────────────────────────────────────────────
329
+
330
+ function _recalcAverages(metrics) {
331
+ const phases = metrics.phase_metrics;
332
+ if (phases.length === 0) return;
333
+
334
+ const withVelocity = phases.filter(p => p.velocity_min > 0);
335
+ if (withVelocity.length > 0) {
336
+ metrics.agent_metrics.avg_velocity_min_per_plan =
337
+ Math.round(withVelocity.reduce((sum, p) => sum + p.velocity_min, 0) / withVelocity.length);
338
+ }
339
+
340
+ const totalDeviations = phases.reduce((sum, p) => sum + (p.deviation_count || 0), 0);
341
+ const totalPlans = phases.reduce((sum, p) => sum + (p.plans_completed || 0), 0);
342
+ if (totalPlans > 0) {
343
+ metrics.agent_metrics.deviation_rate = parseFloat((totalDeviations / totalPlans).toFixed(3));
344
+ metrics.agent_metrics.avg_plans_per_phase = parseFloat((totalPlans / phases.length).toFixed(1));
345
+ }
346
+
347
+ if (metrics.agent_metrics.total_token_cost_usd > 0 && totalPlans > 0) {
348
+ metrics.agent_metrics.avg_cost_per_plan =
349
+ parseFloat((metrics.agent_metrics.total_token_cost_usd / totalPlans).toFixed(3));
350
+ }
351
+ }
352
+
353
+ // ─────────────────────────────────────────────
354
+ // CLI
355
+ // ─────────────────────────────────────────────
356
+
357
+ if (require.main === module) {
358
+ const args = process.argv.slice(2);
359
+ const cmd = args[0];
360
+
361
+ (async () => {
362
+ try {
363
+ if (cmd === 'record-plan') {
364
+ const data = JSON.parse(args[1] || '{}');
365
+ const result = await recordPlanMetrics(data);
366
+ console.log(JSON.stringify({ recorded: true, phase: result }));
367
+ } else if (cmd === 'record-bdd') {
368
+ const data = JSON.parse(args[1] || '{}');
369
+ await recordBddMetrics(data);
370
+ console.log(JSON.stringify({ recorded: true }));
371
+ } else if (cmd === 'record-release') {
372
+ const data = JSON.parse(args[1] || '{}');
373
+ await recordRelease(data);
374
+ console.log(JSON.stringify({ recorded: true }));
375
+ } else if (cmd === 'update-project') {
376
+ const data = JSON.parse(args[1] || '{}');
377
+ await updateProjectMetrics(data);
378
+ console.log(JSON.stringify({ updated: true }));
379
+ } else if (cmd === 'dashboard') {
380
+ console.log(generateDashboard());
381
+ } else if (cmd === 'get') {
382
+ const metrics = loadMetrics();
383
+ console.log(JSON.stringify(metrics, null, 2));
384
+ } else {
385
+ console.error('Commands: record-plan, record-bdd, record-release, update-project, dashboard, get');
386
+ process.exit(1);
387
+ }
388
+ } catch (err) {
389
+ console.error(`Error: ${err.message}`);
390
+ process.exit(1);
391
+ }
392
+ })();
393
+ }
394
+
395
+ module.exports = {
396
+ loadMetrics,
397
+ saveMetrics,
398
+ saveMetricsLocked,
399
+ recordPlanMetrics,
400
+ recordBddMetrics,
401
+ recordRelease,
402
+ updateProjectMetrics,
403
+ generateDashboard,
404
+ METRIC_THRESHOLDS,
405
+ getThresholdStatus
406
+ };