@olegkoval/agent-skills 1.42.0 → 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.
@@ -27,7 +27,7 @@ const OUT = arg('out', './mine.json');
27
27
  const SINCE = Date.parse(`${arg('since', '')}T00:00:00Z`);
28
28
  const UNTIL = Date.parse(`${arg('until', '')}T00:00:00Z`);
29
29
 
30
- if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL) || UNTIL <= SINCE) {
30
+ if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL)) {
31
31
  console.error('usage: mine-transcripts.mjs --since YYYY-MM-DD --until YYYY-MM-DD [--out path] [--root dir]');
32
32
  process.exit(2);
33
33
  }
@@ -46,7 +46,23 @@ function walk(dir, out = []) {
46
46
  }
47
47
 
48
48
  // A user-role text record that is really harness output, not something a person typed.
49
- const HARNESS = /^(<system-reminder|<command-name|<command-message|<local-command|<bash-input|<bash-stdout|<user-prompt-submit-hook|<task-notification|Caveat: The messages below|\[Request interrupted)/;
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('|') + ')');
50
66
 
51
67
  const textOf = (msg) => {
52
68
  const c = msg?.content;
@@ -68,7 +84,8 @@ const S = {
68
84
  limits: [],
69
85
  humanTurns: [],
70
86
  echoTurns: 0,
71
- interrupts: 0,
87
+ interruptsAll: 0,
88
+ interruptsMain: 0,
72
89
  compacts: 0,
73
90
  usage: { in: 0, out: 0, cacheRead: 0, cacheWrite: 0 },
74
91
  };
@@ -134,16 +151,8 @@ for (const f of files) {
134
151
 
135
152
  if (r.type === 'assistant') {
136
153
  const model = r.message?.model;
154
+ if (model) bump(S.models, model);
137
155
  const u = r.message?.usage;
138
- if (model) {
139
- const stats = S.models.get(model) || { messages: 0, in: 0, out: 0, cacheRead: 0, cacheWrite: 0 };
140
- stats.messages++;
141
- stats.in += u?.input_tokens || 0;
142
- stats.out += u?.output_tokens || 0;
143
- stats.cacheRead += u?.cache_read_input_tokens || 0;
144
- stats.cacheWrite += u?.cache_creation_input_tokens || 0;
145
- S.models.set(model, stats);
146
- }
147
156
  if (u) {
148
157
  S.usage.in += u.input_tokens || 0;
149
158
  S.usage.out += u.output_tokens || 0;
@@ -182,9 +191,12 @@ for (const f of files) {
182
191
  }
183
192
 
184
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.
185
197
  if (raw.includes('[Request interrupted')) {
186
- S.interrupts++;
187
- if (sess) sess.interrupts++;
198
+ S.interruptsAll++;
199
+ if (sess) { S.interruptsMain++; sess.interrupts++; }
188
200
  }
189
201
  if (!main || r.userType !== 'external' || !raw || sawToolResult) continue;
190
202
 
@@ -219,13 +231,14 @@ const report = {
219
231
  humanTurnsPerDay: byDay,
220
232
  turnsPerSession: { median: pct(0.5), p90: pct(0.9), max: humanCounts.at(-1) ?? 0 },
221
233
  shortTurnsUnder40Chars: S.humanTurns.filter((t) => t.text.trim().length < 40).length,
222
- interrupts: S.interrupts,
234
+ interruptsMainSessions: S.interruptsMain,
235
+ interruptsAllRecords: S.interruptsAll,
223
236
  compactEvents: S.compacts,
224
237
  capacityLimitEvents: S.limits.length,
225
238
  capacityLimitSamples: S.limits.slice(0, 10),
226
239
  usage: S.usage,
227
240
  cacheReadToOutputRatio: S.usage.out ? +(S.usage.cacheRead / S.usage.out).toFixed(1) : null,
228
- models: [...S.models].sort((a, b) => b[1].messages - a[1].messages),
241
+ models: [...S.models].sort((a, b) => b[1] - a[1]),
229
242
  topTools: [...S.tools].sort((a, b) => b[1] - a[1]).slice(0, 40),
230
243
  agentSpawns: [...S.agents].sort((a, b) => b[1] - a[1]),
231
244
  skillInvocations: [...S.skills].sort((a, b) => b[1] - a[1]),
@@ -233,6 +246,23 @@ const report = {
233
246
  sessions,
234
247
  };
235
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
+
236
266
  fs.writeFileSync(OUT, JSON.stringify(report, null, 1));
237
267
  fs.writeFileSync(OUT.replace(/\.json$/, '') + '-turns.json', JSON.stringify(S.humanTurns, null, 1));
238
268
 
@@ -27,7 +27,7 @@ const OUT = arg('out', './mine.json');
27
27
  const SINCE = Date.parse(`${arg('since', '')}T00:00:00Z`);
28
28
  const UNTIL = Date.parse(`${arg('until', '')}T00:00:00Z`);
29
29
 
30
- if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL) || UNTIL <= SINCE) {
30
+ if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL)) {
31
31
  console.error('usage: mine-transcripts.mjs --since YYYY-MM-DD --until YYYY-MM-DD [--out path] [--root dir]');
32
32
  process.exit(2);
33
33
  }
@@ -46,7 +46,23 @@ function walk(dir, out = []) {
46
46
  }
47
47
 
48
48
  // A user-role text record that is really harness output, not something a person typed.
49
- const HARNESS = /^(<system-reminder|<command-name|<command-message|<local-command|<bash-input|<bash-stdout|<user-prompt-submit-hook|<task-notification|Caveat: The messages below|\[Request interrupted)/;
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('|') + ')');
50
66
 
51
67
  const textOf = (msg) => {
52
68
  const c = msg?.content;
@@ -68,7 +84,8 @@ const S = {
68
84
  limits: [],
69
85
  humanTurns: [],
70
86
  echoTurns: 0,
71
- interrupts: 0,
87
+ interruptsAll: 0,
88
+ interruptsMain: 0,
72
89
  compacts: 0,
73
90
  usage: { in: 0, out: 0, cacheRead: 0, cacheWrite: 0 },
74
91
  };
@@ -134,16 +151,8 @@ for (const f of files) {
134
151
 
135
152
  if (r.type === 'assistant') {
136
153
  const model = r.message?.model;
154
+ if (model) bump(S.models, model);
137
155
  const u = r.message?.usage;
138
- if (model) {
139
- const stats = S.models.get(model) || { messages: 0, in: 0, out: 0, cacheRead: 0, cacheWrite: 0 };
140
- stats.messages++;
141
- stats.in += u?.input_tokens || 0;
142
- stats.out += u?.output_tokens || 0;
143
- stats.cacheRead += u?.cache_read_input_tokens || 0;
144
- stats.cacheWrite += u?.cache_creation_input_tokens || 0;
145
- S.models.set(model, stats);
146
- }
147
156
  if (u) {
148
157
  S.usage.in += u.input_tokens || 0;
149
158
  S.usage.out += u.output_tokens || 0;
@@ -182,9 +191,12 @@ for (const f of files) {
182
191
  }
183
192
 
184
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.
185
197
  if (raw.includes('[Request interrupted')) {
186
- S.interrupts++;
187
- if (sess) sess.interrupts++;
198
+ S.interruptsAll++;
199
+ if (sess) { S.interruptsMain++; sess.interrupts++; }
188
200
  }
189
201
  if (!main || r.userType !== 'external' || !raw || sawToolResult) continue;
190
202
 
@@ -219,13 +231,14 @@ const report = {
219
231
  humanTurnsPerDay: byDay,
220
232
  turnsPerSession: { median: pct(0.5), p90: pct(0.9), max: humanCounts.at(-1) ?? 0 },
221
233
  shortTurnsUnder40Chars: S.humanTurns.filter((t) => t.text.trim().length < 40).length,
222
- interrupts: S.interrupts,
234
+ interruptsMainSessions: S.interruptsMain,
235
+ interruptsAllRecords: S.interruptsAll,
223
236
  compactEvents: S.compacts,
224
237
  capacityLimitEvents: S.limits.length,
225
238
  capacityLimitSamples: S.limits.slice(0, 10),
226
239
  usage: S.usage,
227
240
  cacheReadToOutputRatio: S.usage.out ? +(S.usage.cacheRead / S.usage.out).toFixed(1) : null,
228
- models: [...S.models].sort((a, b) => b[1].messages - a[1].messages),
241
+ models: [...S.models].sort((a, b) => b[1] - a[1]),
229
242
  topTools: [...S.tools].sort((a, b) => b[1] - a[1]).slice(0, 40),
230
243
  agentSpawns: [...S.agents].sort((a, b) => b[1] - a[1]),
231
244
  skillInvocations: [...S.skills].sort((a, b) => b[1] - a[1]),
@@ -233,6 +246,23 @@ const report = {
233
246
  sessions,
234
247
  };
235
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
+
236
266
  fs.writeFileSync(OUT, JSON.stringify(report, null, 1));
237
267
  fs.writeFileSync(OUT.replace(/\.json$/, '') + '-turns.json', JSON.stringify(S.humanTurns, null, 1));
238
268
 
@@ -27,7 +27,7 @@ const OUT = arg('out', './mine.json');
27
27
  const SINCE = Date.parse(`${arg('since', '')}T00:00:00Z`);
28
28
  const UNTIL = Date.parse(`${arg('until', '')}T00:00:00Z`);
29
29
 
30
- if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL) || UNTIL <= SINCE) {
30
+ if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL)) {
31
31
  console.error('usage: mine-transcripts.mjs --since YYYY-MM-DD --until YYYY-MM-DD [--out path] [--root dir]');
32
32
  process.exit(2);
33
33
  }
@@ -46,7 +46,23 @@ function walk(dir, out = []) {
46
46
  }
47
47
 
48
48
  // A user-role text record that is really harness output, not something a person typed.
49
- const HARNESS = /^(<system-reminder|<command-name|<command-message|<local-command|<bash-input|<bash-stdout|<user-prompt-submit-hook|<task-notification|Caveat: The messages below|\[Request interrupted)/;
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('|') + ')');
50
66
 
51
67
  const textOf = (msg) => {
52
68
  const c = msg?.content;
@@ -68,7 +84,8 @@ const S = {
68
84
  limits: [],
69
85
  humanTurns: [],
70
86
  echoTurns: 0,
71
- interrupts: 0,
87
+ interruptsAll: 0,
88
+ interruptsMain: 0,
72
89
  compacts: 0,
73
90
  usage: { in: 0, out: 0, cacheRead: 0, cacheWrite: 0 },
74
91
  };
@@ -134,16 +151,8 @@ for (const f of files) {
134
151
 
135
152
  if (r.type === 'assistant') {
136
153
  const model = r.message?.model;
154
+ if (model) bump(S.models, model);
137
155
  const u = r.message?.usage;
138
- if (model) {
139
- const stats = S.models.get(model) || { messages: 0, in: 0, out: 0, cacheRead: 0, cacheWrite: 0 };
140
- stats.messages++;
141
- stats.in += u?.input_tokens || 0;
142
- stats.out += u?.output_tokens || 0;
143
- stats.cacheRead += u?.cache_read_input_tokens || 0;
144
- stats.cacheWrite += u?.cache_creation_input_tokens || 0;
145
- S.models.set(model, stats);
146
- }
147
156
  if (u) {
148
157
  S.usage.in += u.input_tokens || 0;
149
158
  S.usage.out += u.output_tokens || 0;
@@ -182,9 +191,12 @@ for (const f of files) {
182
191
  }
183
192
 
184
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.
185
197
  if (raw.includes('[Request interrupted')) {
186
- S.interrupts++;
187
- if (sess) sess.interrupts++;
198
+ S.interruptsAll++;
199
+ if (sess) { S.interruptsMain++; sess.interrupts++; }
188
200
  }
189
201
  if (!main || r.userType !== 'external' || !raw || sawToolResult) continue;
190
202
 
@@ -219,13 +231,14 @@ const report = {
219
231
  humanTurnsPerDay: byDay,
220
232
  turnsPerSession: { median: pct(0.5), p90: pct(0.9), max: humanCounts.at(-1) ?? 0 },
221
233
  shortTurnsUnder40Chars: S.humanTurns.filter((t) => t.text.trim().length < 40).length,
222
- interrupts: S.interrupts,
234
+ interruptsMainSessions: S.interruptsMain,
235
+ interruptsAllRecords: S.interruptsAll,
223
236
  compactEvents: S.compacts,
224
237
  capacityLimitEvents: S.limits.length,
225
238
  capacityLimitSamples: S.limits.slice(0, 10),
226
239
  usage: S.usage,
227
240
  cacheReadToOutputRatio: S.usage.out ? +(S.usage.cacheRead / S.usage.out).toFixed(1) : null,
228
- models: [...S.models].sort((a, b) => b[1].messages - a[1].messages),
241
+ models: [...S.models].sort((a, b) => b[1] - a[1]),
229
242
  topTools: [...S.tools].sort((a, b) => b[1] - a[1]).slice(0, 40),
230
243
  agentSpawns: [...S.agents].sort((a, b) => b[1] - a[1]),
231
244
  skillInvocations: [...S.skills].sort((a, b) => b[1] - a[1]),
@@ -233,6 +246,23 @@ const report = {
233
246
  sessions,
234
247
  };
235
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
+
236
266
  fs.writeFileSync(OUT, JSON.stringify(report, null, 1));
237
267
  fs.writeFileSync(OUT.replace(/\.json$/, '') + '-turns.json', JSON.stringify(S.humanTurns, null, 1));
238
268
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olegkoval/agent-skills",
3
- "version": "1.42.0",
3
+ "version": "1.42.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-apple-kit",
3
3
  "description": "Build and ship Apple platform apps: macOS menubar apps, App Store submissions.",
4
- "version": "1.42.0",
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-creative",
3
3
  "description": "Creative and personal projects: photo galleries, music players, listings, wiki editing.",
4
- "version": "1.42.0",
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-garmin-kit",
3
3
  "description": "Build, test and publish Garmin Connect IQ watch faces.",
4
- "version": "1.42.0",
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-git-tools",
3
3
  "description": "Everyday git and GitHub CLI operations: conventional commits, branch hygiene.",
4
- "version": "1.42.0",
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-github-pr",
3
3
  "description": "Drive GitHub pull requests to merge-ready: review-bot loops, CI fixes, descriptions, dependency triage.",
4
- "version": "1.42.0",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -424,6 +424,10 @@ const budgetAtStart = budget.spent()
424
424
  `First Read and follow the prompt file: ${promptDir}/${dim.file}.`,
425
425
  `Parameters: REPO_SLUG=${repoSlug}, PR_NUMBER=${prNumber}, PR_URL=${prUrl},`,
426
426
  `DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
427
+ `Your StructuredOutput MUST be a JSON object with a top-level "findings" array`
428
+ + ` (it is a REQUIRED property, so do not omit it even when there is nothing to report).`
429
+ + ` If you found no issues, return {"findings": []}, never prose, never an empty object,`
430
+ + ` never a "findings" key nested under anything else.`,
427
431
  ].join(' ')
428
432
 
429
433
  if (prevSha) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-obsidian",
3
3
  "description": "Keep an Obsidian vault in sync with work: PR sync, task rollover, morning routine.",
4
- "version": "1.42.0",
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-product",
3
3
  "description": "Take a product idea to a shippable build: MVP passes, full-stack scaffolds, launch plans.",
4
- "version": "1.42.0",
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-reflection",
3
3
  "description": "Look back and improve: self-critique, retrospectives, performance review, rapid learning.",
4
- "version": "1.42.0",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -27,7 +27,7 @@ const OUT = arg('out', './mine.json');
27
27
  const SINCE = Date.parse(`${arg('since', '')}T00:00:00Z`);
28
28
  const UNTIL = Date.parse(`${arg('until', '')}T00:00:00Z`);
29
29
 
30
- if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL) || UNTIL <= SINCE) {
30
+ if (!Number.isFinite(SINCE) || !Number.isFinite(UNTIL)) {
31
31
  console.error('usage: mine-transcripts.mjs --since YYYY-MM-DD --until YYYY-MM-DD [--out path] [--root dir]');
32
32
  process.exit(2);
33
33
  }
@@ -46,7 +46,23 @@ function walk(dir, out = []) {
46
46
  }
47
47
 
48
48
  // A user-role text record that is really harness output, not something a person typed.
49
- const HARNESS = /^(<system-reminder|<command-name|<command-message|<local-command|<bash-input|<bash-stdout|<user-prompt-submit-hook|<task-notification|Caveat: The messages below|\[Request interrupted)/;
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('|') + ')');
50
66
 
51
67
  const textOf = (msg) => {
52
68
  const c = msg?.content;
@@ -68,7 +84,8 @@ const S = {
68
84
  limits: [],
69
85
  humanTurns: [],
70
86
  echoTurns: 0,
71
- interrupts: 0,
87
+ interruptsAll: 0,
88
+ interruptsMain: 0,
72
89
  compacts: 0,
73
90
  usage: { in: 0, out: 0, cacheRead: 0, cacheWrite: 0 },
74
91
  };
@@ -134,16 +151,8 @@ for (const f of files) {
134
151
 
135
152
  if (r.type === 'assistant') {
136
153
  const model = r.message?.model;
154
+ if (model) bump(S.models, model);
137
155
  const u = r.message?.usage;
138
- if (model) {
139
- const stats = S.models.get(model) || { messages: 0, in: 0, out: 0, cacheRead: 0, cacheWrite: 0 };
140
- stats.messages++;
141
- stats.in += u?.input_tokens || 0;
142
- stats.out += u?.output_tokens || 0;
143
- stats.cacheRead += u?.cache_read_input_tokens || 0;
144
- stats.cacheWrite += u?.cache_creation_input_tokens || 0;
145
- S.models.set(model, stats);
146
- }
147
156
  if (u) {
148
157
  S.usage.in += u.input_tokens || 0;
149
158
  S.usage.out += u.output_tokens || 0;
@@ -182,9 +191,12 @@ for (const f of files) {
182
191
  }
183
192
 
184
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.
185
197
  if (raw.includes('[Request interrupted')) {
186
- S.interrupts++;
187
- if (sess) sess.interrupts++;
198
+ S.interruptsAll++;
199
+ if (sess) { S.interruptsMain++; sess.interrupts++; }
188
200
  }
189
201
  if (!main || r.userType !== 'external' || !raw || sawToolResult) continue;
190
202
 
@@ -219,13 +231,14 @@ const report = {
219
231
  humanTurnsPerDay: byDay,
220
232
  turnsPerSession: { median: pct(0.5), p90: pct(0.9), max: humanCounts.at(-1) ?? 0 },
221
233
  shortTurnsUnder40Chars: S.humanTurns.filter((t) => t.text.trim().length < 40).length,
222
- interrupts: S.interrupts,
234
+ interruptsMainSessions: S.interruptsMain,
235
+ interruptsAllRecords: S.interruptsAll,
223
236
  compactEvents: S.compacts,
224
237
  capacityLimitEvents: S.limits.length,
225
238
  capacityLimitSamples: S.limits.slice(0, 10),
226
239
  usage: S.usage,
227
240
  cacheReadToOutputRatio: S.usage.out ? +(S.usage.cacheRead / S.usage.out).toFixed(1) : null,
228
- models: [...S.models].sort((a, b) => b[1].messages - a[1].messages),
241
+ models: [...S.models].sort((a, b) => b[1] - a[1]),
229
242
  topTools: [...S.tools].sort((a, b) => b[1] - a[1]).slice(0, 40),
230
243
  agentSpawns: [...S.agents].sort((a, b) => b[1] - a[1]),
231
244
  skillInvocations: [...S.skills].sort((a, b) => b[1] - a[1]),
@@ -233,6 +246,23 @@ const report = {
233
246
  sessions,
234
247
  };
235
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
+
236
266
  fs.writeFileSync(OUT, JSON.stringify(report, null, 1));
237
267
  fs.writeFileSync(OUT.replace(/\.json$/, '') + '-turns.json', JSON.stringify(S.humanTurns, null, 1));
238
268
 
@@ -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.42.0",
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.42.0",
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.42.0",
4
+ "version": "1.42.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },