@olegkoval/agent-skills 1.41.4 → 1.42.1

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 (29) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/.github/prompts/agent-ops-retro.prompt.md +178 -0
  3. package/.kiro/steering/agent-ops-retro.md +177 -0
  4. package/.windsurf/rules/agent-ops-retro.md +176 -0
  5. package/README.md +3 -2
  6. package/adapters/claude/olko-reflection/skills/agent-ops-retro/SKILL.md +199 -0
  7. package/adapters/claude/olko-reflection/skills/agent-ops-retro/scripts/mine-transcripts.mjs +275 -0
  8. package/adapters/codex/olko-reflection/README.md +1 -0
  9. package/adapters/cursor/olko-reflection/skills/agent-ops-retro/SKILL.md +199 -0
  10. package/adapters/cursor/olko-reflection/skills/agent-ops-retro/scripts/mine-transcripts.mjs +275 -0
  11. package/adapters/grok/olko-reflection/skills/agent-ops-retro/SKILL.md +199 -0
  12. package/adapters/grok/olko-reflection/skills/agent-ops-retro/scripts/mine-transcripts.mjs +275 -0
  13. package/catalog/skills.json +24 -0
  14. package/package.json +1 -1
  15. package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
  16. package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
  17. package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
  18. package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
  19. package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
  20. package/plugins/olko-github-pr/skills/lekker-review/workflow.js +4 -0
  21. package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
  22. package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
  23. package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
  24. package/plugins/olko-reflection/skills/agent-ops-retro/SKILL.md +198 -0
  25. package/plugins/olko-reflection/skills/agent-ops-retro/scripts/mine-transcripts.mjs +275 -0
  26. package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
  27. package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
  28. package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
  29. package/scripts/lib/catalog.mjs +1 -1
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ // mine-transcripts: extract operator-axis metrics from local Claude Code transcripts.
3
+ //
4
+ // Ships beside agent-ops-retro because a rule without its runnable half gets re-derived.
5
+ // Every filter here exists because a previous run got it wrong:
6
+ // - records are selected by their own .timestamp, never by file mtime (a resumed session
7
+ // rewrites old files and drags pre-window turns into the window)
8
+ // - a `user` record is not a human turn; tool_results, system-reminders, command preambles,
9
+ // task notifications and bash echoes are stripped, and they were 45% of the raw count
10
+ // - sidechain and subagent records are counted separately, never folded into session counts
11
+ //
12
+ // node mine-transcripts.mjs --since 2026-08-18 --until 2026-09-02 --out ./mine.json
13
+ //
14
+ // Writes <out> and <out minus .json>-turns.json. Prints a summary to stdout.
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import readline from 'node:readline';
19
+
20
+ const arg = (k, d) => {
21
+ const i = process.argv.indexOf(`--${k}`);
22
+ return i > -1 && process.argv[i + 1] ? process.argv[i + 1] : d;
23
+ };
24
+
25
+ const ROOT = arg('root', path.join(process.env.HOME, '.claude', 'projects'));
26
+ const OUT = arg('out', './mine.json');
27
+ const SINCE = Date.parse(`${arg('since', '')}T00:00:00Z`);
28
+ const UNTIL = Date.parse(`${arg('until', '')}T00:00:00Z`);
29
+
30
+ if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL)) {
31
+ console.error('usage: mine-transcripts.mjs --since YYYY-MM-DD --until YYYY-MM-DD [--out path] [--root dir]');
32
+ process.exit(2);
33
+ }
34
+ if (!fs.existsSync(ROOT)) {
35
+ console.error(`transcript root not found: ${ROOT}. This is a state, not a success: stop here.`);
36
+ process.exit(1);
37
+ }
38
+
39
+ function walk(dir, out = []) {
40
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
41
+ const p = path.join(dir, e.name);
42
+ if (e.isDirectory()) walk(p, out);
43
+ else if (e.name.endsWith('.jsonl')) out.push(p);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ // A user-role text record that is really harness output, not something a person typed.
49
+ // A user-role text record that is really harness output, not something a person typed.
50
+ // Each alternative below was found by a run that mis-counted. The first pass caught the
51
+ // obvious wrappers and still let 26% through: skill body dumps, image attachments,
52
+ // already-loaded stubs and permission-grant echoes all sit in human-turn position.
53
+ const HARNESS = new RegExp('^(' + [
54
+ '<system-reminder', '<command-name', '<command-message', '<local-command',
55
+ '<bash-input', '<bash-stdout', '<bash-stderr', '<user-prompt-submit-hook',
56
+ '<task-notification', '<cross-session-message',
57
+ 'Caveat: The messages below',
58
+ '\\[Request interrupted',
59
+ 'Base directory for this skill:', // a skill body dumped into the turn
60
+ 'Skill /[^ ]+ is already loaded', // re-invocation stub
61
+ 'Permission granted for:', // permission-grant echo
62
+ '\\[Image #\\d+\\]', // bare attachment
63
+ '\\[Image: source:',
64
+ '\\[Pasted text',
65
+ ].join('|') + ')');
66
+
67
+ const textOf = (msg) => {
68
+ const c = msg?.content;
69
+ if (typeof c === 'string') return c;
70
+ if (Array.isArray(c)) return c.filter((b) => b.type === 'text').map((b) => b.text).join('\n');
71
+ return '';
72
+ };
73
+
74
+ const bump = (m, k, n = 1) => m.set(k, (m.get(k) || 0) + n);
75
+
76
+ const S = {
77
+ sessions: new Map(),
78
+ tools: new Map(),
79
+ agents: new Map(),
80
+ skills: new Map(),
81
+ models: new Map(),
82
+ errorsByTool: new Map(),
83
+ errorKinds: new Map(),
84
+ limits: [],
85
+ humanTurns: [],
86
+ echoTurns: 0,
87
+ interruptsAll: 0,
88
+ interruptsMain: 0,
89
+ compacts: 0,
90
+ usage: { in: 0, out: 0, cacheRead: 0, cacheWrite: 0 },
91
+ };
92
+
93
+ function classifyError(text) {
94
+ const t = text.slice(0, 400);
95
+ if (/haven't granted it yet|Permission to use|requires approval/i.test(t)) return 'permission-not-granted';
96
+ if (/auto mode classifier/i.test(t)) return 'auto-mode-classifier';
97
+ if (/isolated in the worktree|stays inside worktree/i.test(t)) return 'worktree-isolation';
98
+ if (/Delegation rule/i.test(t)) return 'delegation-rule';
99
+ if (/does not match required schema/i.test(t)) return 'agent-schema-miss';
100
+ if (/File does not exist|no such file/i.test(t)) return 'file-not-found';
101
+ if (/has not been read yet/i.test(t)) return 'read-before-write';
102
+ if (/String to replace not found|not found in file/i.test(t)) return 'edit-string-not-found';
103
+ if (/timed out|timeout/i.test(t)) return 'timeout';
104
+ if (/\b(40[0-9]|41[0-9]|429)\b.*error|HTTP 4\d\d/i.test(t)) return 'api-4xx';
105
+ if (/\b(50[0-9]|529)\b.*error|HTTP 5\d\d/i.test(t)) return 'api-5xx';
106
+ if (/command not found/i.test(t)) return 'command-not-found';
107
+ if (/Exit code [1-9]/i.test(t)) return 'non-zero-exit';
108
+ return 'other';
109
+ }
110
+
111
+ const files = walk(ROOT);
112
+ let inWindow = 0;
113
+
114
+ for (const f of files) {
115
+ const isSub = f.includes(`${path.sep}subagents${path.sep}`);
116
+ const project = path.relative(ROOT, f).split(path.sep)[0];
117
+ const rl = readline.createInterface({ input: fs.createReadStream(f), crlfDelay: Infinity });
118
+
119
+ for await (const line of rl) {
120
+ if (!line.startsWith('{')) continue;
121
+ let r;
122
+ try { r = JSON.parse(line); } catch { continue; }
123
+
124
+ const ts = r.timestamp ? Date.parse(r.timestamp) : NaN;
125
+ if (!Number.isFinite(ts) || ts < SINCE || ts >= UNTIL) continue;
126
+ inWindow++;
127
+
128
+ const main = !isSub && !r.isSidechain;
129
+ const sid = r.sessionId || path.basename(f, '.jsonl');
130
+ let sess = null;
131
+ if (main) {
132
+ if (!S.sessions.has(sid)) {
133
+ S.sessions.set(sid, { project, first: ts, last: ts, human: 0, assistant: 0, tools: 0, agents: 0, errors: 0, interrupts: 0, peakContext: 0 });
134
+ }
135
+ sess = S.sessions.get(sid);
136
+ sess.first = Math.min(sess.first, ts);
137
+ sess.last = Math.max(sess.last, ts);
138
+ }
139
+
140
+ if (r.isCompactSummary || r.subtype === 'compact_boundary') S.compacts++;
141
+
142
+ // Capacity and API-failure notices are synthetic records and are NOT always type "assistant",
143
+ // so this check sits outside the assistant branch. Gating it on the type reported zero for a
144
+ // window that really held five weekly-limit hits.
145
+ if (r.message?.model === '<synthetic>') {
146
+ const t = textOf(r.message);
147
+ if (/weekly limit|spend limit|rate limit|usage limit/i.test(t)) {
148
+ S.limits.push({ ts: r.timestamp, text: t.slice(0, 90) });
149
+ }
150
+ }
151
+
152
+ if (r.type === 'assistant') {
153
+ const model = r.message?.model;
154
+ if (model) bump(S.models, model);
155
+ const u = r.message?.usage;
156
+ if (u) {
157
+ S.usage.in += u.input_tokens || 0;
158
+ S.usage.out += u.output_tokens || 0;
159
+ S.usage.cacheRead += u.cache_read_input_tokens || 0;
160
+ S.usage.cacheWrite += u.cache_creation_input_tokens || 0;
161
+ if (sess) {
162
+ const ctx = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
163
+ sess.peakContext = Math.max(sess.peakContext, ctx);
164
+ }
165
+ }
166
+ if (sess) sess.assistant++;
167
+ for (const b of Array.isArray(r.message?.content) ? r.message.content : []) {
168
+ if (b.type !== 'tool_use') continue;
169
+ bump(S.tools, b.name);
170
+ if (sess) sess.tools++;
171
+ if (b.name === 'Agent') {
172
+ bump(S.agents, `${b.input?.subagent_type || '(none)'} | ${b.input?.model || '(inherit)'}`);
173
+ if (sess) sess.agents++;
174
+ }
175
+ if (b.name === 'Skill') bump(S.skills, b.input?.skill || '(unnamed)');
176
+ }
177
+ }
178
+
179
+ if (r.type !== 'user') continue;
180
+
181
+ const blocks = Array.isArray(r.message?.content) ? r.message.content : [];
182
+ let sawToolResult = false;
183
+ for (const b of blocks) {
184
+ if (b.type !== 'tool_result') continue;
185
+ sawToolResult = true;
186
+ if (!b.is_error) continue;
187
+ const body = typeof b.content === 'string' ? b.content : JSON.stringify(b.content || '');
188
+ bump(S.errorKinds, classifyError(body));
189
+ bump(S.errorsByTool, b.tool_use_id ? 'attributed' : 'unattributed');
190
+ if (sess) sess.errors++;
191
+ }
192
+
193
+ const raw = textOf(r.message);
194
+ // Two scopes, two names. The first version reported the all-records count and the
195
+ // per-session count under one label `interrupts`, so the top-level figure (27) did not
196
+ // match the sum of the per-session rows (15) and neither number could be trusted.
197
+ if (raw.includes('[Request interrupted')) {
198
+ S.interruptsAll++;
199
+ if (sess) { S.interruptsMain++; sess.interrupts++; }
200
+ }
201
+ if (!main || r.userType !== 'external' || !raw || sawToolResult) continue;
202
+
203
+ const clean = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
204
+ if (!clean) continue;
205
+ if (HARNESS.test(clean)) { S.echoTurns++; continue; }
206
+
207
+ if (sess) sess.human++;
208
+ S.humanTurns.push({ ts: r.timestamp, session: sid, project, text: clean.slice(0, 600) });
209
+ }
210
+ }
211
+
212
+ const sessions = [...S.sessions].map(([id, v]) => ({
213
+ id: id.slice(0, 8), ...v, minutes: Math.round((v.last - v.first) / 60000),
214
+ })).sort((a, b) => b.human - a.human);
215
+
216
+ const humanCounts = sessions.map((s) => s.human).filter((n) => n > 0).sort((a, b) => a - b);
217
+ const pct = (p) => humanCounts[Math.floor((humanCounts.length - 1) * p)] ?? 0;
218
+ const byDay = {};
219
+ for (const t of S.humanTurns) {
220
+ const d = t.ts.slice(0, 10);
221
+ byDay[d] = (byDay[d] || 0) + 1;
222
+ }
223
+
224
+ const report = {
225
+ window: { since: new Date(SINCE).toISOString(), until: new Date(UNTIL).toISOString() },
226
+ filesScanned: files.length,
227
+ recordsInWindow: inWindow,
228
+ mainSessions: S.sessions.size,
229
+ humanTurnsOrganic: S.humanTurns.length,
230
+ humanTurnsEchoesExcluded: S.echoTurns,
231
+ humanTurnsPerDay: byDay,
232
+ turnsPerSession: { median: pct(0.5), p90: pct(0.9), max: humanCounts.at(-1) ?? 0 },
233
+ shortTurnsUnder40Chars: S.humanTurns.filter((t) => t.text.trim().length < 40).length,
234
+ interruptsMainSessions: S.interruptsMain,
235
+ interruptsAllRecords: S.interruptsAll,
236
+ compactEvents: S.compacts,
237
+ capacityLimitEvents: S.limits.length,
238
+ capacityLimitSamples: S.limits.slice(0, 10),
239
+ usage: S.usage,
240
+ cacheReadToOutputRatio: S.usage.out ? +(S.usage.cacheRead / S.usage.out).toFixed(1) : null,
241
+ models: [...S.models].sort((a, b) => b[1] - a[1]),
242
+ topTools: [...S.tools].sort((a, b) => b[1] - a[1]).slice(0, 40),
243
+ agentSpawns: [...S.agents].sort((a, b) => b[1] - a[1]),
244
+ skillInvocations: [...S.skills].sort((a, b) => b[1] - a[1]),
245
+ errorKinds: [...S.errorKinds].sort((a, b) => b[1] - a[1]),
246
+ sessions,
247
+ };
248
+
249
+ // Internal consistency gate. A report whose aggregates disagree with its own rows is not
250
+ // evidence, and a previous version shipped exactly that. Fail loudly rather than emit it.
251
+ const rowInterrupts = sessions.reduce((n, s) => n + s.interrupts, 0);
252
+ const rowHuman = sessions.reduce((n, s) => n + s.human, 0);
253
+ const problems = [];
254
+ if (rowInterrupts !== report.interruptsMainSessions) {
255
+ problems.push(`interruptsMainSessions ${report.interruptsMainSessions} != sum of session rows ${rowInterrupts}`);
256
+ }
257
+ if (rowHuman !== report.humanTurnsOrganic) {
258
+ problems.push(`humanTurnsOrganic ${report.humanTurnsOrganic} != sum of session rows ${rowHuman}`);
259
+ }
260
+ if (problems.length) {
261
+ console.error('INCONSISTENT REPORT, refusing to write:\n ' + problems.join('\n '));
262
+ process.exit(3);
263
+ }
264
+ report.echoFilterVersion = 2;
265
+
266
+ fs.writeFileSync(OUT, JSON.stringify(report, null, 1));
267
+ fs.writeFileSync(OUT.replace(/\.json$/, '') + '-turns.json', JSON.stringify(S.humanTurns, null, 1));
268
+
269
+ console.log(JSON.stringify({
270
+ ...report,
271
+ capacityLimitSamples: report.capacityLimitSamples.length,
272
+ sessions: report.sessions.length,
273
+ }, null, 1));
274
+ console.error(`\nwrote ${OUT} and ${OUT.replace(/\.json$/, '')}-turns.json`);
275
+ console.error(`organic human turns ${report.humanTurnsOrganic}, harness echoes excluded ${report.humanTurnsEchoesExcluded}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-release",
3
3
  "description": "Ship a release: semantic-release setup, changelogs, store listing copy, release-day routine.",
4
- "version": "1.41.4",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-skill-meta",
3
3
  "description": "Author and maintain agent skills and the AI toolchain itself.",
4
- "version": "1.41.4",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-web-ops",
3
3
  "description": "Operate a website: WAF rules, search console audits, analytics bootstrap, docs indexes.",
4
- "version": "1.41.4",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -29,7 +29,7 @@ export const PLUGIN_ASSIGNMENT = {
29
29
  },
30
30
  'olko-reflection': {
31
31
  description: 'Look back and improve: self-critique, retrospectives, performance review, rapid learning.',
32
- skills: ['self-critique', 'review-past-performance', 'retro-analysis', 'crash-course', 'wrap-up'],
32
+ skills: ['self-critique', 'review-past-performance', 'retro-analysis', 'agent-ops-retro', 'crash-course', 'wrap-up'],
33
33
  },
34
34
  'olko-obsidian': {
35
35
  description: 'Keep an Obsidian vault in sync with work: PR sync, task rollover, morning routine.',