@assuredloop/orca-bot-kit 0.18.0 → 0.20.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/README.md +22 -3
- package/SETUP.md +5 -5
- package/package.json +3 -2
- package/skills/obk-arch/NOTICE.md +45 -0
- package/skills/obk-bot-building/NOTICE.md +5 -0
- package/skills/obk-bot-building/SKILL.md +13 -5
- package/skills/obk-debugging/NOTICE.md +28 -0
- package/skills/obk-decision-memo/NOTICE.md +21 -0
- package/skills/obk-fleet-review/NOTICE.md +8 -0
- package/skills/obk-grilling/NOTICE.md +11 -5
- package/skills/obk-grooming/NOTICE.md +11 -0
- package/skills/obk-grooming/SKILL.md +10 -0
- package/skills/obk-handoff/NOTICE.md +6 -0
- package/skills/obk-personal-facilitation/NOTICE.md +9 -0
- package/skills/obk-recall/NOTICE.md +3 -0
- package/skills/obk-researching/NOTICE.md +27 -6
- package/skills/obk-reviewing/NOTICE.md +19 -0
- package/skills/obk-tdd/NOTICE.md +35 -4
- package/skills/obk-teach/NOTICE.md +3 -0
- package/skills/obk-why/NOTICE.md +3 -0
- package/skills/obk-writing/NOTICE.md +24 -0
- package/src/cli.js +53 -17
- package/src/conversations.js +43 -3
- package/src/groom.js +144 -11
- package/src/health.js +20 -1
- package/src/orca.js +13 -3
- package/src/temp.js +99 -10
- package/src/usage.js +86 -12
package/src/groom.js
CHANGED
|
@@ -25,12 +25,20 @@
|
|
|
25
25
|
// asked for, or none. Run in any order, the last one typed is what holds
|
|
26
26
|
// (review of PR #322).
|
|
27
27
|
//
|
|
28
|
+
// A job can make its runs on Codex instead (#238, PRD 6.8): the session stays the
|
|
29
|
+
// Claude Code one, the clock and the reader, and at each fire it makes the run
|
|
30
|
+
// as a temporary Codex session of its own (`obk temp make`), with the job's
|
|
31
|
+
// model and effort on the run's launch line. The run does the grooming, sends
|
|
32
|
+
// its report back by the kit's mail and is retired by the session, which first
|
|
33
|
+
// retires any run an earlier fire left. What the runs are on is in the job's
|
|
34
|
+
// prompt and nowhere else, so the kit reads it back from there.
|
|
35
|
+
//
|
|
28
36
|
// Claude Code ends a recurring job a week after it was made, so the job's own
|
|
29
37
|
// prompt ends by renewing it. A renewal that fails either way shows here: no
|
|
30
38
|
// job means the fleet has quietly stopped being groomed, two mean it is groomed
|
|
31
39
|
// twice, and both are said plainly.
|
|
32
40
|
|
|
33
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
41
|
+
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
34
42
|
import path from 'node:path';
|
|
35
43
|
|
|
36
44
|
import { botDir, readBot, requireBotsFolder } from './bot.js';
|
|
@@ -38,7 +46,7 @@ import { readBook } from './book.js';
|
|
|
38
46
|
import { claudeTranscript } from './conversations.js';
|
|
39
47
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
40
48
|
import { tabs, tabToTypeInto, typeIntoTab } from './orca.js';
|
|
41
|
-
import { BOT_FATHER } from './up.js';
|
|
49
|
+
import { BOT_FATHER, promptPath } from './up.js';
|
|
42
50
|
import { lines } from './usage.js';
|
|
43
51
|
|
|
44
52
|
/** Bot Father's session the grooming runs in. */
|
|
@@ -50,6 +58,12 @@ const AT = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
|
50
58
|
/** A daily cron as the kit writes one: minute, hour, every day. */
|
|
51
59
|
const DAILY = /^(\d{1,2}) (\d{1,2}) \* \* \*$/;
|
|
52
60
|
|
|
61
|
+
/** The harnesses a job can make its runs on, besides running them in the session itself (#238). */
|
|
62
|
+
const RUN_ON = ['codex'];
|
|
63
|
+
|
|
64
|
+
/** The name each run is given, filled in by the session at the fire it makes it. */
|
|
65
|
+
const RUN_NAME = 'groom-<YYYYMMDD-HHMM>';
|
|
66
|
+
|
|
53
67
|
/** How long Claude Code keeps a recurring job after it was made (tech notes, section 2). */
|
|
54
68
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
55
69
|
|
|
@@ -79,8 +93,9 @@ export const markerOf = (bots) => `obk grooming for ${bots}`;
|
|
|
79
93
|
* (or null), the grooming jobs alive in its conversation as the command found
|
|
80
94
|
* them, before anything it typed was done, and what it typed (or null).
|
|
81
95
|
*/
|
|
82
|
-
export function grooming(bots, { at, ask } = {}) {
|
|
96
|
+
export function grooming(bots, { at, ask, run } = {}) {
|
|
83
97
|
requireBotsFolder(bots);
|
|
98
|
+
refuseRun(run, ask);
|
|
84
99
|
|
|
85
100
|
// The folder as the file system knows it: Claude Code files a transcript
|
|
86
101
|
// under the real path, and Orca knows a tab's project by it.
|
|
@@ -108,7 +123,7 @@ export function grooming(bots, { at, ask } = {}) {
|
|
|
108
123
|
? groomingJobs(ofThisProcess(home, entry), markerOf(bots), Date.now())
|
|
109
124
|
: [];
|
|
110
125
|
|
|
111
|
-
const line = ask === undefined ? undefined : lineFor(ask, { bots, session, jobs, at });
|
|
126
|
+
const line = ask === undefined ? undefined : lineFor(ask, { bots, session, jobs, at, run });
|
|
112
127
|
if (line !== undefined) typeInto(bots, home, session, tab, line);
|
|
113
128
|
|
|
114
129
|
// A move is turning it on at another time, and is typed as one.
|
|
@@ -120,7 +135,7 @@ export function grooming(bots, { at, ask } = {}) {
|
|
|
120
135
|
* nothing to ask. Everything that makes the ask impossible refuses here, before
|
|
121
136
|
* Orca is asked for anything.
|
|
122
137
|
*/
|
|
123
|
-
function lineFor(ask, { bots, session, jobs, at }) {
|
|
138
|
+
function lineFor(ask, { bots, session, jobs, at, run }) {
|
|
124
139
|
// Nothing to turn off is an answer, unless a session is there to be told: an
|
|
125
140
|
// `--on` typed a moment ago may still be waiting in its tab.
|
|
126
141
|
if (ask === 'off' && jobs.length === 0 && !(session?.harness === 'claude' && session.up)) return undefined;
|
|
@@ -147,7 +162,28 @@ function lineFor(ask, { bots, session, jobs, at }) {
|
|
|
147
162
|
? `grooming is off, so there is no time to keep. Say when: ${groomCommand(bots)} --on --at <HH:MM>.`
|
|
148
163
|
: `the grooming jobs there run at different times (${times.join(', ')}). Say which one to keep: ${groomCommand(bots)} --on --at <HH:MM>.`);
|
|
149
164
|
}
|
|
150
|
-
|
|
165
|
+
// A move keeps what the runs are on, when every job there says the same.
|
|
166
|
+
const runs = [...new Set(jobs.map((job) => JSON.stringify(job.run ?? null)))];
|
|
167
|
+
const keep = ask === 'move' && runs.length === 1 ? JSON.parse(runs[0]) ?? undefined : undefined;
|
|
168
|
+
return onLine(bots, cronOf(when), jobs, run ?? keep);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Refuse what the run was given before anything else is looked at: its flags
|
|
173
|
+
* go with --on, --run-on takes codex, and a model, an effort or an extra
|
|
174
|
+
* argument is the run's, so it goes with --run-on codex.
|
|
175
|
+
*/
|
|
176
|
+
function refuseRun(run, ask) {
|
|
177
|
+
if (run === undefined) return;
|
|
178
|
+
if (run.harness === undefined) {
|
|
179
|
+
throw new Error('--model, --effort and --extra-arg are what each grooming run is launched with when it runs on Codex, and go with --run-on codex. The grooming session\'s own runs use its own settings. Nothing was typed.');
|
|
180
|
+
}
|
|
181
|
+
if (!RUN_ON.includes(run.harness)) {
|
|
182
|
+
throw new Error(`--run-on takes codex: grooming runs either in its Claude Code session, as it does without --run-on, or as a temporary Codex session made at each fire. Got: ${run.harness === '' ? 'nothing' : run.harness}. Nothing was typed.`);
|
|
183
|
+
}
|
|
184
|
+
if (ask !== 'on') {
|
|
185
|
+
throw new Error('--run-on says what the grooming runs are on, and goes with --on. Nothing was typed.');
|
|
186
|
+
}
|
|
151
187
|
}
|
|
152
188
|
|
|
153
189
|
/**
|
|
@@ -230,7 +266,8 @@ export function groomingJobs(entries, marker, now) {
|
|
|
230
266
|
const { cron, prompt, recurring } = call.input ?? {};
|
|
231
267
|
const made = Date.parse(entry.timestamp ?? '');
|
|
232
268
|
if (recurring === false || typeof prompt !== 'string' || !prompt.startsWith(marker) || Number.isNaN(made)) continue;
|
|
233
|
-
|
|
269
|
+
const run = runIn(prompt);
|
|
270
|
+
alive.set(result.id, { id: result.id, at: timeOf(cron), cron, made, expires: made + WEEK_MS, ...(run === undefined ? {} : { run }) });
|
|
234
271
|
}
|
|
235
272
|
}
|
|
236
273
|
return [...alive.values()]
|
|
@@ -239,6 +276,32 @@ export function groomingJobs(entries, marker, now) {
|
|
|
239
276
|
.map((job) => ({ ...job, made: new Date(job.made).toISOString(), expires: new Date(job.expires).toISOString() }));
|
|
240
277
|
}
|
|
241
278
|
|
|
279
|
+
/**
|
|
280
|
+
* What a job's runs are on, read out of its prompt: the harness, model and
|
|
281
|
+
* effort its \`temp make\` is given, each left out when it was not; undefined
|
|
282
|
+
* for a job whose runs are the session's own.
|
|
283
|
+
*/
|
|
284
|
+
function runIn(prompt) {
|
|
285
|
+
const make = / temp make ([^`]*)`/.exec(prompt);
|
|
286
|
+
if (make === null) return undefined;
|
|
287
|
+
const value = (flag) => {
|
|
288
|
+
const found = new RegExp(`(?:^| )--${flag}(?:=| )('[^']*'|\\S+)`).exec(make[1]);
|
|
289
|
+
return found === null ? undefined : found[1].replace(/^'(.*)'$/, '$1');
|
|
290
|
+
};
|
|
291
|
+
const run = { harness: value('harness') };
|
|
292
|
+
if (run.harness === undefined) return undefined;
|
|
293
|
+
for (const field of ['model', 'effort']) {
|
|
294
|
+
const got = value(field);
|
|
295
|
+
if (got !== undefined) run[field] = got;
|
|
296
|
+
}
|
|
297
|
+
// Each extra argument as the job wrote it, one shell word, read back to what
|
|
298
|
+
// it was given as, so a move keeps them all (review of PR #439).
|
|
299
|
+
const extra = [...make[1].matchAll(/(?:^| )--extra-arg=('(?:[^']|'\\'')*'|[A-Za-z0-9,._+:@%/=-]+)/g)]
|
|
300
|
+
.map((found) => (found[1].startsWith('\'') ? found[1].slice(1, -1).replaceAll(`'\\''`, '\'') : found[1]));
|
|
301
|
+
if (extra.length > 0) run.extra_args = extra;
|
|
302
|
+
return run;
|
|
303
|
+
}
|
|
304
|
+
|
|
242
305
|
/** `HH:MM` as the daily cron Claude Code takes: `04:00` is `0 4 * * *`. */
|
|
243
306
|
const cronOf = (at) => {
|
|
244
307
|
const [, hour, minute] = AT.exec(at);
|
|
@@ -268,11 +331,81 @@ const run = (bots) =>
|
|
|
268
331
|
* fired with, so the job carries itself on; the one that fired is then the
|
|
269
332
|
* older of the two.
|
|
270
333
|
*/
|
|
271
|
-
const jobPrompt = (bots, cron) =>
|
|
272
|
-
`${run(bots)} When the report is sent,
|
|
334
|
+
const jobPrompt = (bots, cron, onRun) => (onRun === undefined
|
|
335
|
+
? `${run(bots)} When the report is sent, ${renewal(cron)}`
|
|
336
|
+
// A run on Codex reports in a later turn, and may never: the job renews at
|
|
337
|
+
// the fire, once the run is made, so a run that never reports does not end
|
|
338
|
+
// the schedule with it (review of PR #439).
|
|
339
|
+
: `${runElsewhere(bots, onRun)} Then ${renewal(cron)} Then end your turn. ${readReport(bots)}`);
|
|
340
|
+
|
|
341
|
+
/** The part of a job that renews it, which Claude Code would otherwise end a week after it was made. */
|
|
342
|
+
const renewal = (cron) =>
|
|
343
|
+
'renew this schedule, which Claude Code ends a week after it was made: '
|
|
273
344
|
+ `call CronCreate with cron "${cron}", recurring true, and this whole prompt, word for word, as its prompt; `
|
|
274
345
|
+ `then call CronList and CronDelete every other job there whose prompt starts with "${PREFIX}".`;
|
|
275
346
|
|
|
347
|
+
/**
|
|
348
|
+
* The part of a job whose runs are on Codex (#238): clear what an earlier run
|
|
349
|
+
* left, make this fire's run, and read its report when it comes. Each command
|
|
350
|
+
* is the kit by its own path, as the run above names it, because a Claude Code
|
|
351
|
+
* allow rule matches a command as written (#344). The run's task goes in a file
|
|
352
|
+
* the kit writes now, beside the bots folder, as it leaves every prompt.
|
|
353
|
+
*/
|
|
354
|
+
function runElsewhere(bots, onRun) {
|
|
355
|
+
const kit = shellWord(ownCli());
|
|
356
|
+
const folder = shellWord(bots);
|
|
357
|
+
const retire = `\`${kit} temp retire --bots ${folder} --name ${RUN_NAME}\``;
|
|
358
|
+
const make = [
|
|
359
|
+
`${kit} temp make --bots ${folder} --name ${RUN_NAME} --harness ${onRun.harness}`,
|
|
360
|
+
...(onRun.model === undefined ? [] : [`--model ${shellWord(onRun.model)}`]),
|
|
361
|
+
...(onRun.effort === undefined ? [] : [`--effort ${shellWord(onRun.effort)}`]),
|
|
362
|
+
...(onRun.extra_args ?? []).map((arg) => `--extra-arg=${shellWord(arg)}`),
|
|
363
|
+
`--prompt-file ${shellWord(writeRunTask(bots))}`,
|
|
364
|
+
].join(' ');
|
|
365
|
+
return `${markerOf(bots)}: the daily grooming for this fleet, run on ${onRun.harness} as a temporary session of yours `
|
|
366
|
+
+ `that does the work and reports back to you. First, if any of your temporary sessions whose names start with groom- `
|
|
367
|
+
+ `is still there (${BOT_FATHER}'s bot.yaml lists them), a run an earlier fire left, retire each with ${retire}, `
|
|
368
|
+
+ `its own name in place of ${RUN_NAME}. Then make this fire's run with \`${make}\`, `
|
|
369
|
+
+ `where ${RUN_NAME} is this fire's date and time. Its tab can stop on Codex's first-run screens: answer its folder trust `
|
|
370
|
+
+ 'yourself, as the make\'s output and your rules say, and the review of its hooks with '
|
|
371
|
+
+ `\`${kit} temp trust-hooks --bots ${folder} --name ${RUN_NAME}\`, its name in place of ${RUN_NAME}.`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The part of a Codex job for the later turn its run's report comes in: read it, send it on, retire the run. */
|
|
375
|
+
function readReport(bots) {
|
|
376
|
+
const retire = `\`${shellWord(ownCli())} temp retire --bots ${shellWord(bots)} --name ${RUN_NAME}\``;
|
|
377
|
+
const road = `\`${shellWord(ownCli())} message to --bots ${shellWord(bots)} --to ${BOT_FATHER}/<that session>\``;
|
|
378
|
+
// Claude Code's own messaging reaches every session on the machine, so a
|
|
379
|
+
// name that only looks right can be another fleet's session (#450).
|
|
380
|
+
return `When a line comes saying fleet mail from that run is waiting, read the mail the way the line says: it is the grooming report. `
|
|
381
|
+
+ `Send it on, as it is, to ${BOT_FATHER}'s management session, as a grooming run's report goes: to exactly the address or mailbox `
|
|
382
|
+
+ `${road} answers, and to no other. If your messaging does not list that address, send the report nowhere else, and say so. `
|
|
383
|
+
+ `Then retire the run with ${retire}, its name in place of ${RUN_NAME}. Schedule nothing then: the job renewed itself when it fired.`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Write the task a Codex run is started with, and answer where: do the daily
|
|
388
|
+
* grooming with the obk-grooming skill, send the report to the grooming session
|
|
389
|
+
* by the kit's mail, and stop. It schedules nothing: the schedule is the
|
|
390
|
+
* grooming session's.
|
|
391
|
+
*/
|
|
392
|
+
function writeRunTask(bots) {
|
|
393
|
+
const real = realHome(bots);
|
|
394
|
+
const kit = shellWord(ownCli());
|
|
395
|
+
const file = path.resolve(promptPath(real, BOT_FATHER, `${GROOMING}-run`));
|
|
396
|
+
const task = `You are one run of the daily grooming of the fleet in ${real}, made by ${BOT_FATHER}'s ${GROOMING} session. `
|
|
397
|
+
+ 'Run the daily grooming for this fleet, with the obk-grooming skill. Fix this run\'s end now and read what has happened '
|
|
398
|
+
+ `since the last run's end up to it, counting with ${kit} usage --since <last end> --until <this end>, and write this end down for the next run. `
|
|
399
|
+
+ 'Work out what it cost with obk-finops, and keep each bot\'s profile notes. '
|
|
400
|
+
+ `The kit here is ${kit}: run it wherever a skill says obk. The bots folder is ${real}. `
|
|
401
|
+
+ `When the grooming is done, send one short report to ${BOT_FATHER}/${GROOMING} by the kit's mail: `
|
|
402
|
+
+ `ask for the road with ${kit} message to --bots ${shellWord(real)} --to ${BOT_FATHER}/${GROOMING}, then send it with the message send command its answer names. `
|
|
403
|
+
+ 'Then stop: schedule nothing, make no session, and say nothing more.\n';
|
|
404
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
405
|
+
writeFileSync(file, task);
|
|
406
|
+
return file;
|
|
407
|
+
}
|
|
408
|
+
|
|
276
409
|
/**
|
|
277
410
|
* The part of a line that clears the grooming jobs the session has when it runs
|
|
278
411
|
* the line, which is not always what the kit saw when it typed it.
|
|
@@ -281,11 +414,11 @@ const clearing = (jobs) =>
|
|
|
281
414
|
`call CronList, and call CronDelete on every job there whose prompt starts with "${PREFIX}"`
|
|
282
415
|
+ (jobs.length === 0 ? '' : ` (when this was typed they were ${jobs.map((job) => job.id).join(', ')})`);
|
|
283
416
|
|
|
284
|
-
const onLine = (bots, cron, jobs) =>
|
|
417
|
+
const onLine = (bots, cron, jobs, onRun) =>
|
|
285
418
|
`obk groom asks you to schedule the daily grooming. First ${clearing(jobs)}. `
|
|
286
419
|
+ `Then call CronCreate once, with cron "${cron}", recurring true, and as its prompt everything after "Prompt:" `
|
|
287
420
|
+ 'at the end of this line, word for word. Do not run the grooming now; say in one line what you scheduled. '
|
|
288
|
-
+ `Prompt: ${jobPrompt(bots, cron)}`;
|
|
421
|
+
+ `Prompt: ${jobPrompt(bots, cron, onRun)}`;
|
|
289
422
|
|
|
290
423
|
const offLine = (jobs) =>
|
|
291
424
|
`obk groom asks you to turn the daily grooming off: ${clearing(jobs)}. `
|
package/src/health.js
CHANGED
|
@@ -25,7 +25,7 @@ import { bookFile, readBook, sessionIdsIn, tabIdsIn } from './book.js';
|
|
|
25
25
|
import { botDir, botNames, botsDir, readBot, unknownKeys } from './bot.js';
|
|
26
26
|
import { transcriptsIn } from './conversations.js';
|
|
27
27
|
import { hookTrouble } from './hooks.js';
|
|
28
|
-
import { bypassFlags, harnessOf, HARNESSES, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
28
|
+
import { bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
29
29
|
import { frontOfTab, orcaDefaultArgs, projects, tabs, wordsOfProcess } from './orca.js';
|
|
30
30
|
import { permissionsTrouble } from './permissions.js';
|
|
31
31
|
import { TAB_ENV } from './record.js';
|
|
@@ -259,7 +259,26 @@ function runningOn(bots, home, bot, book, handles, sessions) {
|
|
|
259
259
|
// that is not its own harness, or a front that cannot be read, leaves it
|
|
260
260
|
// unsaid, and nothing unsaid is a finding.
|
|
261
261
|
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${bot.name} --session ${session.name}`;
|
|
262
|
+
// A Claude session is written to by its name, and Claude Code's own
|
|
263
|
+
// messaging reaches every session on the machine. One under a name the kit
|
|
264
|
+
// did not make, such as the bare <bot>.<session> every fleet had before
|
|
265
|
+
// #286, can be written to by another fleet's session (#450). Its next start
|
|
266
|
+
// through the kit gives it a name of its own; one that Orca brought back,
|
|
267
|
+
// or that has run since, keeps the old one until then.
|
|
262
268
|
const front = frontOfTab(handle);
|
|
269
|
+
const address = entry.address;
|
|
270
|
+
if (harnessOf(session, bot.harness) === 'claude' && typeof address === 'string' && address !== '' && !isAddressOf(bot.name, session.name, address)) {
|
|
271
|
+
const shared = address === `${bot.name}.${session.name}`
|
|
272
|
+
? `the name every fleet's ${bot.name}/${session.name} was given before #286, so it is shared: a Claude session of another fleet with that bot and session can write to it, thinking it is its own`
|
|
273
|
+
: `not one the kit made for it, so it may not be this session's alone: another session, in this fleet or another, can go by it too`;
|
|
274
|
+
// A restart closes the tab, and with no conversation in the book it
|
|
275
|
+
// refuses rather than end one that is running, so the fix starts there.
|
|
276
|
+
// With only the shell in front there is nothing to end, and it goes.
|
|
277
|
+
const fix = typeof entry.session === 'string' || front.front === 'shell'
|
|
278
|
+
? `${restart} starts it on an address of its own, <bot>.<session> and a token.`
|
|
279
|
+
: `The book does not say which conversation it is running, and a restart refuses until it does: write the id into ${bookFile(home)} under ${session.name} as session: <id>, then ${restart} starts it on an address of its own, <bot>.<session> and a token.`;
|
|
280
|
+
found.push(finding('session', bookFile(home), `${bot.name}'s session ${session.name} goes by the address ${address}, ${shared}. ${fix}`, bot.name));
|
|
281
|
+
}
|
|
263
282
|
if (front.front === 'shell') {
|
|
264
283
|
found.push(finding('session', entry.tab, `${bot.name}'s session ${session.name} is not running: its tab ${entry.tab} is open with only the tab's shell in front, so its harness quit or crashed. obk up finds the tab open and types nothing into it, so it does not bring the session back; ${restart} does.`, bot.name));
|
|
265
284
|
continue;
|
package/src/orca.js
CHANGED
|
@@ -383,16 +383,26 @@ export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
|
383
383
|
* section 1).
|
|
384
384
|
*/
|
|
385
385
|
function screenOf(handle, readMs) {
|
|
386
|
+
const read = screenRows(handle, readMs);
|
|
387
|
+
return read.rows === undefined ? { screenUnreadable: read.unreadable } : { question: questionIn(read.rows) };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The rows the tab `handle` renders, `{ rows }`, or `{ unreadable: <why> }`.
|
|
392
|
+
* Orca refusing, or answering with anything but the rendered screen, is a
|
|
393
|
+
* screen that cannot be read, not an error.
|
|
394
|
+
*/
|
|
395
|
+
export function screenRows(handle, readMs) {
|
|
386
396
|
let read;
|
|
387
397
|
try {
|
|
388
398
|
read = orca(['terminal', 'read', '--terminal', handle, '--screen'], { timeoutMs: readMs }).terminal;
|
|
389
399
|
} catch (error) {
|
|
390
|
-
return {
|
|
400
|
+
return { unreadable: `Orca would not read its screen: ${error.message}` };
|
|
391
401
|
}
|
|
392
402
|
if (read?.source !== 'screen' || !Array.isArray(read.tail)) {
|
|
393
|
-
return {
|
|
403
|
+
return { unreadable: `Orca gave no rendered screen for it (source: ${read?.source ?? 'none'})` };
|
|
394
404
|
}
|
|
395
|
-
return {
|
|
405
|
+
return { rows: read.tail };
|
|
396
406
|
}
|
|
397
407
|
|
|
398
408
|
/** A row the harness starts with its selection pointer: `›` on Codex, `❯` on Claude Code. */
|
package/src/temp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// `obk temp make` and `obk temp retire`: a session's own temporary sessions
|
|
2
|
-
// (PRD 6.4, #227).
|
|
2
|
+
// (PRD 6.4, #227). And `obk temp trust-hooks`, the one answer a maker gives its
|
|
3
|
+
// Codex run's hooks review (#238).
|
|
3
4
|
//
|
|
4
5
|
// Any long-lived session can make a temporary session of its own bot for a
|
|
5
6
|
// piece of work, and retire it when the work is done, without Bot Father, who
|
|
@@ -22,11 +23,13 @@
|
|
|
22
23
|
|
|
23
24
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
24
25
|
import path from 'node:path';
|
|
26
|
+
import { setTimeout as pause } from 'node:timers/promises';
|
|
25
27
|
|
|
26
28
|
import { readBook, updateBook } from './book.js';
|
|
27
29
|
import { addSession, dropSession, NAME, readBot } from './bot.js';
|
|
28
|
-
import { isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
30
|
+
import { harnessOf, isShortPrompt, ownCli, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
29
31
|
import { sessionInTab } from './message.js';
|
|
32
|
+
import { orca, screenRows, tabs } from './orca.js';
|
|
30
33
|
import { retireSession } from './retire.js';
|
|
31
34
|
import { bringUp, promptPath } from './up.js';
|
|
32
35
|
|
|
@@ -61,10 +64,14 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
61
64
|
const settings = { name: given.name };
|
|
62
65
|
const harness = given.harness ?? maker?.harness;
|
|
63
66
|
if (harness !== undefined) settings.harness = harness;
|
|
67
|
+
// On another harness than its maker's, the model, the effort and the context
|
|
68
|
+
// are that harness's own, and only the approval carries over (#238, PRD 6.4).
|
|
69
|
+
const same = (harness ?? bot.harness) === harnessOf(maker ?? {}, bot.harness);
|
|
64
70
|
for (const field of INHERITED) {
|
|
65
|
-
const value = given[field] ?? maker?.[field];
|
|
71
|
+
const value = given[field] ?? (same || field === 'approval' ? maker?.[field] : undefined);
|
|
66
72
|
if (value !== undefined) settings[field] = value;
|
|
67
73
|
}
|
|
74
|
+
if (given.extra_args !== undefined) settings.extra_args = given.extra_args;
|
|
68
75
|
if (given.prompt !== undefined) settings.prompt = given.prompt;
|
|
69
76
|
else settings.prompt_file = given.prompt_file;
|
|
70
77
|
settings.work_dir = `work/${given.name}`;
|
|
@@ -145,28 +152,110 @@ function writePromptFirst(bots, caller, settings) {
|
|
|
145
152
|
*/
|
|
146
153
|
export async function retireTemp(bots, { tab, name }) {
|
|
147
154
|
const caller = callerIn(bots, tab, 'retire');
|
|
155
|
+
ownTemp(caller, name, 'retire', 'Nothing was retired.');
|
|
156
|
+
return { ...(await retireSession(bots, { bot: caller.bot, session: name })), maker: caller.session };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The caller's own temporary session `name`, from bot.yaml, or a refusal: one
|
|
161
|
+
* the bot does not have, a long-lived one, or one another session made.
|
|
162
|
+
* `verb` finishes "to <verb>", and `nothing` is the sentence that ends each.
|
|
163
|
+
*/
|
|
164
|
+
function ownTemp(caller, name, verb, nothing) {
|
|
148
165
|
const bot = readBot(caller.home, caller.bot);
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
const session = bot.sessions.find((one) => one.name === name);
|
|
167
|
+
if (session === undefined) {
|
|
168
|
+
throw new Error(`${caller.bot} has no session called ${name}, so there is nothing of yours to ${verb}. ${nothing}`);
|
|
151
169
|
}
|
|
152
170
|
const temporary = readBook(caller.home).sessions[name]?.temporary;
|
|
153
171
|
if (temporary === undefined) {
|
|
154
|
-
throw new Error(`${caller.bot}/${name} is a long-lived session, and those are Bot Father's to
|
|
172
|
+
throw new Error(`${caller.bot}/${name} is a long-lived session, and those are Bot Father's to ${verb}. ${nothing}`);
|
|
155
173
|
}
|
|
156
174
|
if (temporary.maker !== caller.session) {
|
|
157
|
-
throw new Error(`${caller.bot}/${name} is a temporary session ${temporary.maker} made, not ${caller.session}, so it is ${temporary.maker}'s to
|
|
175
|
+
throw new Error(`${caller.bot}/${name} is a temporary session ${temporary.maker} made, not ${caller.session}, so it is ${temporary.maker}'s to ${verb}. ${nothing}`);
|
|
158
176
|
}
|
|
159
|
-
return {
|
|
177
|
+
return { bot, session };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Codex's hooks review, and the one of its choices this kit answers it with (tech notes, section 3). */
|
|
181
|
+
const HOOKS_REVIEW = 'Hooks need review';
|
|
182
|
+
const TRUST_ALL = 'Trust all and continue';
|
|
183
|
+
|
|
184
|
+
/** How long the review is given to go once answered, looked at every half second. */
|
|
185
|
+
const REVIEW_GONE_MS = 5000;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Answer the hooks review of a Codex run the caller made with "Trust all and
|
|
189
|
+
* continue", and confirm the review went (#238, the owner's choice (b)). The
|
|
190
|
+
* one answer a permission rule of its own can allow: a raw `orca terminal
|
|
191
|
+
* send` cannot be narrowed to a tab or to these keys. Everything that can
|
|
192
|
+
* refuse is checked before a key is sent. Returns `{ bot, session, maker }`.
|
|
193
|
+
*/
|
|
194
|
+
export async function trustHooks(bots, { tab, name }) {
|
|
195
|
+
const caller = callerIn(bots, tab, 'trust-hooks', "answer a run's hooks review");
|
|
196
|
+
const nothing = 'Nothing was typed.';
|
|
197
|
+
const { bot, session } = ownTemp(caller, name, 'answer for', nothing);
|
|
198
|
+
const run = `${caller.bot}/${name}`;
|
|
199
|
+
const harness = harnessOf(session, bot.harness);
|
|
200
|
+
if (harness !== 'codex') {
|
|
201
|
+
throw new Error(`${run} runs on ${harness}, and temp trust-hooks answers a Codex run's "${HOOKS_REVIEW}" alone. ${nothing}`);
|
|
202
|
+
}
|
|
203
|
+
const tabId = readBook(caller.home).sessions[name]?.tab;
|
|
204
|
+
const handle = tabId === undefined ? undefined : tabs(caller.home).find((one) => one.tabId === tabId)?.handle;
|
|
205
|
+
if (handle === undefined) {
|
|
206
|
+
throw new Error(`${run} has no tab open in Orca, so there is no review of its to answer. ${nothing}`);
|
|
207
|
+
}
|
|
208
|
+
const seen = screenRows(handle);
|
|
209
|
+
if (seen.rows === undefined) {
|
|
210
|
+
throw new Error(`${run}'s screen could not be read (${seen.unreadable}), so the kit cannot tell whether its hooks review is there. ${nothing}`);
|
|
211
|
+
}
|
|
212
|
+
const keys = keysToTrustAll(seen.rows);
|
|
213
|
+
if (keys === undefined) {
|
|
214
|
+
throw new Error(`${run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(seen.rows)}. ${nothing}`);
|
|
215
|
+
}
|
|
216
|
+
// The return is inside the text: `--enter` would be a second key.
|
|
217
|
+
orca(['terminal', 'send', '--terminal', handle, '--text', keys]);
|
|
218
|
+
for (let waited = 0; ; waited += 500) {
|
|
219
|
+
const after = screenRows(handle);
|
|
220
|
+
if (after.rows !== undefined && !after.rows.some((row) => row.includes(HOOKS_REVIEW))) break;
|
|
221
|
+
if (waited >= REVIEW_GONE_MS) {
|
|
222
|
+
const still = after.rows === undefined ? `its screen could not be read again (${after.unreadable})` : `its screen still shows "${HOOKS_REVIEW}"`;
|
|
223
|
+
throw new Error(`${run}: "${TRUST_ALL}" was chosen on its hooks review, but ${REVIEW_GONE_MS / 1000} seconds later ${still}. Look at its tab.`);
|
|
224
|
+
}
|
|
225
|
+
await pause(500);
|
|
226
|
+
}
|
|
227
|
+
return { bot: caller.bot, session: name, maker: caller.session };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The keys that take Codex's hooks review to "2. Trust all and continue" and
|
|
232
|
+
* answer it, from wherever its pointer is: arrows, then return, never a digit
|
|
233
|
+
* (a digit and return once took whatever was highlighted). Undefined when the
|
|
234
|
+
* rows show no such review with the pointer on one of its choices.
|
|
235
|
+
*/
|
|
236
|
+
function keysToTrustAll(rows) {
|
|
237
|
+
if (!rows.some((row) => row.includes(HOOKS_REVIEW))) return undefined;
|
|
238
|
+
if (!rows.some((row) => new RegExp(`^ *(?:› +)?2\\. ${TRUST_ALL}`).test(row))) return undefined;
|
|
239
|
+
const pointer = rows.map((row) => /^ *› +(\d+)\. /.exec(row)).findLast((found) => found !== null);
|
|
240
|
+
if (pointer === undefined) return undefined;
|
|
241
|
+
const moves = 2 - Number(pointer[1]);
|
|
242
|
+
return (moves > 0 ? '\x1b[B'.repeat(moves) : '\x1b[A'.repeat(-moves)) + '\r';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** What a screen shows, on one line: its rows with words in them, cut short. */
|
|
246
|
+
function shown(rows) {
|
|
247
|
+
const text = rows.map((row) => row.trim()).filter((row) => row !== '').join(' ⏎ ');
|
|
248
|
+
return text.length > 500 ? `${text.slice(0, 500)}…` : text;
|
|
160
249
|
}
|
|
161
250
|
|
|
162
251
|
/**
|
|
163
252
|
* The session this runs in, as the book knows it, with its home and whether it
|
|
164
253
|
* is itself temporary.
|
|
165
254
|
*/
|
|
166
|
-
function callerIn(bots, tab, what) {
|
|
255
|
+
function callerIn(bots, tab, what, doing = `${what} it`) {
|
|
167
256
|
const found = tab === undefined ? undefined : sessionInTab(bots, tab);
|
|
168
257
|
if (found === undefined) {
|
|
169
|
-
throw new Error(`temp ${what} is run by a session in its own tab, and this is not one of the fleet's tabs, so there is no session here to ${
|
|
258
|
+
throw new Error(`temp ${what} is run by a session in its own tab, and this is not one of the fleet's tabs, so there is no session here to ${doing} for. Nothing was done.`);
|
|
170
259
|
}
|
|
171
260
|
const entry = readBook(found.home).sessions[found.session] ?? {};
|
|
172
261
|
return { bot: found.bot, session: found.session, home: found.home, temporary: entry.temporary };
|
package/src/usage.js
CHANGED
|
@@ -43,7 +43,7 @@ import { realpathSync } from 'node:fs';
|
|
|
43
43
|
|
|
44
44
|
import { botDir, botNames, readBot } from './bot.js';
|
|
45
45
|
import { readBook, sessionIdsIn } from './book.js';
|
|
46
|
-
import { claudeSubagentTranscripts, claudeTranscriptAnywhere, codexRollout, transcriptsIn } from './conversations.js';
|
|
46
|
+
import { claudeSubagentTranscripts, claudeTranscriptAnywhere, codexRollout, codexSubagents, transcriptsIn } from './conversations.js';
|
|
47
47
|
import { eachLine } from './lines.js';
|
|
48
48
|
import { HARNESSES, harnessOf } from './launch.js';
|
|
49
49
|
|
|
@@ -91,11 +91,41 @@ export function readUsage(bots, { bot: only, session: onlySession, since, until
|
|
|
91
91
|
throw new Error(`--until is before --since, so there is no window between them: ${until} comes before ${since}.`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// Who owns a Codex subagent is a question about the whole fleet, asked once:
|
|
95
|
+
// one can run in another bot's folder, or be claimed by another bot's book.
|
|
96
|
+
const fleet = subagentOwners(bots, names);
|
|
97
|
+
return (only === undefined ? names : [only]).map((name) => forBot(bots, name, onlySession, { from, to }, fleet));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Every conversation any bot's book names, the Codex subagents that hang off
|
|
102
|
+
* each by their own links, and the ones so attached (#449). A subagent a book
|
|
103
|
+
* names is that book's, and is never attached under another: the book's word
|
|
104
|
+
* comes first (ADR 0012). One claimed or attached anywhere is no bot's
|
|
105
|
+
* unclaimed.
|
|
106
|
+
*/
|
|
107
|
+
function subagentOwners(bots, names) {
|
|
108
|
+
const claimed = new Set();
|
|
109
|
+
for (const name of names) for (const id of sessionIdsIn(readBook(realHome(botDir(bots, name))))) claimed.add(id);
|
|
110
|
+
const childrenOf = new Map();
|
|
111
|
+
for (const one of codexSubagents()) {
|
|
112
|
+
if (claimed.has(one.id)) continue;
|
|
113
|
+
if (!childrenOf.has(one.parent)) childrenOf.set(one.parent, []);
|
|
114
|
+
childrenOf.get(one.parent).push(one);
|
|
115
|
+
}
|
|
116
|
+
const attached = new Set();
|
|
117
|
+
const waiting = [...claimed].flatMap((id) => childrenOf.get(id) ?? []);
|
|
118
|
+
while (waiting.length > 0) {
|
|
119
|
+
const child = waiting.shift();
|
|
120
|
+
if (attached.has(child.id)) continue;
|
|
121
|
+
attached.add(child.id);
|
|
122
|
+
waiting.push(...(childrenOf.get(child.id) ?? []));
|
|
123
|
+
}
|
|
124
|
+
return { claimed, childrenOf, attached };
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
/** One bot: what each of its sessions used, and what nobody claims. */
|
|
98
|
-
function forBot(bots, name, onlySession, window) {
|
|
128
|
+
function forBot(bots, name, onlySession, window, fleet) {
|
|
99
129
|
// The folder as the file system knows it, not as the caller spelled it: a
|
|
100
130
|
// bots folder reached through a symlink is the same fleet, and the harnesses
|
|
101
131
|
// file their transcripts under the real path (as `restart` and `message` do).
|
|
@@ -138,6 +168,31 @@ function forBot(bots, name, onlySession, window) {
|
|
|
138
168
|
}
|
|
139
169
|
}
|
|
140
170
|
|
|
171
|
+
// A Codex subagent names the conversation it was started for, and what it
|
|
172
|
+
// spent is that conversation's, at any depth, as a Claude Code subagent's is
|
|
173
|
+
// (#449). It is found by that link wherever it ran, and joined only through
|
|
174
|
+
// links that end at a conversation the book names, as the fleet's owners
|
|
175
|
+
// say: a parent no session claims leaves its children where they were, and
|
|
176
|
+
// nothing is guessed from timing or folder. The book's word is enough
|
|
177
|
+
// without the parent's own file, whose calls are then said to be unreadable.
|
|
178
|
+
const { childrenOf } = fleet;
|
|
179
|
+
const attached = new Set();
|
|
180
|
+
for (const id of claimed) {
|
|
181
|
+
const own = onRecord.get(id);
|
|
182
|
+
if (own !== undefined && own.harness !== 'codex') continue;
|
|
183
|
+
const found = [];
|
|
184
|
+
const waiting = [...(childrenOf.get(id) ?? [])];
|
|
185
|
+
while (waiting.length > 0) {
|
|
186
|
+
const child = waiting.shift();
|
|
187
|
+
if (attached.has(child.id)) continue;
|
|
188
|
+
attached.add(child.id);
|
|
189
|
+
// A review of a subagent is still the harness's approval, not the work.
|
|
190
|
+
found.push({ file: child.file, kind: child.review ? 'review' : 'subagent' });
|
|
191
|
+
waiting.push(...(childrenOf.get(child.id) ?? []));
|
|
192
|
+
}
|
|
193
|
+
if (found.length > 0) onRecord.set(id, { id, harness: 'codex', subagent: false, ...own, children: found });
|
|
194
|
+
}
|
|
195
|
+
|
|
141
196
|
// Two entries can name the same conversation, one resumed after its session
|
|
142
197
|
// was retired, and one entry can name it twice, now and in its history: it is
|
|
143
198
|
// counted once, in the first block asked for that names it.
|
|
@@ -155,7 +210,9 @@ function forBot(bots, name, onlySession, window) {
|
|
|
155
210
|
.filter(({ name }) => onlySession === undefined || name === onlySession)
|
|
156
211
|
.map(({ name, entry, ...was }) => ({ name, ...('retired' in was ? { retired: was.retired } : {}), ...block(entry) }));
|
|
157
212
|
|
|
158
|
-
|
|
213
|
+
// What any bot's book names, or any claim gathered in, is not unclaimed here,
|
|
214
|
+
// wherever its record lies (#449 review).
|
|
215
|
+
const unclaimed = all([...onRecord.values()].filter((one) => !fleet.claimed.has(one.id) && !fleet.attached.has(one.id)), window);
|
|
159
216
|
|
|
160
217
|
return { bot: name, home, sessions, unclaimed: unclaimed.conversations, unclaimed_not_counted: unclaimed.gaps };
|
|
161
218
|
}
|
|
@@ -187,6 +244,7 @@ function counted(one, window) {
|
|
|
187
244
|
const tally = {
|
|
188
245
|
calls: 0,
|
|
189
246
|
subagentCalls: 0,
|
|
247
|
+
reviewCalls: 0,
|
|
190
248
|
compactions: 0,
|
|
191
249
|
tokens: Object.fromEntries(KINDS.map((kind) => [kind, 0])),
|
|
192
250
|
models: new Set(),
|
|
@@ -203,7 +261,10 @@ function counted(one, window) {
|
|
|
203
261
|
// own, and what they spent is the conversation's.
|
|
204
262
|
const subagents = one.harness === 'claude' ? claudeSubagentTranscripts(one.file) : { files: [], unreadable: 0 };
|
|
205
263
|
tally.gaps.unreadable_transcripts += subagents.unreadable;
|
|
206
|
-
|
|
264
|
+
// A Codex conversation the book names, whose own record is gone but whose
|
|
265
|
+
// subagents' are not: its own calls cannot be counted, and that is said (#449).
|
|
266
|
+
if (one.file === undefined) tally.gaps.unreadable_transcripts += 1;
|
|
267
|
+
const sources = [...(one.file === undefined ? [] : [one.file]), ...subagents.files]
|
|
207
268
|
.map((file, index) => {
|
|
208
269
|
const { entries, unreadable, broken } = transcript(file, countable);
|
|
209
270
|
tally.gaps.unreadable_transcripts += unreadable;
|
|
@@ -212,8 +273,19 @@ function counted(one, window) {
|
|
|
212
273
|
});
|
|
213
274
|
if (one.harness === 'claude') fromClaude(sources, window, tally);
|
|
214
275
|
else {
|
|
215
|
-
|
|
216
|
-
|
|
276
|
+
if (one.file !== undefined) {
|
|
277
|
+
const copied = originOf(sources[0].entries, tally);
|
|
278
|
+
if (copied !== null) fromCodex(sources[0].entries, window, tally, copied);
|
|
279
|
+
}
|
|
280
|
+
// Each subagent's rollout keeps its own running total, so each is counted
|
|
281
|
+
// on its own, from its own records (#449).
|
|
282
|
+
for (const child of one.children ?? []) {
|
|
283
|
+
const { entries, unreadable, broken } = transcript(child.file, countable);
|
|
284
|
+
tally.gaps.unreadable_transcripts += unreadable;
|
|
285
|
+
tally.gaps.broken_lines += broken;
|
|
286
|
+
const itsCopied = originOf(entries, tally);
|
|
287
|
+
if (itsCopied !== null) fromCodex(entries, window, tally, itsCopied, child.kind);
|
|
288
|
+
}
|
|
217
289
|
}
|
|
218
290
|
if (tally.calls === 0 && KINDS.every((kind) => tally.tokens[kind] === 0)) return { gaps: tally.gaps };
|
|
219
291
|
|
|
@@ -221,6 +293,7 @@ function counted(one, window) {
|
|
|
221
293
|
id: one.id,
|
|
222
294
|
calls: tally.calls,
|
|
223
295
|
subagent_calls: tally.subagentCalls,
|
|
296
|
+
review_calls: tally.reviewCalls,
|
|
224
297
|
tokens: tally.tokens,
|
|
225
298
|
by_model: [...tally.byModel].map(([model, its]) => ({ model, calls: its.calls, tokens: its.tokens })),
|
|
226
299
|
models: [...tally.models],
|
|
@@ -309,7 +382,7 @@ function fromClaude(sources, window, tally) {
|
|
|
309
382
|
if (!isNew && KINDS.every((kind) => grew[kind] === 0)) continue;
|
|
310
383
|
|
|
311
384
|
const subagent = records.every((record) => record.subagent);
|
|
312
|
-
count(tally, grew, atEnd.entry.message?.model, atEnd.entry.effort, isNew ? made : atEnd.when, isNew ? 1 : 0, subagent);
|
|
385
|
+
count(tally, grew, atEnd.entry.message?.model, atEnd.entry.effort, isNew ? made : atEnd.when, isNew ? 1 : 0, subagent ? 'subagent' : undefined);
|
|
313
386
|
}
|
|
314
387
|
}
|
|
315
388
|
|
|
@@ -380,7 +453,7 @@ const CODEX_FIELDS = [
|
|
|
380
453
|
* A leading record with a figure missing may be a copy or the fork's own call;
|
|
381
454
|
* it is not counted, and is said to be.
|
|
382
455
|
*/
|
|
383
|
-
function fromCodex(entries, window, tally, copied) {
|
|
456
|
+
function fromCodex(entries, window, tally, copied, kind) {
|
|
384
457
|
let model;
|
|
385
458
|
let effort;
|
|
386
459
|
let running;
|
|
@@ -451,7 +524,7 @@ function fromCodex(entries, window, tally, copied) {
|
|
|
451
524
|
continue;
|
|
452
525
|
}
|
|
453
526
|
|
|
454
|
-
count(tally, used, model, effort, when);
|
|
527
|
+
count(tally, used, model, effort, when, 1, kind);
|
|
455
528
|
}
|
|
456
529
|
}
|
|
457
530
|
|
|
@@ -546,9 +619,10 @@ function kindsOf(raw) {
|
|
|
546
619
|
* the total that has to be complete, and a row headed by nothing would be worse
|
|
547
620
|
* than no row.
|
|
548
621
|
*/
|
|
549
|
-
function count(tally, used, model, effort, when, calls = 1,
|
|
622
|
+
function count(tally, used, model, effort, when, calls = 1, kind = undefined) {
|
|
550
623
|
tally.calls += calls;
|
|
551
|
-
if (subagent) tally.subagentCalls += calls;
|
|
624
|
+
if (kind === 'subagent') tally.subagentCalls += calls;
|
|
625
|
+
if (kind === 'review') tally.reviewCalls += calls;
|
|
552
626
|
for (const kind of KINDS) tally.tokens[kind] += used[kind];
|
|
553
627
|
add(tally.models, model);
|
|
554
628
|
add(tally.efforts, effort);
|