@howlil/ez-agents 3.4.2 → 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.
- package/README.md +77 -2
- package/agents/ez-observer-agent.md +260 -0
- package/agents/ez-release-agent.md +333 -0
- package/agents/ez-requirements-agent.md +377 -0
- package/agents/ez-scrum-master-agent.md +242 -0
- package/agents/ez-tech-lead-agent.md +267 -0
- package/bin/install.js +3221 -3272
- package/commands/ez/arch-review.md +102 -0
- package/commands/ez/execute-phase.md +11 -0
- package/commands/ez/export-session.md +79 -0
- package/commands/ez/gather-requirements.md +117 -0
- package/commands/ez/git-workflow.md +72 -0
- package/commands/ez/hotfix.md +120 -0
- package/commands/ez/import-session.md +82 -0
- package/commands/ez/list-sessions.md +96 -0
- package/commands/ez/package-manager.md +316 -0
- package/commands/ez/plan-phase.md +9 -1
- package/commands/ez/preflight.md +79 -0
- package/commands/ez/progress.md +13 -1
- package/commands/ez/release.md +153 -0
- package/commands/ez/resume.md +107 -0
- package/commands/ez/standup.md +85 -0
- package/ez-agents/bin/ez-tools.cjs +1095 -716
- package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
- package/ez-agents/bin/lib/content-scanner.cjs +238 -0
- package/ez-agents/bin/lib/context-cache.cjs +154 -0
- package/ez-agents/bin/lib/context-errors.cjs +71 -0
- package/ez-agents/bin/lib/context-manager.cjs +220 -0
- package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
- package/ez-agents/bin/lib/file-access.cjs +207 -0
- package/ez-agents/bin/lib/git-errors.cjs +83 -0
- package/ez-agents/bin/lib/git-utils.cjs +321 -203
- package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
- package/ez-agents/bin/lib/index.cjs +46 -2
- package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
- package/ez-agents/bin/lib/logger.cjs +124 -154
- package/ez-agents/bin/lib/memory-compression.cjs +256 -0
- package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
- package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
- package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
- package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
- package/ez-agents/bin/lib/release-validator.cjs +614 -0
- package/ez-agents/bin/lib/safe-exec.cjs +128 -214
- package/ez-agents/bin/lib/session-chain.cjs +304 -0
- package/ez-agents/bin/lib/session-errors.cjs +81 -0
- package/ez-agents/bin/lib/session-export.cjs +251 -0
- package/ez-agents/bin/lib/session-import.cjs +262 -0
- package/ez-agents/bin/lib/session-manager.cjs +280 -0
- package/ez-agents/bin/lib/tier-manager.cjs +428 -0
- package/ez-agents/bin/lib/url-fetch.cjs +170 -0
- package/ez-agents/references/metrics-schema.md +118 -0
- package/ez-agents/references/planning-config.md +140 -0
- package/ez-agents/references/tier-strategy.md +103 -0
- package/ez-agents/templates/bdd-feature.md +173 -0
- package/ez-agents/templates/discussion.md +68 -0
- package/ez-agents/templates/incident-runbook.md +205 -0
- package/ez-agents/templates/release-checklist.md +133 -0
- package/ez-agents/templates/rollback-plan.md +201 -0
- package/ez-agents/workflows/arch-review.md +54 -0
- package/ez-agents/workflows/autonomous.md +844 -743
- package/ez-agents/workflows/execute-phase.md +45 -0
- package/ez-agents/workflows/export-session.md +255 -0
- package/ez-agents/workflows/gather-requirements.md +206 -0
- package/ez-agents/workflows/help.md +92 -0
- package/ez-agents/workflows/hotfix.md +291 -0
- package/ez-agents/workflows/import-session.md +303 -0
- package/ez-agents/workflows/new-milestone.md +713 -384
- package/ez-agents/workflows/new-project.md +1107 -1113
- package/ez-agents/workflows/plan-phase.md +22 -0
- package/ez-agents/workflows/progress.md +15 -25
- package/ez-agents/workflows/release.md +253 -0
- package/ez-agents/workflows/resume-session.md +215 -0
- package/ez-agents/workflows/standup.md +64 -0
- package/package.json +9 -2
|
@@ -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
|
+
};
|