clawpowers 1.1.4 → 2.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/CHANGELOG.md +94 -0
- package/LICENSE +44 -0
- package/README.md +204 -228
- package/SECURITY.md +72 -0
- package/dist/index.d.ts +844 -0
- package/dist/index.js +2536 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -44
- package/.claude-plugin/manifest.json +0 -19
- package/.codex/INSTALL.md +0 -36
- package/.cursor-plugin/manifest.json +0 -21
- package/.opencode/INSTALL.md +0 -52
- package/ARCHITECTURE.md +0 -69
- package/bin/clawpowers.js +0 -625
- package/bin/clawpowers.sh +0 -91
- package/docs/demo/clawpowers-demo.cast +0 -197
- package/docs/demo/clawpowers-demo.gif +0 -0
- package/docs/launch-images/25-skills-breakdown.jpg +0 -0
- package/docs/launch-images/clawpowers-vs-superpowers.jpg +0 -0
- package/docs/launch-images/economic-code-optimization.jpg +0 -0
- package/docs/launch-images/native-vs-bridge-2.jpg +0 -0
- package/docs/launch-images/native-vs-bridge.jpg +0 -0
- package/docs/launch-images/post1-hero-lobster.jpg +0 -0
- package/docs/launch-images/post2-dashboard.jpg +0 -0
- package/docs/launch-images/post3-superpowers.jpg +0 -0
- package/docs/launch-images/post4-before-after.jpg +0 -0
- package/docs/launch-images/post5-install-now.jpg +0 -0
- package/docs/launch-images/ultimate-stack.jpg +0 -0
- package/docs/launch-posts.md +0 -76
- package/docs/quickstart-first-transaction.md +0 -204
- package/gemini-extension.json +0 -32
- package/hooks/session-start +0 -205
- package/hooks/session-start.cmd +0 -43
- package/hooks/session-start.js +0 -163
- package/runtime/demo/README.md +0 -78
- package/runtime/demo/x402-mock-server.js +0 -230
- package/runtime/feedback/analyze.js +0 -621
- package/runtime/feedback/analyze.sh +0 -546
- package/runtime/init.js +0 -210
- package/runtime/init.sh +0 -178
- package/runtime/metrics/collector.js +0 -361
- package/runtime/metrics/collector.sh +0 -308
- package/runtime/payments/ledger.js +0 -305
- package/runtime/payments/ledger.sh +0 -262
- package/runtime/payments/pipeline.js +0 -455
- package/runtime/persistence/store.js +0 -433
- package/runtime/persistence/store.sh +0 -303
- package/skill.json +0 -106
- package/skills/agent-bounties/SKILL.md +0 -553
- package/skills/agent-payments/SKILL.md +0 -479
- package/skills/brainstorming/SKILL.md +0 -233
- package/skills/content-pipeline/SKILL.md +0 -282
- package/skills/cross-project-knowledge/SKILL.md +0 -345
- package/skills/dispatching-parallel-agents/SKILL.md +0 -305
- package/skills/economic-code-optimization/SKILL.md +0 -265
- package/skills/executing-plans/SKILL.md +0 -255
- package/skills/finishing-a-development-branch/SKILL.md +0 -260
- package/skills/formal-verification-lite/SKILL.md +0 -441
- package/skills/learn-how-to-learn/SKILL.md +0 -235
- package/skills/market-intelligence/SKILL.md +0 -323
- package/skills/meta-skill-evolution/SKILL.md +0 -325
- package/skills/prospecting/SKILL.md +0 -454
- package/skills/receiving-code-review/SKILL.md +0 -225
- package/skills/requesting-code-review/SKILL.md +0 -206
- package/skills/security-audit/SKILL.md +0 -353
- package/skills/self-healing-code/SKILL.md +0 -369
- package/skills/subagent-driven-development/SKILL.md +0 -244
- package/skills/systematic-debugging/SKILL.md +0 -355
- package/skills/test-driven-development/SKILL.md +0 -416
- package/skills/using-clawpowers/SKILL.md +0 -160
- package/skills/using-git-worktrees/SKILL.md +0 -261
- package/skills/validator/SKILL.md +0 -281
- package/skills/verification-before-completion/SKILL.md +0 -254
- package/skills/writing-plans/SKILL.md +0 -276
- package/skills/writing-skills/SKILL.md +0 -260
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// runtime/metrics/collector.js — Skill execution outcome tracking
|
|
3
|
-
//
|
|
4
|
-
// Appends one JSON line per skill execution to ~/.clawpowers/metrics/YYYY-MM.jsonl
|
|
5
|
-
// Each line records: skill name, timestamp, duration, outcome, and notes.
|
|
6
|
-
//
|
|
7
|
-
// Usage:
|
|
8
|
-
// node collector.js record --skill <name> --outcome <success|failure|partial|skipped> [options]
|
|
9
|
-
// node collector.js show [--skill <name>] [--limit <n>]
|
|
10
|
-
// node collector.js summary [--skill <name>]
|
|
11
|
-
//
|
|
12
|
-
// Options for record:
|
|
13
|
-
// --skill <name> Skill name (required)
|
|
14
|
-
// --outcome <result> success, failure, partial, or skipped (required)
|
|
15
|
-
// --duration <seconds> Execution duration in seconds (optional)
|
|
16
|
-
// --notes <text> Free-text notes about this execution (optional)
|
|
17
|
-
// --session-id <id> Session identifier for grouping (optional)
|
|
18
|
-
//
|
|
19
|
-
// Output format (one JSON line per execution):
|
|
20
|
-
// {"ts":"ISO8601","skill":"name","outcome":"success","duration_s":47,"notes":"...","session":"..."}
|
|
21
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
const os = require('os');
|
|
26
|
-
|
|
27
|
-
// Metrics directory — monthly JSONL files are written here
|
|
28
|
-
const METRICS_DIR = path.join(
|
|
29
|
-
process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers'),
|
|
30
|
-
'metrics'
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Accepted outcome values — any other value triggers a validation error
|
|
34
|
-
const VALID_OUTCOMES = new Set(['success', 'failure', 'partial', 'skipped']);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Creates the metrics directory if it doesn't already exist.
|
|
38
|
-
* Mode 0o700 restricts access to the current user only.
|
|
39
|
-
*/
|
|
40
|
-
function ensureDir() {
|
|
41
|
-
if (!fs.existsSync(METRICS_DIR)) {
|
|
42
|
-
fs.mkdirSync(METRICS_DIR, { recursive: true, mode: 0o700 });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Returns the path to the current month's JSONL log file.
|
|
48
|
-
* Files are rotated monthly so individual files stay manageable and
|
|
49
|
-
* historical data can be archived or deleted by month.
|
|
50
|
-
*
|
|
51
|
-
* Example output: ~/.clawpowers/metrics/2025-01.jsonl
|
|
52
|
-
*
|
|
53
|
-
* @returns {string} Absolute path to this month's log file.
|
|
54
|
-
*/
|
|
55
|
-
function currentLogfile() {
|
|
56
|
-
const now = new Date();
|
|
57
|
-
const year = now.getUTCFullYear();
|
|
58
|
-
// Pad month to two digits: January = "01", not "1"
|
|
59
|
-
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
60
|
-
return path.join(METRICS_DIR, `${year}-${month}.jsonl`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Returns an ISO 8601 timestamp without milliseconds.
|
|
65
|
-
* Milliseconds are stripped for compactness in the JSONL records.
|
|
66
|
-
*
|
|
67
|
-
* @returns {string} e.g. "2025-01-15T12:00:00Z"
|
|
68
|
-
*/
|
|
69
|
-
function isoTimestamp() {
|
|
70
|
-
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parses a flat --key value style argument array into an options object.
|
|
75
|
-
* Every argument must be a --flag followed by its value; bare positional
|
|
76
|
-
* arguments are rejected with an error.
|
|
77
|
-
*
|
|
78
|
-
* Example: ['--skill', 'my-skill', '--outcome', 'success'] → { skill: 'my-skill', outcome: 'success' }
|
|
79
|
-
*
|
|
80
|
-
* @param {string[]} argv - Array of argument strings.
|
|
81
|
-
* @returns {Object.<string, string>} Parsed key-value pairs.
|
|
82
|
-
* @throws {Error} If a flag is missing its value or an unknown positional argument is encountered.
|
|
83
|
-
*/
|
|
84
|
-
function parseArgs(argv) {
|
|
85
|
-
const opts = {};
|
|
86
|
-
for (let i = 0; i < argv.length; i++) {
|
|
87
|
-
const arg = argv[i];
|
|
88
|
-
if (arg.startsWith('--')) {
|
|
89
|
-
const key = arg.slice(2); // Strip leading '--'
|
|
90
|
-
const value = argv[i + 1];
|
|
91
|
-
// A flag with no following value, or whose "value" is another flag, is invalid
|
|
92
|
-
if (value === undefined || value.startsWith('--')) {
|
|
93
|
-
throw new Error(`Option ${arg} requires a value`);
|
|
94
|
-
}
|
|
95
|
-
opts[key] = value;
|
|
96
|
-
i++; // Skip the consumed value
|
|
97
|
-
} else {
|
|
98
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return opts;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* `record` command — appends one JSONL record to the current month's log file.
|
|
106
|
-
*
|
|
107
|
-
* Required options: --skill, --outcome
|
|
108
|
-
* Optional options: --duration, --notes, --session-id
|
|
109
|
-
*
|
|
110
|
-
* @param {string[]} argv - Argument array after 'record' (e.g. ['--skill', 'foo', '--outcome', 'success']).
|
|
111
|
-
* @throws {Error} If required fields are missing or values are invalid.
|
|
112
|
-
*/
|
|
113
|
-
function cmdRecord(argv) {
|
|
114
|
-
const opts = parseArgs(argv);
|
|
115
|
-
|
|
116
|
-
// Validate required fields before touching the filesystem
|
|
117
|
-
if (!opts.skill) throw new Error('--skill is required');
|
|
118
|
-
if (!opts.outcome) throw new Error('--outcome is required (success|failure|partial|skipped)');
|
|
119
|
-
if (!VALID_OUTCOMES.has(opts.outcome)) {
|
|
120
|
-
throw new Error(`--outcome must be success, failure, partial, or skipped`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Duration is optional, but if provided it must be a non-negative number
|
|
124
|
-
const duration = opts.duration !== undefined ? opts.duration : null;
|
|
125
|
-
if (duration !== null && !/^\d+(\.\d+)?$/.test(duration)) {
|
|
126
|
-
throw new Error('--duration must be a number (seconds)');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
ensureDir();
|
|
130
|
-
|
|
131
|
-
// Build the record object — only include optional fields when provided
|
|
132
|
-
const record = {
|
|
133
|
-
ts: isoTimestamp(),
|
|
134
|
-
skill: opts.skill,
|
|
135
|
-
outcome: opts.outcome,
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
if (duration !== null) record.duration_s = parseFloat(duration);
|
|
139
|
-
if (opts.notes) record.notes = opts.notes;
|
|
140
|
-
// 'session-id' CLI arg maps to 'session' in the stored record for brevity
|
|
141
|
-
if (opts['session-id']) record.session = opts['session-id'];
|
|
142
|
-
|
|
143
|
-
const jsonLine = JSON.stringify(record);
|
|
144
|
-
const logfile = currentLogfile();
|
|
145
|
-
|
|
146
|
-
// appendFileSync is safe here — each append is a complete JSON line
|
|
147
|
-
fs.appendFileSync(logfile, jsonLine + '\n');
|
|
148
|
-
// Restrict log file to owner-only access (may already be set; non-fatal on Windows)
|
|
149
|
-
try { fs.chmodSync(logfile, 0o600); } catch (_) { /* non-fatal on Windows */ }
|
|
150
|
-
|
|
151
|
-
console.log(`Recorded: ${opts.skill} → ${opts.outcome} (${path.basename(logfile)})`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Reads all JSONL metric records from every monthly log file, optionally
|
|
156
|
-
* filtering to a single skill. Records are returned in chronological order
|
|
157
|
-
* (files are sorted by filename which is YYYY-MM.jsonl).
|
|
158
|
-
*
|
|
159
|
-
* Malformed JSON lines are silently skipped rather than crashing — log files
|
|
160
|
-
* may be partially corrupted without invalidating the rest of the data.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} [skillFilter=''] - If non-empty, only return records for this skill.
|
|
163
|
-
* @returns {Object[]} Array of parsed record objects in chronological order.
|
|
164
|
-
*/
|
|
165
|
-
function loadAllLines(skillFilter) {
|
|
166
|
-
if (!fs.existsSync(METRICS_DIR)) return [];
|
|
167
|
-
|
|
168
|
-
// Sort filenames so we read records in chronological order (YYYY-MM.jsonl sorts lexicographically)
|
|
169
|
-
const files = fs.readdirSync(METRICS_DIR)
|
|
170
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
171
|
-
.sort()
|
|
172
|
-
.map(f => path.join(METRICS_DIR, f));
|
|
173
|
-
|
|
174
|
-
const lines = [];
|
|
175
|
-
for (const file of files) {
|
|
176
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
177
|
-
for (const line of content.split('\n')) {
|
|
178
|
-
if (!line.trim()) continue; // Skip blank lines between records
|
|
179
|
-
try {
|
|
180
|
-
const record = JSON.parse(line);
|
|
181
|
-
if (skillFilter && record.skill !== skillFilter) continue;
|
|
182
|
-
lines.push(record);
|
|
183
|
-
} catch (_) {
|
|
184
|
-
// Skip malformed lines — don't crash on a single bad record
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return lines;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* `show` command — prints recent execution records as JSON lines to stdout.
|
|
193
|
-
* Supports optional skill filter and record limit.
|
|
194
|
-
*
|
|
195
|
-
* @param {string[]} argv - Argument array (e.g. ['--skill', 'foo', '--limit', '10']).
|
|
196
|
-
*/
|
|
197
|
-
function cmdShow(argv) {
|
|
198
|
-
const opts = parseArgs(argv);
|
|
199
|
-
const skillFilter = opts.skill || '';
|
|
200
|
-
const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
|
|
201
|
-
|
|
202
|
-
ensureDir();
|
|
203
|
-
const lines = loadAllLines(skillFilter);
|
|
204
|
-
// Show the last `limit` records — tail semantics (most recent first makes no sense for a log)
|
|
205
|
-
const slice = lines.slice(Math.max(0, lines.length - limit));
|
|
206
|
-
slice.forEach(record => console.log(JSON.stringify(record)));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Computes aggregate statistics from an array of metric records.
|
|
211
|
-
* Duration statistics are computed only over records that include a duration_s field.
|
|
212
|
-
*
|
|
213
|
-
* @param {Object[]} lines - Array of parsed JSONL records.
|
|
214
|
-
* @returns {{total: number, success: number, failure: number, partial: number,
|
|
215
|
-
* skipped: number, rate: number, avgDuration: number}} Statistics object.
|
|
216
|
-
* `rate` is the success percentage (0-100). `avgDuration` is -1 if no durations were recorded.
|
|
217
|
-
*/
|
|
218
|
-
function computeStats(lines) {
|
|
219
|
-
let success = 0, failure = 0, partial = 0, skipped = 0;
|
|
220
|
-
let totalDuration = 0, durationCount = 0;
|
|
221
|
-
|
|
222
|
-
for (const r of lines) {
|
|
223
|
-
if (r.outcome === 'success') success++;
|
|
224
|
-
else if (r.outcome === 'failure') failure++;
|
|
225
|
-
else if (r.outcome === 'partial') partial++;
|
|
226
|
-
else if (r.outcome === 'skipped') skipped++;
|
|
227
|
-
|
|
228
|
-
// Only include records with a valid non-negative duration in the average
|
|
229
|
-
if (typeof r.duration_s === 'number' && r.duration_s >= 0) {
|
|
230
|
-
totalDuration += r.duration_s;
|
|
231
|
-
durationCount++;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const total = lines.length;
|
|
236
|
-
const rate = total > 0 ? Math.round(success / total * 100) : 0;
|
|
237
|
-
const avgDuration = durationCount > 0 ? Math.round(totalDuration / durationCount) : -1;
|
|
238
|
-
|
|
239
|
-
return { total, success, failure, partial, skipped, rate, avgDuration };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* `summary` command — prints aggregated statistics to stdout.
|
|
244
|
-
* When no skill filter is provided, also prints a per-skill breakdown.
|
|
245
|
-
*
|
|
246
|
-
* @param {string[]} argv - Argument array (e.g. ['--skill', 'foo']).
|
|
247
|
-
*/
|
|
248
|
-
function cmdSummary(argv) {
|
|
249
|
-
const opts = parseArgs(argv);
|
|
250
|
-
const skillFilter = opts.skill || '';
|
|
251
|
-
|
|
252
|
-
ensureDir();
|
|
253
|
-
const lines = loadAllLines(skillFilter);
|
|
254
|
-
|
|
255
|
-
if (lines.length === 0) {
|
|
256
|
-
console.log(`No metrics recorded${skillFilter ? ` for skill: ${skillFilter}` : ''}`);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const stats = computeStats(lines);
|
|
261
|
-
|
|
262
|
-
// Format percentages as integers — floating point noise isn't meaningful here
|
|
263
|
-
console.log(`Total executions: ${stats.total}`);
|
|
264
|
-
console.log(` Success: ${stats.success} (${stats.rate}%)`);
|
|
265
|
-
console.log(` Failure: ${stats.failure} (${Math.round(stats.failure / stats.total * 100)}%)`);
|
|
266
|
-
console.log(` Partial: ${stats.partial} (${Math.round(stats.partial / stats.total * 100)}%)`);
|
|
267
|
-
if (stats.skipped > 0) {
|
|
268
|
-
console.log(` Skipped: ${stats.skipped} (${Math.round(stats.skipped / stats.total * 100)}%)`);
|
|
269
|
-
}
|
|
270
|
-
if (stats.avgDuration >= 0) {
|
|
271
|
-
console.log(`Avg duration: ${stats.avgDuration}s`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!skillFilter) {
|
|
275
|
-
// Per-skill breakdown — shows which skills have been used most
|
|
276
|
-
const skillCounts = {};
|
|
277
|
-
for (const r of lines) {
|
|
278
|
-
skillCounts[r.skill] = (skillCounts[r.skill] || 0) + 1;
|
|
279
|
-
}
|
|
280
|
-
console.log('\nSkill breakdown:');
|
|
281
|
-
// Sort alphabetically for consistent output
|
|
282
|
-
for (const [skill, count] of Object.entries(skillCounts).sort()) {
|
|
283
|
-
console.log(` ${skill}: ${count}`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Prints usage information for the collector CLI to stdout.
|
|
290
|
-
*/
|
|
291
|
-
function printUsage() {
|
|
292
|
-
console.log(`Usage: collector.js <command> [options]
|
|
293
|
-
|
|
294
|
-
Commands:
|
|
295
|
-
record Record a skill execution outcome
|
|
296
|
-
show Show recent execution records
|
|
297
|
-
summary Show aggregated statistics
|
|
298
|
-
|
|
299
|
-
record options:
|
|
300
|
-
--skill <name> Skill name (required)
|
|
301
|
-
--outcome <result> success | failure | partial | skipped (required)
|
|
302
|
-
--duration <seconds> Execution time in seconds
|
|
303
|
-
--notes <text> Notes about this execution
|
|
304
|
-
--session-id <id> Session identifier
|
|
305
|
-
|
|
306
|
-
Examples:
|
|
307
|
-
collector.js record --skill systematic-debugging --outcome success --duration 1800 \\
|
|
308
|
-
--notes "payment-pool: 3 hypotheses, root cause found in git bisect"
|
|
309
|
-
|
|
310
|
-
collector.js show --skill test-driven-development --limit 10
|
|
311
|
-
|
|
312
|
-
collector.js summary
|
|
313
|
-
collector.js summary --skill systematic-debugging`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* CLI dispatch — routes argv to the appropriate command function.
|
|
318
|
-
*
|
|
319
|
-
* @param {string[]} argv - Argument array (typically process.argv.slice(2)).
|
|
320
|
-
*/
|
|
321
|
-
function main(argv) {
|
|
322
|
-
const [cmd, ...rest] = argv;
|
|
323
|
-
|
|
324
|
-
switch (cmd) {
|
|
325
|
-
case 'record':
|
|
326
|
-
cmdRecord(rest);
|
|
327
|
-
break;
|
|
328
|
-
case 'show':
|
|
329
|
-
cmdShow(rest);
|
|
330
|
-
break;
|
|
331
|
-
case 'summary':
|
|
332
|
-
cmdSummary(rest);
|
|
333
|
-
break;
|
|
334
|
-
case 'help':
|
|
335
|
-
case '-h':
|
|
336
|
-
case '--help':
|
|
337
|
-
printUsage();
|
|
338
|
-
break;
|
|
339
|
-
case undefined:
|
|
340
|
-
case '':
|
|
341
|
-
printUsage();
|
|
342
|
-
process.exit(1);
|
|
343
|
-
break;
|
|
344
|
-
default:
|
|
345
|
-
process.stderr.write(`Unknown command: ${cmd}\n`);
|
|
346
|
-
printUsage();
|
|
347
|
-
process.exit(1);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Guard: only run CLI dispatch when invoked directly, not when require()'d
|
|
352
|
-
if (require.main === module) {
|
|
353
|
-
try {
|
|
354
|
-
main(process.argv.slice(2));
|
|
355
|
-
} catch (err) {
|
|
356
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
module.exports = { cmdRecord, cmdShow, cmdSummary, loadAllLines, computeStats, METRICS_DIR };
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# runtime/metrics/collector.sh — Skill execution outcome tracking
|
|
3
|
-
#
|
|
4
|
-
# Appends one JSON line per skill execution to ~/.clawpowers/metrics/YYYY-MM.jsonl
|
|
5
|
-
# Each line records: skill name, timestamp, duration, outcome, and notes.
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# collector.sh record --skill <name> --outcome <success|failure|partial|skipped> [options]
|
|
9
|
-
# collector.sh show [--skill <name>] [--limit <n>]
|
|
10
|
-
# collector.sh summary [--skill <name>]
|
|
11
|
-
#
|
|
12
|
-
# Options for record:
|
|
13
|
-
# --skill <name> Skill name (required)
|
|
14
|
-
# --outcome <result> success, failure, partial, or skipped (required)
|
|
15
|
-
# --duration <seconds> Execution duration in seconds (optional)
|
|
16
|
-
# --notes <text> Free-text notes about this execution (optional)
|
|
17
|
-
# --session-id <id> Session identifier for grouping (optional)
|
|
18
|
-
#
|
|
19
|
-
# Output format (one JSON line per execution):
|
|
20
|
-
# {"ts":"ISO8601","skill":"name","outcome":"success","duration_s":47,"notes":"...","session":"..."}
|
|
21
|
-
set -euo pipefail
|
|
22
|
-
|
|
23
|
-
## === Configuration ===
|
|
24
|
-
|
|
25
|
-
# Metrics directory — override parent with CLAWPOWERS_DIR env var for testing
|
|
26
|
-
METRICS_DIR="${CLAWPOWERS_DIR:-$HOME/.clawpowers}/metrics"
|
|
27
|
-
|
|
28
|
-
## === Internal Utilities ===
|
|
29
|
-
|
|
30
|
-
# Creates the metrics directory if it doesn't already exist.
|
|
31
|
-
# Mode 700 ensures log files are accessible only to the current user.
|
|
32
|
-
ensure_dir() {
|
|
33
|
-
if [[ ! -d "$METRICS_DIR" ]]; then
|
|
34
|
-
mkdir -p "$METRICS_DIR"
|
|
35
|
-
chmod 700 "$METRICS_DIR"
|
|
36
|
-
fi
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# Returns the path to the current month's JSONL log file.
|
|
40
|
-
# Files are named YYYY-MM.jsonl and rotated automatically each month.
|
|
41
|
-
# Monthly rotation keeps individual files manageable without any cleanup overhead.
|
|
42
|
-
current_logfile() {
|
|
43
|
-
local month
|
|
44
|
-
month=$(date +%Y-%m)
|
|
45
|
-
echo "$METRICS_DIR/${month}.jsonl"
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Escapes a string for safe embedding in a JSON double-quoted value.
|
|
49
|
-
# Order matters: backslashes must be escaped before quotes, then control characters.
|
|
50
|
-
json_string() {
|
|
51
|
-
local s="$1"
|
|
52
|
-
s="${s//\\/\\\\}" # Escape backslashes first (must be before quote escaping)
|
|
53
|
-
s="${s//\"/\\\"}" # Escape double quotes
|
|
54
|
-
s="${s//$'\n'/\\n}" # Escape newlines (notes may span multiple lines)
|
|
55
|
-
s="${s//$'\r'/\\r}" # Escape carriage returns (Windows line endings)
|
|
56
|
-
s="${s//$'\t'/\\t}" # Escape tabs
|
|
57
|
-
echo "$s"
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
## === Command Implementations ===
|
|
61
|
-
|
|
62
|
-
# record — Append one JSONL record to the current month's log file.
|
|
63
|
-
# Parses --flag value style arguments and validates required fields before writing.
|
|
64
|
-
cmd_record() {
|
|
65
|
-
local skill="" outcome="" duration="" notes="" session_id=""
|
|
66
|
-
|
|
67
|
-
## --- Argument Parsing ---
|
|
68
|
-
# Parse --key value style flags; reject unknown arguments
|
|
69
|
-
while [[ $# -gt 0 ]]; do
|
|
70
|
-
case "$1" in
|
|
71
|
-
--skill) skill="$2"; shift 2 ;;
|
|
72
|
-
--outcome) outcome="$2"; shift 2 ;;
|
|
73
|
-
--duration) duration="$2"; shift 2 ;;
|
|
74
|
-
--notes) notes="$2"; shift 2 ;;
|
|
75
|
-
--session-id) session_id="$2"; shift 2 ;;
|
|
76
|
-
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
77
|
-
esac
|
|
78
|
-
done
|
|
79
|
-
|
|
80
|
-
## --- Validation ---
|
|
81
|
-
if [[ -z "$skill" ]]; then
|
|
82
|
-
echo "Error: --skill is required" >&2
|
|
83
|
-
exit 1
|
|
84
|
-
fi
|
|
85
|
-
if [[ -z "$outcome" ]]; then
|
|
86
|
-
echo "Error: --outcome is required (success|failure|partial|skipped)" >&2
|
|
87
|
-
exit 1
|
|
88
|
-
fi
|
|
89
|
-
# Only allow the three defined outcome values to maintain data consistency
|
|
90
|
-
if [[ ! "$outcome" =~ ^(success|failure|partial|skipped)$ ]]; then
|
|
91
|
-
echo "Error: --outcome must be success, failure, or partial" >&2
|
|
92
|
-
exit 1
|
|
93
|
-
fi
|
|
94
|
-
# Duration must be a non-negative number (integer or decimal seconds)
|
|
95
|
-
if [[ -n "$duration" ]] && ! [[ "$duration" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
|
|
96
|
-
echo "Error: --duration must be a number (seconds)" >&2
|
|
97
|
-
exit 1
|
|
98
|
-
fi
|
|
99
|
-
|
|
100
|
-
ensure_dir
|
|
101
|
-
|
|
102
|
-
local ts
|
|
103
|
-
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
104
|
-
|
|
105
|
-
## --- Optional JSON Field Fragments ---
|
|
106
|
-
# Build optional field strings only when values are present; empty fields
|
|
107
|
-
# are omitted entirely from the JSON record to keep file sizes minimal
|
|
108
|
-
|
|
109
|
-
local session_part=""
|
|
110
|
-
if [[ -n "$session_id" ]]; then
|
|
111
|
-
session_part=',"session":"'"$(json_string "$session_id")"'"'
|
|
112
|
-
fi
|
|
113
|
-
|
|
114
|
-
local duration_part=""
|
|
115
|
-
if [[ -n "$duration" ]]; then
|
|
116
|
-
# duration_s is a JSON number, not a string — no quotes around the value
|
|
117
|
-
duration_part=',"duration_s":'"$duration"
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
local notes_part=""
|
|
121
|
-
if [[ -n "$notes" ]]; then
|
|
122
|
-
notes_part=',"notes":"'"$(json_string "$notes")"'"'
|
|
123
|
-
fi
|
|
124
|
-
|
|
125
|
-
local logfile
|
|
126
|
-
logfile=$(current_logfile)
|
|
127
|
-
|
|
128
|
-
# Construct the JSON line manually (no jq dependency — pure bash string ops)
|
|
129
|
-
local json_line="{\"ts\":\"${ts}\",\"skill\":\"$(json_string "$skill")\",\"outcome\":\"${outcome}\"${duration_part}${notes_part}${session_part}}"
|
|
130
|
-
|
|
131
|
-
# appendFileSync equivalent — each append is a complete JSON line
|
|
132
|
-
echo "$json_line" >> "$logfile"
|
|
133
|
-
# Restrict log file permissions to owner-only after every write
|
|
134
|
-
chmod 600 "$logfile"
|
|
135
|
-
|
|
136
|
-
echo "Recorded: $skill → $outcome ($(basename "$logfile"))"
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# show — Print recent execution records as raw JSON lines to stdout.
|
|
140
|
-
# Supports optional skill filter (--skill) and record count limit (--limit).
|
|
141
|
-
cmd_show() {
|
|
142
|
-
local skill_filter="" limit=20
|
|
143
|
-
|
|
144
|
-
while [[ $# -gt 0 ]]; do
|
|
145
|
-
case "$1" in
|
|
146
|
-
--skill) skill_filter="$2"; shift 2 ;;
|
|
147
|
-
--limit) limit="$2"; shift 2 ;;
|
|
148
|
-
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
149
|
-
esac
|
|
150
|
-
done
|
|
151
|
-
|
|
152
|
-
ensure_dir
|
|
153
|
-
|
|
154
|
-
## --- Read All Records Into Array ---
|
|
155
|
-
# Collect all JSONL files sorted by filename (YYYY-MM.jsonl = chronological)
|
|
156
|
-
local lines=()
|
|
157
|
-
for f in "$METRICS_DIR"/*.jsonl; do
|
|
158
|
-
[[ -f "$f" ]] || continue # Skip if glob expands to literal string (no files)
|
|
159
|
-
while IFS= read -r line; do
|
|
160
|
-
[[ -z "$line" ]] && continue # Skip blank separator lines
|
|
161
|
-
if [[ -n "$skill_filter" ]]; then
|
|
162
|
-
# Grep-based filter avoids jq dependency — exact JSON field match
|
|
163
|
-
echo "$line" | grep -q "\"skill\":\"${skill_filter}\"" || continue
|
|
164
|
-
fi
|
|
165
|
-
lines+=("$line")
|
|
166
|
-
done < "$f"
|
|
167
|
-
done
|
|
168
|
-
|
|
169
|
-
## --- Output Last N Records ---
|
|
170
|
-
# Tail semantics: show the most recent `limit` records
|
|
171
|
-
local total=${#lines[@]}
|
|
172
|
-
local start=$((total - limit))
|
|
173
|
-
[[ $start -lt 0 ]] && start=0
|
|
174
|
-
|
|
175
|
-
for ((i=start; i<total; i++)); do
|
|
176
|
-
echo "${lines[$i]}"
|
|
177
|
-
done
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
# summary — Print aggregated statistics across all recorded executions.
|
|
181
|
-
# Uses awk for JSON field extraction without requiring jq or python.
|
|
182
|
-
# When no skill filter is provided, also shows a per-skill breakdown.
|
|
183
|
-
cmd_summary() {
|
|
184
|
-
local skill_filter=""
|
|
185
|
-
|
|
186
|
-
while [[ $# -gt 0 ]]; do
|
|
187
|
-
case "$1" in
|
|
188
|
-
--skill) skill_filter="$2"; shift 2 ;;
|
|
189
|
-
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
190
|
-
esac
|
|
191
|
-
done
|
|
192
|
-
|
|
193
|
-
ensure_dir
|
|
194
|
-
|
|
195
|
-
## --- Collect All Matching Records ---
|
|
196
|
-
# Accumulate all matching JSON lines into a single variable for awk processing
|
|
197
|
-
local all_lines=""
|
|
198
|
-
for f in "$METRICS_DIR"/*.jsonl; do
|
|
199
|
-
[[ -f "$f" ]] || continue
|
|
200
|
-
while IFS= read -r line; do
|
|
201
|
-
[[ -z "$line" ]] && continue
|
|
202
|
-
if [[ -n "$skill_filter" ]]; then
|
|
203
|
-
echo "$line" | grep -q "\"skill\":\"${skill_filter}\"" || continue
|
|
204
|
-
fi
|
|
205
|
-
all_lines+="$line"$'\n'
|
|
206
|
-
done < "$f"
|
|
207
|
-
done
|
|
208
|
-
|
|
209
|
-
if [[ -z "$all_lines" ]]; then
|
|
210
|
-
echo "No metrics recorded${skill_filter:+ for skill: $skill_filter}"
|
|
211
|
-
return 0
|
|
212
|
-
fi
|
|
213
|
-
|
|
214
|
-
## --- Compute and Print Stats via awk ---
|
|
215
|
-
# Pure awk JSON parsing — extracts outcome counts, duration averages, and skill breakdown
|
|
216
|
-
# without any external dependencies beyond the POSIX awk that's on every Unix system.
|
|
217
|
-
echo "$all_lines" | awk '
|
|
218
|
-
BEGIN {
|
|
219
|
-
total = 0; success = 0; failure = 0; partial = 0
|
|
220
|
-
total_duration = 0; duration_count = 0
|
|
221
|
-
split("", skill_counts)
|
|
222
|
-
}
|
|
223
|
-
# Count outcomes by matching the JSON "outcome" field value
|
|
224
|
-
/\"outcome\":\"success\"/ { success++ }
|
|
225
|
-
/\"outcome\":\"failure\"/ { failure++ }
|
|
226
|
-
/\"outcome\":\"partial\"/ { partial++ }
|
|
227
|
-
# Extract duration_s numeric value using string operations (no regex group captures in awk)
|
|
228
|
-
/\"duration_s\":/ {
|
|
229
|
-
p = index($0, "\"duration_s\":")
|
|
230
|
-
if (p > 0) {
|
|
231
|
-
rest = substr($0, p + 13)
|
|
232
|
-
val = rest + 0
|
|
233
|
-
if (val > 0 || substr(rest, 1, 1) == "0") {
|
|
234
|
-
total_duration += val
|
|
235
|
-
duration_count++
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
# Extract skill name for the per-skill breakdown table
|
|
240
|
-
/\"skill\":/ {
|
|
241
|
-
p = index($0, "\"skill\":\"")
|
|
242
|
-
if (p > 0) {
|
|
243
|
-
rest = substr($0, p + 9)
|
|
244
|
-
q = index(rest, "\"")
|
|
245
|
-
if (q > 0) skill_counts[substr(rest, 1, q - 1)]++
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
{ total++ }
|
|
249
|
-
END {
|
|
250
|
-
print "Total executions:", total
|
|
251
|
-
print " Success:", success, "(" int(success/total*100+0.5) "%)"
|
|
252
|
-
print " Failure:", failure, "(" int(failure/total*100+0.5) "%)"
|
|
253
|
-
print " Partial:", partial, "(" int(partial/total*100+0.5) "%)"
|
|
254
|
-
if (duration_count > 0) {
|
|
255
|
-
print "Avg duration:", int(total_duration/duration_count+0.5) "s"
|
|
256
|
-
}
|
|
257
|
-
# Show skill breakdown only when no skill filter was applied
|
|
258
|
-
if (!'"$([ -n "$skill_filter" ] && echo 1 || echo 0)"') {
|
|
259
|
-
print "\nSkill breakdown:"
|
|
260
|
-
for (s in skill_counts) {
|
|
261
|
-
print " " s ": " skill_counts[s]
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
'
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
## === Usage ===
|
|
269
|
-
|
|
270
|
-
cmd_usage() {
|
|
271
|
-
cat << 'EOF'
|
|
272
|
-
Usage: collector.sh <command> [options]
|
|
273
|
-
|
|
274
|
-
Commands:
|
|
275
|
-
record Record a skill execution outcome
|
|
276
|
-
show Show recent execution records
|
|
277
|
-
summary Show aggregated statistics
|
|
278
|
-
|
|
279
|
-
record options:
|
|
280
|
-
--skill <name> Skill name (required)
|
|
281
|
-
--outcome <result> success | failure | partial (required)
|
|
282
|
-
--duration <seconds> Execution time in seconds
|
|
283
|
-
--notes <text> Notes about this execution
|
|
284
|
-
--session-id <id> Session identifier
|
|
285
|
-
|
|
286
|
-
Examples:
|
|
287
|
-
collector.sh record --skill systematic-debugging --outcome success --duration 1800 \
|
|
288
|
-
--notes "payment-pool: 3 hypotheses, root cause found in git bisect"
|
|
289
|
-
|
|
290
|
-
collector.sh show --skill test-driven-development --limit 10
|
|
291
|
-
|
|
292
|
-
collector.sh summary
|
|
293
|
-
collector.sh summary --skill systematic-debugging
|
|
294
|
-
EOF
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
## === Main Dispatch ===
|
|
298
|
-
|
|
299
|
-
# Route the first positional argument to the appropriate command function.
|
|
300
|
-
# Arguments after the command name are forwarded with `shift` before each call.
|
|
301
|
-
case "${1:-}" in
|
|
302
|
-
record) shift; cmd_record "$@" ;;
|
|
303
|
-
show) shift; cmd_show "$@" ;;
|
|
304
|
-
summary) shift; cmd_summary "$@" ;;
|
|
305
|
-
help|-h|--help) cmd_usage ;;
|
|
306
|
-
"") cmd_usage; exit 1 ;;
|
|
307
|
-
*) echo "Unknown command: $1"; cmd_usage; exit 1 ;;
|
|
308
|
-
esac
|