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.
Files changed (42) hide show
  1. package/.claude-plugin/manifest.json +19 -0
  2. package/.codex/INSTALL.md +36 -0
  3. package/.cursor-plugin/manifest.json +21 -0
  4. package/.opencode/INSTALL.md +52 -0
  5. package/ARCHITECTURE.md +69 -0
  6. package/README.md +381 -0
  7. package/bin/clawpowers.js +390 -0
  8. package/bin/clawpowers.sh +91 -0
  9. package/gemini-extension.json +32 -0
  10. package/hooks/session-start +205 -0
  11. package/hooks/session-start.cmd +43 -0
  12. package/hooks/session-start.js +163 -0
  13. package/package.json +54 -0
  14. package/runtime/feedback/analyze.js +621 -0
  15. package/runtime/feedback/analyze.sh +546 -0
  16. package/runtime/init.js +172 -0
  17. package/runtime/init.sh +145 -0
  18. package/runtime/metrics/collector.js +361 -0
  19. package/runtime/metrics/collector.sh +308 -0
  20. package/runtime/persistence/store.js +433 -0
  21. package/runtime/persistence/store.sh +303 -0
  22. package/skill.json +74 -0
  23. package/skills/agent-payments/SKILL.md +411 -0
  24. package/skills/brainstorming/SKILL.md +233 -0
  25. package/skills/content-pipeline/SKILL.md +282 -0
  26. package/skills/dispatching-parallel-agents/SKILL.md +305 -0
  27. package/skills/executing-plans/SKILL.md +255 -0
  28. package/skills/finishing-a-development-branch/SKILL.md +260 -0
  29. package/skills/learn-how-to-learn/SKILL.md +235 -0
  30. package/skills/market-intelligence/SKILL.md +288 -0
  31. package/skills/prospecting/SKILL.md +313 -0
  32. package/skills/receiving-code-review/SKILL.md +225 -0
  33. package/skills/requesting-code-review/SKILL.md +206 -0
  34. package/skills/security-audit/SKILL.md +308 -0
  35. package/skills/subagent-driven-development/SKILL.md +244 -0
  36. package/skills/systematic-debugging/SKILL.md +279 -0
  37. package/skills/test-driven-development/SKILL.md +299 -0
  38. package/skills/using-clawpowers/SKILL.md +137 -0
  39. package/skills/using-git-worktrees/SKILL.md +261 -0
  40. package/skills/verification-before-completion/SKILL.md +254 -0
  41. package/skills/writing-plans/SKILL.md +276 -0
  42. 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
+ };