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,172 @@
1
+ #!/usr/bin/env node
2
+ // runtime/init.js — Initialize the ClawPowers runtime directory structure
3
+ //
4
+ // Creates ~/.clawpowers/ with all required subdirectories on first run.
5
+ // Safe to run multiple times (idempotent).
6
+ //
7
+ // Usage:
8
+ // node runtime/init.js
9
+ // npx clawpowers init
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+
16
+ const VERSION = '1.0.0';
17
+
18
+ // Runtime root — override with CLAWPOWERS_DIR env var for testing or custom locations
19
+ const CLAWPOWERS_DIR = process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers');
20
+
21
+ /**
22
+ * Creates the full runtime directory tree under CLAWPOWERS_DIR.
23
+ * Each directory is created with mode 0o700 (owner-only access) so
24
+ * skill state and metrics aren't readable by other users on the system.
25
+ * Directories that already exist are silently skipped.
26
+ *
27
+ * @returns {number} Count of directories actually created (0 if already initialized).
28
+ */
29
+ function createStructure() {
30
+ const dirs = [
31
+ CLAWPOWERS_DIR,
32
+ path.join(CLAWPOWERS_DIR, 'state'), // Key-value persistence files
33
+ path.join(CLAWPOWERS_DIR, 'metrics'), // JSONL outcome logs per month
34
+ path.join(CLAWPOWERS_DIR, 'checkpoints'), // Resumable plan state (executing-plans skill)
35
+ path.join(CLAWPOWERS_DIR, 'feedback'), // RSI analysis reports
36
+ path.join(CLAWPOWERS_DIR, 'memory'), // Cross-session knowledge base
37
+ path.join(CLAWPOWERS_DIR, 'logs'), // Debug and audit logs
38
+ ];
39
+
40
+ let created = 0;
41
+ for (const dir of dirs) {
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
44
+ created++;
45
+ }
46
+ }
47
+ return created;
48
+ }
49
+
50
+ /**
51
+ * Writes a .version file to CLAWPOWERS_DIR on first initialization.
52
+ * The file contains the ClawPowers version and an ISO timestamp so we can
53
+ * track when the runtime was first set up and run migrations in the future.
54
+ * No-op if the file already exists.
55
+ */
56
+ function writeVersion() {
57
+ const versionFile = path.join(CLAWPOWERS_DIR, '.version');
58
+ if (!fs.existsSync(versionFile)) {
59
+ const ts = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
60
+ const content = `version=${VERSION}\ninitialized=${ts}\n`;
61
+ // 0o600 = owner read/write only — this file may contain version metadata
62
+ fs.writeFileSync(versionFile, content, { mode: 0o600 });
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Writes a human-readable README into CLAWPOWERS_DIR explaining its purpose.
68
+ * Helps users who discover the directory understand what it is and that it is
69
+ * safe to delete (ClawPowers will recreate it on next run).
70
+ * No-op if the README already exists.
71
+ */
72
+ function writeReadme() {
73
+ const readme = path.join(CLAWPOWERS_DIR, 'README');
74
+ if (!fs.existsSync(readme)) {
75
+ const content = [
76
+ 'ClawPowers Runtime Directory',
77
+ '============================',
78
+ '',
79
+ 'This directory is managed by ClawPowers (https://github.com/up2itnow0822/clawpowers).',
80
+ '',
81
+ 'Directory Structure:',
82
+ ' state/ Key-value state store for skill data (managed by persistence/store.js)',
83
+ ' metrics/ Skill execution outcome logs in JSONL format',
84
+ ' checkpoints/ Resumable workflow state (created by executing-plans skill)',
85
+ ' feedback/ RSI analysis output and recommendations',
86
+ ' memory/ Cross-session knowledge base',
87
+ ' logs/ Debug and audit logs',
88
+ '',
89
+ 'Safe to delete: Yes — ClawPowers recreates this directory on next init.',
90
+ 'Never share: Contains agent state and potentially sensitive workflow data.',
91
+ '',
92
+ 'Manage with: npx clawpowers status',
93
+ '',
94
+ ].join('\n');
95
+ fs.writeFileSync(readme, content, { mode: 0o600 });
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Updates the version stamp in .version after initialization.
101
+ * Currently a no-op placeholder for actual schema migrations; the version
102
+ * string is updated in place so future versions can detect and migrate old
103
+ * runtime layouts.
104
+ */
105
+ function runMigrations() {
106
+ const versionFile = path.join(CLAWPOWERS_DIR, '.version');
107
+ if (!fs.existsSync(versionFile)) return;
108
+
109
+ // Replace the version= line with the current version to keep .version current
110
+ let content = fs.readFileSync(versionFile, 'utf8');
111
+ content = content.replace(/^version=.*/m, `version=${VERSION}`);
112
+ fs.writeFileSync(versionFile, content, { mode: 0o600 });
113
+ }
114
+
115
+ /**
116
+ * Reads the stored version string from .version.
117
+ * Used in the "already initialized" status message to show what version is
118
+ * currently installed in the runtime directory.
119
+ *
120
+ * @returns {string} Stored version string, or the current VERSION if unreadable.
121
+ */
122
+ function getStoredVersion() {
123
+ const versionFile = path.join(CLAWPOWERS_DIR, '.version');
124
+ if (!fs.existsSync(versionFile)) return VERSION;
125
+ const content = fs.readFileSync(versionFile, 'utf8');
126
+ const match = content.match(/^version=(.+)$/m);
127
+ return match ? match[1].trim() : VERSION;
128
+ }
129
+
130
+ /**
131
+ * Main initialization sequence:
132
+ * 1. Create directory structure (idempotent).
133
+ * 2. Write .version file (first run only).
134
+ * 3. Write README (first run only).
135
+ * 4. Run migrations to update version stamp.
136
+ * 5. Print status to stdout (suppressed when CLAWPOWERS_QUIET=1).
137
+ */
138
+ function main() {
139
+ const created = createStructure();
140
+ writeVersion();
141
+ writeReadme();
142
+
143
+ // Only run migrations when .version exists (i.e., after writeVersion)
144
+ if (fs.existsSync(path.join(CLAWPOWERS_DIR, '.version'))) {
145
+ runMigrations();
146
+ }
147
+
148
+ // CLAWPOWERS_QUIET=1 suppresses output when called from session-start hook
149
+ // so the hook's JSON output isn't polluted with init messages
150
+ if (process.env.CLAWPOWERS_QUIET !== '1') {
151
+ if (created > 0) {
152
+ console.log(`ClawPowers runtime initialized at ${CLAWPOWERS_DIR}`);
153
+ console.log(` Directories created: ${created}`);
154
+ console.log(` Version: ${VERSION}`);
155
+ } else {
156
+ console.log(`ClawPowers runtime already initialized at ${CLAWPOWERS_DIR}`);
157
+ console.log(` Version: ${getStoredVersion()}`);
158
+ }
159
+ }
160
+ }
161
+
162
+ // Only run main() when executed directly; allow require() without side effects
163
+ if (require.main === module) {
164
+ try {
165
+ main();
166
+ } catch (err) {
167
+ process.stderr.write(`Error: ${err.message}\n`);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ module.exports = { main, CLAWPOWERS_DIR, VERSION };
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # runtime/init.sh — Initialize the ClawPowers runtime directory structure
3
+ #
4
+ # Creates ~/.clawpowers/ with all required subdirectories on first run.
5
+ # Safe to run multiple times (idempotent).
6
+ #
7
+ # Usage:
8
+ # bash runtime/init.sh
9
+ # npx clawpowers init
10
+ set -euo pipefail
11
+
12
+ # Runtime root — override with CLAWPOWERS_DIR env var for testing or custom locations
13
+ CLAWPOWERS_DIR="${CLAWPOWERS_DIR:-$HOME/.clawpowers}"
14
+ VERSION="1.0.0"
15
+
16
+ ## === Directory Setup ===
17
+
18
+ # Creates all required runtime subdirectories.
19
+ # Each directory is created with mode 700 (owner-only) so skill state
20
+ # and metrics aren't readable by other users on the system.
21
+ # Prints the count of newly created directories (0 if already initialized).
22
+ create_structure() {
23
+ local dirs=(
24
+ "$CLAWPOWERS_DIR" # Root runtime directory
25
+ "$CLAWPOWERS_DIR/state" # Key-value persistence (store.sh / store.js)
26
+ "$CLAWPOWERS_DIR/metrics" # Skill outcome JSONL logs, rotated monthly
27
+ "$CLAWPOWERS_DIR/checkpoints" # Resumable plan state (executing-plans skill)
28
+ "$CLAWPOWERS_DIR/feedback" # RSI analysis reports (analyze.sh / analyze.js)
29
+ "$CLAWPOWERS_DIR/memory" # Cross-session knowledge base
30
+ "$CLAWPOWERS_DIR/logs" # Debug and audit logs
31
+ )
32
+
33
+ local created=0
34
+ for dir in "${dirs[@]}"; do
35
+ if [[ ! -d "$dir" ]]; then
36
+ mkdir -p "$dir"
37
+ chmod 700 "$dir"
38
+ # Bash arithmetic in set -e contexts requires || true to suppress exit on 0-return
39
+ ((created++)) || true
40
+ fi
41
+ done
42
+
43
+ echo "$created"
44
+ }
45
+
46
+ ## === Version File ===
47
+
48
+ # Writes .version on first initialization. Contains the ClawPowers version and
49
+ # an ISO timestamp so we can track install date and run future migrations.
50
+ # No-op if the file already exists.
51
+ write_version() {
52
+ local version_file="$CLAWPOWERS_DIR/.version"
53
+ if [[ ! -f "$version_file" ]]; then
54
+ cat > "$version_file" << EOF
55
+ version=$VERSION
56
+ initialized=$(date -u +%Y-%m-%dT%H:%M:%SZ)
57
+ EOF
58
+ # 0o600 = owner read/write only
59
+ chmod 600 "$version_file"
60
+ fi
61
+ }
62
+
63
+ ## === README ===
64
+
65
+ # Writes a human-readable README into CLAWPOWERS_DIR explaining its purpose.
66
+ # Helps users who discover the directory understand what it is and that it
67
+ # is safe to delete (ClawPowers recreates it on next run).
68
+ # No-op if the README already exists.
69
+ write_readme() {
70
+ local readme="$CLAWPOWERS_DIR/README"
71
+ if [[ ! -f "$readme" ]]; then
72
+ # Single-quoted heredoc prevents variable expansion inside the README
73
+ cat > "$readme" << 'EOF'
74
+ ClawPowers Runtime Directory
75
+ ============================
76
+
77
+ This directory is managed by ClawPowers (https://github.com/up2itnow0822/clawpowers).
78
+
79
+ Directory Structure:
80
+ state/ Key-value state store for skill data (managed by persistence/store.sh)
81
+ metrics/ Skill execution outcome logs in JSONL format
82
+ checkpoints/ Resumable workflow state (created by executing-plans skill)
83
+ feedback/ RSI analysis output and recommendations
84
+ memory/ Cross-session knowledge base
85
+ logs/ Debug and audit logs
86
+
87
+ Safe to delete: Yes — ClawPowers recreates this directory on next init.
88
+ Never share: Contains agent state and potentially sensitive workflow data.
89
+
90
+ Manage with: npx clawpowers status
91
+ EOF
92
+ chmod 600 "$readme"
93
+ fi
94
+ }
95
+
96
+ ## === Migrations ===
97
+
98
+ # Updates the version stamp in .version to the current version.
99
+ # Placeholder for future schema migrations (e.g., restructuring state/ layout).
100
+ # The sed command replaces the version= line in place; .bak is cleaned up immediately.
101
+ run_migrations() {
102
+ local current_version
103
+ current_version=$(grep "^version=" "$CLAWPOWERS_DIR/.version" 2>/dev/null | cut -d= -f2 || echo "0.0.0")
104
+
105
+ # Future migration hooks go here, e.g.:
106
+ # if [[ "$current_version" < "2.0.0" ]]; then
107
+ # migrate_v1_to_v2
108
+ # fi
109
+
110
+ # Always update the version stamp to reflect the currently running version
111
+ sed -i.bak "s/^version=.*/version=$VERSION/" "$CLAWPOWERS_DIR/.version" 2>/dev/null || true
112
+ rm -f "$CLAWPOWERS_DIR/.version.bak"
113
+ }
114
+
115
+ ## === Main ===
116
+
117
+ main() {
118
+ local created
119
+ created=$(create_structure)
120
+
121
+ write_version
122
+ write_readme
123
+
124
+ # Migrations only apply when the version file exists (guaranteed after write_version)
125
+ if [[ -f "$CLAWPOWERS_DIR/.version" ]]; then
126
+ run_migrations
127
+ fi
128
+
129
+ # CLAWPOWERS_QUIET=1 suppresses all output when called from a hook or
130
+ # another script that needs clean stdout (e.g., session-start emitting JSON)
131
+ if [[ "${CLAWPOWERS_QUIET:-}" != "1" ]]; then
132
+ if [[ $created -gt 0 ]]; then
133
+ echo "ClawPowers runtime initialized at $CLAWPOWERS_DIR"
134
+ echo " Directories created: $created"
135
+ echo " Version: $VERSION"
136
+ else
137
+ echo "ClawPowers runtime already initialized at $CLAWPOWERS_DIR"
138
+ local stored_version
139
+ stored_version=$(grep "^version=" "$CLAWPOWERS_DIR/.version" | cut -d= -f2)
140
+ echo " Version: $stored_version"
141
+ fi
142
+ fi
143
+ }
144
+
145
+ main "$@"
@@ -0,0 +1,361 @@
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 };