clawpowers 1.0.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/.claude-plugin/manifest.json +19 -0
- package/.codex/INSTALL.md +36 -0
- package/.cursor-plugin/manifest.json +21 -0
- package/.opencode/INSTALL.md +52 -0
- package/ARCHITECTURE.md +69 -0
- package/README.md +381 -0
- package/bin/clawpowers.js +390 -0
- package/bin/clawpowers.sh +91 -0
- package/gemini-extension.json +32 -0
- package/hooks/session-start +205 -0
- package/hooks/session-start.cmd +43 -0
- package/hooks/session-start.js +163 -0
- package/package.json +54 -0
- package/runtime/feedback/analyze.js +621 -0
- package/runtime/feedback/analyze.sh +546 -0
- package/runtime/init.js +172 -0
- package/runtime/init.sh +145 -0
- package/runtime/metrics/collector.js +361 -0
- package/runtime/metrics/collector.sh +308 -0
- package/runtime/persistence/store.js +433 -0
- package/runtime/persistence/store.sh +303 -0
- package/skill.json +74 -0
- package/skills/agent-payments/SKILL.md +411 -0
- package/skills/brainstorming/SKILL.md +233 -0
- package/skills/content-pipeline/SKILL.md +282 -0
- package/skills/dispatching-parallel-agents/SKILL.md +305 -0
- package/skills/executing-plans/SKILL.md +255 -0
- package/skills/finishing-a-development-branch/SKILL.md +260 -0
- package/skills/learn-how-to-learn/SKILL.md +235 -0
- package/skills/market-intelligence/SKILL.md +288 -0
- package/skills/prospecting/SKILL.md +313 -0
- package/skills/receiving-code-review/SKILL.md +225 -0
- package/skills/requesting-code-review/SKILL.md +206 -0
- package/skills/security-audit/SKILL.md +308 -0
- package/skills/subagent-driven-development/SKILL.md +244 -0
- package/skills/systematic-debugging/SKILL.md +279 -0
- package/skills/test-driven-development/SKILL.md +299 -0
- package/skills/using-clawpowers/SKILL.md +137 -0
- package/skills/using-git-worktrees/SKILL.md +261 -0
- package/skills/verification-before-completion/SKILL.md +254 -0
- package/skills/writing-plans/SKILL.md +276 -0
- package/skills/writing-skills/SKILL.md +260 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// runtime/feedback/analyze.js — RSI feedback engine
|
|
3
|
+
//
|
|
4
|
+
// Reads metrics, computes per-skill success rates, identifies declining performance,
|
|
5
|
+
// and outputs actionable recommendations for skill improvement.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node analyze.js Full analysis of all skills
|
|
9
|
+
// node analyze.js --skill <name> Analysis for one skill
|
|
10
|
+
// node analyze.js --plan <name> Plan execution analysis
|
|
11
|
+
// node analyze.js --worktrees Worktree lifecycle report
|
|
12
|
+
// node analyze.js --recommendations Show improvement recommendations only
|
|
13
|
+
// node analyze.js --format json Output as JSON (default: human-readable)
|
|
14
|
+
//
|
|
15
|
+
// RSI Cycle: measure → analyze → adapt
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// All runtime paths derived from CLAWPOWERS_DIR for testability
|
|
23
|
+
const CLAWPOWERS_DIR = process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers');
|
|
24
|
+
const METRICS_DIR = path.join(CLAWPOWERS_DIR, 'metrics');
|
|
25
|
+
const STATE_DIR = path.join(CLAWPOWERS_DIR, 'state');
|
|
26
|
+
const FEEDBACK_DIR = path.join(CLAWPOWERS_DIR, 'feedback');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensures all required runtime directories exist.
|
|
30
|
+
* Called at the start of each command function so analysis works even if
|
|
31
|
+
* the user has never run `clawpowers init`.
|
|
32
|
+
*/
|
|
33
|
+
function ensureDirs() {
|
|
34
|
+
for (const dir of [METRICS_DIR, STATE_DIR, FEEDBACK_DIR]) {
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns an ISO 8601 timestamp without milliseconds.
|
|
43
|
+
*
|
|
44
|
+
* @returns {string} e.g. "2025-01-15T12:00:00Z"
|
|
45
|
+
*/
|
|
46
|
+
function isoTimestamp() {
|
|
47
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads all JSONL metric records from every monthly log file.
|
|
52
|
+
* Files are read in chronological order (sorted by YYYY-MM filename).
|
|
53
|
+
* Malformed JSON lines are silently skipped.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} [skillFilter=''] - If non-empty, only return records for this skill name.
|
|
56
|
+
* @returns {Object[]} Parsed record objects in chronological order.
|
|
57
|
+
*/
|
|
58
|
+
function loadAllLines(skillFilter) {
|
|
59
|
+
if (!fs.existsSync(METRICS_DIR)) return [];
|
|
60
|
+
|
|
61
|
+
const files = fs.readdirSync(METRICS_DIR)
|
|
62
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
63
|
+
.sort() // YYYY-MM.jsonl sorts chronologically
|
|
64
|
+
.map(f => path.join(METRICS_DIR, f));
|
|
65
|
+
|
|
66
|
+
const lines = [];
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
69
|
+
for (const line of content.split('\n')) {
|
|
70
|
+
if (!line.trim()) continue;
|
|
71
|
+
try {
|
|
72
|
+
const record = JSON.parse(line);
|
|
73
|
+
if (skillFilter && record.skill !== skillFilter) continue;
|
|
74
|
+
lines.push(record);
|
|
75
|
+
} catch (_) { /* skip malformed lines without crashing */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return lines;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns a sorted, deduplicated list of all skill names that have been
|
|
83
|
+
* recorded in the metrics store.
|
|
84
|
+
*
|
|
85
|
+
* @returns {string[]} Alphabetically sorted array of skill names.
|
|
86
|
+
*/
|
|
87
|
+
function getAllSkills() {
|
|
88
|
+
const lines = loadAllLines(); // No filter — load all records
|
|
89
|
+
const skills = new Set(lines.map(r => r.skill).filter(Boolean));
|
|
90
|
+
return [...skills].sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Computes aggregate statistics for a single skill.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} skill - The skill name to analyze.
|
|
97
|
+
* @returns {{total: number, success: number, failure: number, partial: number,
|
|
98
|
+
* skipped: number, rate: number, avgDuration: number} | null}
|
|
99
|
+
* Statistics object, or null if no records exist for this skill.
|
|
100
|
+
* `rate` is success percentage (0-100). `avgDuration` is -1 if no durations recorded.
|
|
101
|
+
*/
|
|
102
|
+
function computeSkillStats(skill) {
|
|
103
|
+
const lines = loadAllLines(skill);
|
|
104
|
+
if (lines.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
let success = 0, failure = 0, partial = 0, skipped = 0;
|
|
107
|
+
let totalDuration = 0, durationCount = 0;
|
|
108
|
+
|
|
109
|
+
for (const r of lines) {
|
|
110
|
+
if (r.outcome === 'success') success++;
|
|
111
|
+
else if (r.outcome === 'failure') failure++;
|
|
112
|
+
else if (r.outcome === 'partial') partial++;
|
|
113
|
+
else if (r.outcome === 'skipped') skipped++;
|
|
114
|
+
|
|
115
|
+
if (typeof r.duration_s === 'number' && r.duration_s >= 0) {
|
|
116
|
+
totalDuration += r.duration_s;
|
|
117
|
+
durationCount++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const total = lines.length;
|
|
122
|
+
const rate = Math.round(success / total * 100);
|
|
123
|
+
const avgDuration = durationCount > 0 ? Math.round(totalDuration / durationCount) : -1;
|
|
124
|
+
|
|
125
|
+
return { total, success, failure, partial, skipped, rate, avgDuration };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detects whether a skill's recent performance is declining compared to its
|
|
130
|
+
* all-time success rate. "Recent" is defined as the last `window` executions.
|
|
131
|
+
*
|
|
132
|
+
* A decline is flagged when the all-time rate minus the recent rate is >= 20
|
|
133
|
+
* percentage points. This threshold avoids noise from small sample sizes.
|
|
134
|
+
*
|
|
135
|
+
* Requires at least 2×window total records to produce a meaningful comparison;
|
|
136
|
+
* returns null for skills with insufficient data.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} skill - Skill name to check.
|
|
139
|
+
* @param {number} [window=5] - Number of recent executions to compare against all-time.
|
|
140
|
+
* @returns {string | null} A descriptive message if declining, null otherwise.
|
|
141
|
+
*/
|
|
142
|
+
function detectDecline(skill, window = 5) {
|
|
143
|
+
const lines = loadAllLines(skill);
|
|
144
|
+
if (lines.length === 0) return null;
|
|
145
|
+
|
|
146
|
+
const total = lines.length;
|
|
147
|
+
// Need at least 2×window records for a meaningful all-time vs. recent comparison
|
|
148
|
+
if (total < window * 2) return null;
|
|
149
|
+
|
|
150
|
+
const allSuccess = lines.filter(r => r.outcome === 'success').length;
|
|
151
|
+
const allRate = allSuccess / total * 100;
|
|
152
|
+
|
|
153
|
+
// Compare against only the most recent `window` records
|
|
154
|
+
const recent = lines.slice(total - window);
|
|
155
|
+
const recentSuccess = recent.filter(r => r.outcome === 'success').length;
|
|
156
|
+
const recentRate = recentSuccess / recent.length * 100;
|
|
157
|
+
|
|
158
|
+
if (allRate - recentRate >= 20) {
|
|
159
|
+
return `DECLINING: ${skill} (all-time ${Math.round(allRate)}% → recent ${Math.round(recentRate)}%)`;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generates human-readable improvement recommendations for a skill based on
|
|
166
|
+
* its success rate and execution count.
|
|
167
|
+
*
|
|
168
|
+
* Three tiers:
|
|
169
|
+
* - <60%: Low — methodology review recommended
|
|
170
|
+
* - 60-79%: Moderate — check failure notes for patterns
|
|
171
|
+
* - ≥80%: Good — performing well
|
|
172
|
+
*
|
|
173
|
+
* Requires at least 3 executions before making recommendations; returns an
|
|
174
|
+
* "insufficient data" message for skills with fewer records.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} skill - Skill name (used in recommendation text).
|
|
177
|
+
* @param {number} rate - Success rate percentage (0-100).
|
|
178
|
+
* @param {number} total - Total number of executions recorded.
|
|
179
|
+
* @returns {string[]} Array of recommendation lines ready to print.
|
|
180
|
+
*/
|
|
181
|
+
function generateRecommendations(skill, rate, total) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
if (total < 3) {
|
|
184
|
+
lines.push(` Not enough data (${total} executions). Need 3+ to analyze.`);
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
if (rate < 60) {
|
|
188
|
+
lines.push(` ⚠ LOW SUCCESS RATE (${rate}%): Review skill methodology.`);
|
|
189
|
+
lines.push(` Consider: Is the 'When to Use' triggering at wrong times?`);
|
|
190
|
+
lines.push(` Consider: Are anti-patterns in the skill being followed anyway?`);
|
|
191
|
+
} else if (rate < 80) {
|
|
192
|
+
lines.push(` ℹ MODERATE RATE (${rate}%): Some improvement opportunity.`);
|
|
193
|
+
lines.push(` Review recent failure notes for common causes.`);
|
|
194
|
+
} else {
|
|
195
|
+
lines.push(` ✓ GOOD RATE (${rate}%): Skill performing well.`);
|
|
196
|
+
}
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================
|
|
201
|
+
// Store bridge — thin wrappers around store.js so analyze.js doesn't
|
|
202
|
+
// need store.js to be present (graceful degradation when store is missing)
|
|
203
|
+
// ============================================================
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Fetches a single value from the key-value store.
|
|
207
|
+
* Returns `defaultVal` if the store module is unavailable or the key doesn't exist.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} key - Store key in namespace:entity:attribute format.
|
|
210
|
+
* @param {string} defaultVal - Value to return on error or missing key.
|
|
211
|
+
* @returns {string} The stored value or the default.
|
|
212
|
+
*/
|
|
213
|
+
function storeGet(key, defaultVal) {
|
|
214
|
+
const storeJs = path.join(__dirname, '..', 'persistence', 'store.js');
|
|
215
|
+
if (!fs.existsSync(storeJs)) return defaultVal;
|
|
216
|
+
try {
|
|
217
|
+
const store = require(storeJs);
|
|
218
|
+
return store.cmdGet(key, defaultVal);
|
|
219
|
+
} catch (_) {
|
|
220
|
+
return defaultVal;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Lists all store keys matching a prefix.
|
|
226
|
+
* Returns an empty array if the store module is unavailable.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} prefix - Key prefix to filter by.
|
|
229
|
+
* @returns {string[]} Matching keys, or [] on error.
|
|
230
|
+
*/
|
|
231
|
+
function storeList(prefix) {
|
|
232
|
+
const storeJs = path.join(__dirname, '..', 'persistence', 'store.js');
|
|
233
|
+
if (!fs.existsSync(storeJs)) return [];
|
|
234
|
+
try {
|
|
235
|
+
const store = require(storeJs);
|
|
236
|
+
return store.cmdList(prefix);
|
|
237
|
+
} catch (_) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Lists all store key=value pairs matching a prefix.
|
|
244
|
+
* Returns an empty array if the store module is unavailable.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} prefix - Key prefix to filter by.
|
|
247
|
+
* @returns {string[]} Matching "key=value" strings, or [] on error.
|
|
248
|
+
*/
|
|
249
|
+
function storeListValues(prefix) {
|
|
250
|
+
const storeJs = path.join(__dirname, '..', 'persistence', 'store.js');
|
|
251
|
+
if (!fs.existsSync(storeJs)) return [];
|
|
252
|
+
try {
|
|
253
|
+
const store = require(storeJs);
|
|
254
|
+
return store.cmdListValues(prefix);
|
|
255
|
+
} catch (_) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================
|
|
261
|
+
// Command functions — each corresponds to one CLI invocation mode
|
|
262
|
+
// ============================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Full RSI analysis across all tracked skills.
|
|
266
|
+
* Prints per-skill statistics, recommendations, decline warnings, and
|
|
267
|
+
* an overall summary. Also saves a plain-text report to the feedback directory.
|
|
268
|
+
*
|
|
269
|
+
* This is the default view shown by `clawpowers status`.
|
|
270
|
+
*/
|
|
271
|
+
function cmdFullAnalysis() {
|
|
272
|
+
ensureDirs();
|
|
273
|
+
|
|
274
|
+
console.log('ClawPowers RSI Feedback Analysis');
|
|
275
|
+
console.log('=================================');
|
|
276
|
+
console.log(`Generated: ${isoTimestamp()}`);
|
|
277
|
+
console.log('');
|
|
278
|
+
|
|
279
|
+
const skills = getAllSkills();
|
|
280
|
+
|
|
281
|
+
if (skills.length === 0) {
|
|
282
|
+
console.log('No metrics found. Run some skills and record outcomes with:');
|
|
283
|
+
console.log(' node runtime/metrics/collector.js record --skill <name> --outcome success');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log('## Per-Skill Analysis');
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
let overallTotal = 0;
|
|
291
|
+
let overallSuccess = 0;
|
|
292
|
+
const decliningSkills = [];
|
|
293
|
+
|
|
294
|
+
for (const skill of skills) {
|
|
295
|
+
const stats = computeSkillStats(skill);
|
|
296
|
+
if (!stats) continue;
|
|
297
|
+
|
|
298
|
+
overallTotal += stats.total;
|
|
299
|
+
overallSuccess += stats.success;
|
|
300
|
+
|
|
301
|
+
console.log(`### ${skill}`);
|
|
302
|
+
|
|
303
|
+
// Build stat line, appending avg duration only when we have duration data
|
|
304
|
+
let statLine = ` Executions: ${stats.total} | Success rate: ${stats.rate}%`;
|
|
305
|
+
if (stats.avgDuration >= 0) statLine += ` | Avg duration: ${stats.avgDuration}s`;
|
|
306
|
+
console.log(statLine);
|
|
307
|
+
|
|
308
|
+
// Print improvement recommendations based on the success rate tier
|
|
309
|
+
generateRecommendations(skill, stats.rate, stats.total).forEach(r => console.log(r));
|
|
310
|
+
|
|
311
|
+
// Flag skills with a significant performance drop in recent executions
|
|
312
|
+
const decline = detectDecline(skill);
|
|
313
|
+
if (decline) {
|
|
314
|
+
console.log(` ⚠ ${decline}`);
|
|
315
|
+
decliningSkills.push(skill);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log('');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Aggregate stats across all skills
|
|
322
|
+
console.log('## Overall Summary');
|
|
323
|
+
if (overallTotal > 0) {
|
|
324
|
+
const overallRate = Math.round(overallSuccess / overallTotal * 100);
|
|
325
|
+
console.log(` Total executions: ${overallTotal}`);
|
|
326
|
+
console.log(` Overall success rate: ${overallRate}%`);
|
|
327
|
+
|
|
328
|
+
if (decliningSkills.length > 0) {
|
|
329
|
+
console.log('');
|
|
330
|
+
console.log(` ⚠ Declining skills: ${decliningSkills.join(' ')}`);
|
|
331
|
+
console.log(' These skills show degraded performance in recent executions.');
|
|
332
|
+
console.log(' Recommended: Review skill methodology and recent failure notes.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Count state keys and metrics files for the runtime health section
|
|
337
|
+
let stateKeyCount = 0;
|
|
338
|
+
if (fs.existsSync(STATE_DIR)) {
|
|
339
|
+
stateKeyCount = fs.readdirSync(STATE_DIR).filter(f =>
|
|
340
|
+
fs.statSync(path.join(STATE_DIR, f)).isFile()
|
|
341
|
+
).length;
|
|
342
|
+
}
|
|
343
|
+
let metricsFileCount = 0;
|
|
344
|
+
if (fs.existsSync(METRICS_DIR)) {
|
|
345
|
+
metricsFileCount = fs.readdirSync(METRICS_DIR).filter(f => f.endsWith('.jsonl')).length;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log('');
|
|
349
|
+
console.log('## Runtime State');
|
|
350
|
+
console.log(` State keys stored: ${stateKeyCount}`);
|
|
351
|
+
console.log(` Metrics files: ${metricsFileCount}`);
|
|
352
|
+
|
|
353
|
+
// Persist a compact summary to the feedback directory for later reference
|
|
354
|
+
const reportFile = path.join(FEEDBACK_DIR, `analysis-${new Date().toISOString().slice(0, 10)}.txt`);
|
|
355
|
+
const safeRate = overallTotal > 0 ? Math.round(overallSuccess / overallTotal * 100) : 0;
|
|
356
|
+
const reportLines = [
|
|
357
|
+
`Analysis generated: ${isoTimestamp()}`,
|
|
358
|
+
`Overall success rate: ${safeRate}%`,
|
|
359
|
+
`Total executions: ${overallTotal}`,
|
|
360
|
+
];
|
|
361
|
+
if (decliningSkills.length > 0) reportLines.push(`Declining: ${decliningSkills.join(' ')}`);
|
|
362
|
+
try {
|
|
363
|
+
fs.writeFileSync(reportFile, reportLines.join('\n') + '\n', { mode: 0o600 });
|
|
364
|
+
} catch (_) { /* non-fatal: report save failure doesn't affect console output */ }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Detailed analysis for a single named skill.
|
|
369
|
+
* Shows statistics, recommendations, the 5 most recent executions, and
|
|
370
|
+
* any related keys in the state store.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} skill - Name of the skill to analyze.
|
|
373
|
+
*/
|
|
374
|
+
function cmdSkillAnalysis(skill) {
|
|
375
|
+
if (!skill) {
|
|
376
|
+
process.stderr.write('Error: --skill requires a skill name\n');
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
ensureDirs();
|
|
381
|
+
|
|
382
|
+
console.log(`Skill Analysis: ${skill}`);
|
|
383
|
+
console.log('='.repeat(40));
|
|
384
|
+
console.log(`Generated: ${isoTimestamp()}`);
|
|
385
|
+
console.log('');
|
|
386
|
+
|
|
387
|
+
const stats = computeSkillStats(skill);
|
|
388
|
+
if (!stats) {
|
|
389
|
+
console.log(`No metrics found for skill: ${skill}`);
|
|
390
|
+
console.log('Record some executions with:');
|
|
391
|
+
console.log(` node runtime/metrics/collector.js record --skill ${skill} --outcome success`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const failRate = Math.round(stats.failure / stats.total * 100);
|
|
396
|
+
|
|
397
|
+
console.log('## Statistics');
|
|
398
|
+
console.log(` Total executions: ${stats.total}`);
|
|
399
|
+
console.log(` Success: ${stats.success} (${stats.rate}%)`);
|
|
400
|
+
console.log(` Failure: ${stats.failure} (${failRate}%)`);
|
|
401
|
+
if (stats.avgDuration >= 0) {
|
|
402
|
+
// Show duration in both seconds and minutes+seconds for readability
|
|
403
|
+
const mins = Math.floor(stats.avgDuration / 60);
|
|
404
|
+
const secs = stats.avgDuration % 60;
|
|
405
|
+
console.log(` Average duration: ${stats.avgDuration}s (${mins}m ${secs}s)`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
console.log('');
|
|
409
|
+
console.log('## Recommendations');
|
|
410
|
+
generateRecommendations(skill, stats.rate, stats.total).forEach(r => console.log(r));
|
|
411
|
+
|
|
412
|
+
// Show last 5 executions as a quick sanity check on recent behavior
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log('## Recent Executions');
|
|
415
|
+
const lines = loadAllLines(skill).slice(-5);
|
|
416
|
+
for (const r of lines) {
|
|
417
|
+
const ts = r.ts || '';
|
|
418
|
+
// Pad outcome to 10 chars for aligned column output
|
|
419
|
+
const outcome = (r.outcome || '').padEnd(10);
|
|
420
|
+
const notes = r.notes || '(no notes)';
|
|
421
|
+
console.log(` ${ts} | ${outcome} | ${notes}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Show any store keys that belong to this skill's namespace
|
|
425
|
+
console.log('');
|
|
426
|
+
console.log('## Related State Keys');
|
|
427
|
+
const relatedKeys = storeList(`${skill}:`);
|
|
428
|
+
if (relatedKeys.length === 0) {
|
|
429
|
+
console.log(' (none)');
|
|
430
|
+
} else {
|
|
431
|
+
relatedKeys.forEach(k => console.log(` ${k}`));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Analyzes the execution of a named plan.
|
|
437
|
+
* Reads estimated vs. actual duration from the store and computes estimation
|
|
438
|
+
* accuracy. Also lists all task statuses tracked under this plan's namespace.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} planName - Plan identifier (as used in store keys).
|
|
441
|
+
*/
|
|
442
|
+
function cmdPlanAnalysis(planName) {
|
|
443
|
+
if (!planName) {
|
|
444
|
+
process.stderr.write('Error: --plan requires a plan name\n');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
ensureDirs();
|
|
449
|
+
|
|
450
|
+
console.log(`Plan Execution Analysis: ${planName}`);
|
|
451
|
+
console.log('='.repeat(50));
|
|
452
|
+
console.log('');
|
|
453
|
+
|
|
454
|
+
// Read plan timing metadata from the store (set by the executing-plans skill)
|
|
455
|
+
const estimated = storeGet(`plan:${planName}:estimated_duration`, 'unknown');
|
|
456
|
+
const actual = storeGet(`plan:${planName}:actual_duration`, 'unknown');
|
|
457
|
+
|
|
458
|
+
console.log(`Estimated duration: ${estimated}min`);
|
|
459
|
+
console.log(`Actual duration: ${actual}min`);
|
|
460
|
+
|
|
461
|
+
if (estimated !== 'unknown' && actual !== 'unknown') {
|
|
462
|
+
// Accuracy ratio: 1.0 = perfect, >1.0 = took longer than estimated
|
|
463
|
+
const error = parseFloat(actual) / parseFloat(estimated);
|
|
464
|
+
console.log(`Estimation accuracy: ${error.toFixed(1)}x (1.0 = perfect)`);
|
|
465
|
+
// Flag significant underestimates (>30% over estimate) with a recommendation
|
|
466
|
+
if (error > 1.3) {
|
|
467
|
+
console.log(`Recommendation: Increase task time estimates by ${error.toFixed(1)}x for similar work`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log('');
|
|
472
|
+
console.log('Task Status:');
|
|
473
|
+
// Task keys follow the pattern: execution:<planName>:task_<n>:status
|
|
474
|
+
const taskPairs = storeListValues(`execution:${planName}:task_`);
|
|
475
|
+
if (taskPairs.length === 0) {
|
|
476
|
+
console.log(' (none)');
|
|
477
|
+
} else {
|
|
478
|
+
for (const pair of taskPairs) {
|
|
479
|
+
const eqIdx = pair.indexOf('=');
|
|
480
|
+
// Pad the key to 40 chars for aligned two-column output
|
|
481
|
+
const key = pair.slice(0, eqIdx).padEnd(40);
|
|
482
|
+
const val = pair.slice(eqIdx + 1);
|
|
483
|
+
console.log(` ${key} ${val}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Reports on all active git worktrees tracked in the state store.
|
|
490
|
+
* Worktrees are registered by the using-git-worktrees skill and should be
|
|
491
|
+
* cleaned up after branch merges.
|
|
492
|
+
*/
|
|
493
|
+
function cmdWorktreeReport() {
|
|
494
|
+
ensureDirs();
|
|
495
|
+
|
|
496
|
+
console.log('Worktree Lifecycle Report');
|
|
497
|
+
console.log('=========================');
|
|
498
|
+
console.log('');
|
|
499
|
+
|
|
500
|
+
console.log('Active Worktrees:');
|
|
501
|
+
// Worktree keys are registered under the "worktree:" namespace
|
|
502
|
+
const worktreePairs = storeListValues('worktree:');
|
|
503
|
+
if (worktreePairs.length === 0) {
|
|
504
|
+
console.log(' (none registered)');
|
|
505
|
+
} else {
|
|
506
|
+
for (const pair of worktreePairs) {
|
|
507
|
+
const eqIdx = pair.indexOf('=');
|
|
508
|
+
const key = pair.slice(0, eqIdx);
|
|
509
|
+
const val = pair.slice(eqIdx + 1);
|
|
510
|
+
console.log(` ${key}: ${val}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
console.log('');
|
|
515
|
+
console.log('Tip: After merging a branch, clean up its worktree:');
|
|
516
|
+
console.log(' git worktree remove <path> && git branch -d <branch>');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Shows only the skills that have improvement recommendations (success rate < 80%).
|
|
521
|
+
* Useful for quick triage without the full analysis output.
|
|
522
|
+
* Skills with fewer than 3 executions are excluded (insufficient data).
|
|
523
|
+
*/
|
|
524
|
+
function cmdRecommendations() {
|
|
525
|
+
ensureDirs();
|
|
526
|
+
|
|
527
|
+
console.log('ClawPowers Recommendations');
|
|
528
|
+
console.log('==========================');
|
|
529
|
+
console.log('');
|
|
530
|
+
|
|
531
|
+
const skills = getAllSkills();
|
|
532
|
+
|
|
533
|
+
if (skills.length === 0) {
|
|
534
|
+
console.log('No metrics yet. Record skill outcomes to get recommendations.');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let hasRecommendations = false;
|
|
539
|
+
|
|
540
|
+
for (const skill of skills) {
|
|
541
|
+
const stats = computeSkillStats(skill);
|
|
542
|
+
if (!stats) continue;
|
|
543
|
+
// Only surface skills that have enough data and are underperforming
|
|
544
|
+
if (stats.total >= 3 && stats.rate < 80) {
|
|
545
|
+
console.log(`[${skill}] Success rate: ${stats.rate}% (${stats.total} executions)`);
|
|
546
|
+
generateRecommendations(skill, stats.rate, stats.total).forEach(r => console.log(r));
|
|
547
|
+
console.log('');
|
|
548
|
+
hasRecommendations = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!hasRecommendations) {
|
|
553
|
+
console.log('All tracked skills performing well (≥80% success rate).');
|
|
554
|
+
console.log('Keep recording outcomes to refine this analysis.');
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Prints usage information for the analyze CLI to stdout.
|
|
560
|
+
*/
|
|
561
|
+
function printUsage() {
|
|
562
|
+
console.log(`Usage: analyze.js [options]
|
|
563
|
+
|
|
564
|
+
Options:
|
|
565
|
+
(no args) Full analysis of all skills
|
|
566
|
+
--skill <name> Analysis for one specific skill
|
|
567
|
+
--plan <name> Plan execution analysis (duration, task status)
|
|
568
|
+
--worktrees Worktree lifecycle report
|
|
569
|
+
--recommendations Show improvement recommendations only
|
|
570
|
+
--format json JSON output (future: human is default)
|
|
571
|
+
|
|
572
|
+
Examples:
|
|
573
|
+
node analyze.js
|
|
574
|
+
node analyze.js --skill systematic-debugging
|
|
575
|
+
node analyze.js --plan auth-service
|
|
576
|
+
node analyze.js --worktrees
|
|
577
|
+
node analyze.js --recommendations`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* CLI dispatch — routes the first flag argument to the appropriate command.
|
|
582
|
+
*
|
|
583
|
+
* @param {string[]} argv - Argument array (typically process.argv.slice(2)).
|
|
584
|
+
*/
|
|
585
|
+
function main(argv) {
|
|
586
|
+
const [flag, value] = argv;
|
|
587
|
+
|
|
588
|
+
switch (flag) {
|
|
589
|
+
case '--skill': cmdSkillAnalysis(value); break;
|
|
590
|
+
case '--plan': cmdPlanAnalysis(value); break;
|
|
591
|
+
case '--worktrees': cmdWorktreeReport(); break;
|
|
592
|
+
case '--recommendations': cmdRecommendations(); break;
|
|
593
|
+
// --format accepts a format name; human-readable is the only current format
|
|
594
|
+
case '--format': cmdFullAnalysis(); break;
|
|
595
|
+
case 'help':
|
|
596
|
+
case '-h':
|
|
597
|
+
case '--help': printUsage(); break;
|
|
598
|
+
case undefined:
|
|
599
|
+
case '': cmdFullAnalysis(); break;
|
|
600
|
+
default:
|
|
601
|
+
process.stderr.write(`Unknown option: ${flag}\n`);
|
|
602
|
+
printUsage();
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Guard: only run CLI dispatch when invoked directly, not when require()'d
|
|
608
|
+
if (require.main === module) {
|
|
609
|
+
try {
|
|
610
|
+
main(process.argv.slice(2));
|
|
611
|
+
} catch (err) {
|
|
612
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
module.exports = {
|
|
618
|
+
cmdFullAnalysis, cmdSkillAnalysis, cmdPlanAnalysis,
|
|
619
|
+
cmdWorktreeReport, cmdRecommendations,
|
|
620
|
+
loadAllLines, computeSkillStats, detectDecline, generateRecommendations,
|
|
621
|
+
};
|