@assuredloop/orca-bot-kit 0.3.0 → 0.5.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 +29 -3
- package/SETUP.md +3 -2
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +27 -12
- package/src/book.js +3 -0
- package/src/cli.js +110 -21
- package/src/conversations.js +58 -4
- package/src/health.js +124 -23
- package/src/message.js +25 -28
- package/src/orca-window.cjs +21 -0
- package/src/orca.js +117 -10
- package/src/record.js +8 -1
- package/src/retire.js +39 -4
- package/src/rules.js +15 -0
- package/src/settings.js +158 -0
- package/src/skills.js +107 -16
- package/src/up.js +65 -20
- package/src/usage.js +147 -52
package/README.md
CHANGED
|
@@ -263,12 +263,22 @@ name for a directory in `skills/` inside your bots folder, or anything with a
|
|
|
263
263
|
`/` in it for a skill directory anywhere on disk. `obk up` links them before it
|
|
264
264
|
opens a tab, and `bot create` gives a new bot what the lists already name.
|
|
265
265
|
|
|
266
|
+
A bot whose sessions are running gets a changed list without a restart.
|
|
267
|
+
When `skills build` links or takes away a skill, it tells each running
|
|
268
|
+
session of that bot. A Claude Code session gets `/reload-skills` typed into
|
|
269
|
+
its tab: Claude Code's own reload, which a busy session runs when its turn
|
|
270
|
+
ends. Codex has no reload command and takes the change at the start of its
|
|
271
|
+
next turn, so nothing is typed there. The kit prints the new skills'
|
|
272
|
+
`SKILL.md` paths, and says that a restart would make a skill appear if it is
|
|
273
|
+
still missing after that turn. A session the kit could not tell is named,
|
|
274
|
+
with the reason.
|
|
275
|
+
|
|
266
276
|
What lands in the bot is a symlink, never a copy
|
|
267
277
|
([ADR 0014](docs/adr/0014-skills-are-linked-never-copied.md)) — into
|
|
268
278
|
`.claude/skills` and `.agents/skills` in the bot home, which is where each
|
|
269
279
|
harness reads a project's skills from. So a kit skill is read where npm
|
|
270
|
-
installed it,
|
|
271
|
-
|
|
280
|
+
installed it, a running session reads an edited skill the next time it loads
|
|
281
|
+
it, and updating the kit updates every bot at once.
|
|
272
282
|
|
|
273
283
|
The kit takes away only what it can prove it put there. It writes down every
|
|
274
284
|
link it makes, in `.obk-skills.yaml` in the bot folder, and when no list names
|
|
@@ -404,7 +414,10 @@ same refusals, and `obk up` leaves what is paused closed until `obk unpause`
|
|
|
404
414
|
brings it back with its conversations. Retiring a session takes it off the bot
|
|
405
415
|
and keeps its conversations in the book under `retired`. Retiring a bot closes
|
|
406
416
|
its tabs, removes its Orca project and moves its folder to `retired/`; it will
|
|
407
|
-
not touch a bot whose Orca project holds a tab your book does not name.
|
|
417
|
+
not touch a bot whose Orca project holds a tab your book does not name. The
|
|
418
|
+
folder moves only once Orca no longer lists the project; if Orca still lists it,
|
|
419
|
+
or its list cannot be read, retire says so and leaves the bot where it is, and
|
|
420
|
+
retiring it again finishes the job. Bot
|
|
408
421
|
Father itself, and its management session `daily`, are never paused or
|
|
409
422
|
retired; its other sessions are like any bot's.
|
|
410
423
|
|
|
@@ -430,10 +443,23 @@ What it looks for:
|
|
|
430
443
|
go, and a link with nothing at the end of it;
|
|
431
444
|
- **a session the book knows that Orca does not**: its tab was closed, or the
|
|
432
445
|
machine was restarted, and `obk up` brings it back;
|
|
446
|
+
- **a session whose tab is open and whose harness is gone**: only the tab's
|
|
447
|
+
shell is in front, because the harness quit or crashed. `obk up` leaves an
|
|
448
|
+
open tab alone, so the finding names the `obk restart … --session` that
|
|
449
|
+
brings it back; when the book names no conversation for it, restart would
|
|
450
|
+
refuse, and the finding says to close the tab in Orca and run
|
|
451
|
+
`obk up … --session`. A tab the kit cannot read is never called down;
|
|
433
452
|
- **leftovers no book owns**: an Orca project inside your bots folder with no
|
|
434
453
|
bot in it, a tab in a bot's project that the book does not name, a start
|
|
435
454
|
prompt written for a session that has gone, a clone of a skills source you no
|
|
436
455
|
longer list, and the conversations the kit found and would not assign;
|
|
456
|
+
- **a session running on something other than its bot asks for**: a model,
|
|
457
|
+
effort, context or approval level that the harness's own record of the
|
|
458
|
+
conversation says differs from `bot.yaml`, and a session that started before
|
|
459
|
+
its bot's `AGENTS.md` last changed, with what brings it up to date. Beside the
|
|
460
|
+
findings, each running session gets a line saying, for each setting, whether
|
|
461
|
+
it matches, differs, was not asked for, or cannot be told from the record;
|
|
462
|
+
cannot be told is never taken for a match;
|
|
437
463
|
- **Orca's own default launch arguments**. Orca adds these to the agents it
|
|
438
464
|
launches, relaunches and resumes, so when they carry a permission bypass every
|
|
439
465
|
session runs in that mode whatever the kit asked for. The kit never sets that
|
package/SETUP.md
CHANGED
|
@@ -145,8 +145,9 @@ different answers.
|
|
|
145
145
|
|
|
146
146
|
`obk init` tells you which, for every tab it opened:
|
|
147
147
|
|
|
148
|
-
- *the harness was typed in and
|
|
149
|
-
|
|
148
|
+
- *the harness was typed in and is running. The kit cannot see whether a screen
|
|
149
|
+
in it is waiting for an answer* — look at it: Orca does not flag every first-run
|
|
150
|
+
screen, so it may still be asking something of its own. That is the table below.
|
|
150
151
|
- *the harness was typed in and came up, waiting on: …* — running and blocked on
|
|
151
152
|
a question. The table below.
|
|
152
153
|
- *the harness was typed in, and no session came up in the tab* — **the line did
|
package/package.json
CHANGED
|
@@ -273,24 +273,39 @@ again and check a session is running before you call the bot ready.
|
|
|
273
273
|
## When the bot is already running
|
|
274
274
|
|
|
275
275
|
Changing a bot whose sessions are up is the ordinary case rather than the
|
|
276
|
-
awkward one.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
awkward one. Putting a skill on a bot or taking one off reaches its running
|
|
277
|
+
sessions without a restart, and `obk skills build` says, session by session,
|
|
278
|
+
how:
|
|
279
|
+
|
|
280
|
+
- Claude Code: the kit types `/reload-skills` into the session's tab. That is
|
|
281
|
+
Claude Code's own reload. A busy session queues it and runs it as a command
|
|
282
|
+
when its turn ends, so it never reaches the model as text.
|
|
283
|
+
- Codex: nothing is typed, because Codex has no reload command. It takes the
|
|
284
|
+
change at the start of its next turn, not in the middle of the one it is on.
|
|
285
|
+
|
|
286
|
+
The kit reports a session it could not tell, and why: not up, or on a screen
|
|
287
|
+
waiting for an answer. A session that is not up reads its skills when it
|
|
288
|
+
starts.
|
|
289
|
+
|
|
290
|
+
Told is not the same as confirmed. Before telling anyone a skill is there,
|
|
291
|
+
check that the session can use it: ask it to load the skill. Do not look
|
|
292
|
+
again at the disk you just wrote, since that says nothing about what another
|
|
293
|
+
process has loaded. Do not ask a Codex session whether the skill is in its
|
|
294
|
+
list: it has answered "none" while its list held the skill. Where a skill has
|
|
295
|
+
not appeared after the next turn, say so. A restart would make it appear, and
|
|
296
|
+
meanwhile the session can read the skill's `SKILL.md` by the path the kit
|
|
297
|
+
printed. A restart is the user's to ask for. Taking one because it is quick
|
|
298
|
+
ends a conversation they were in the middle of.
|
|
287
299
|
|
|
288
300
|
Instructions are not skills. A session is working from the rules it read when
|
|
289
301
|
it started, so a change to a bot's `AGENTS.md` may not reach one that is
|
|
290
302
|
already up even when a skill change would, and a session's model, effort or
|
|
291
303
|
approval only changes when it is started again (`obk restart`, on the user's
|
|
292
304
|
word, and then read its tabs as above). Say which you changed, and what that
|
|
293
|
-
means for the sessions running now.
|
|
305
|
+
means for the sessions running now. `obk health` names each running session
|
|
306
|
+
still on the older rules or on other settings, and what brings it up to date: on
|
|
307
|
+
Claude Code a `/clear` reads the rules again, and a start does on both harnesses.
|
|
308
|
+
Where everything needs restarting, remind
|
|
294
309
|
them to do it from Bot Father's ops tab.
|
|
295
310
|
|
|
296
311
|
And tell them. A bot's sessions share its rules and its skills, so a change
|
package/src/book.js
CHANGED
|
@@ -25,6 +25,9 @@ const HEADER = `# What Orca calls this bot on this machine, and where each of it
|
|
|
25
25
|
# and kept, because an address that is a tab dies with the tab. \`address\` is what
|
|
26
26
|
# a Claude session is called, which is what another Claude session writes to.
|
|
27
27
|
#
|
|
28
|
+
# \`rules\` is a stamp of the bot's AGENTS.md as the session last read it, so a
|
|
29
|
+
# change to it after the session started can be told apart.
|
|
30
|
+
#
|
|
28
31
|
# \`retired\` holds what the book knew about each session \`obk retire\` took off
|
|
29
32
|
# the bot, with when, so the conversations it had are still accounted for.
|
|
30
33
|
`;
|
package/src/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { checkHealth, orcaSettingFindings } from './health.js';
|
|
|
18
18
|
import { initBots } from './init.js';
|
|
19
19
|
import { APPROVALS, HARNESSES, ownCli, shellWord, workDirOf } from './launch.js';
|
|
20
20
|
import { checkMail, lookUp, sendMessage } from './message.js';
|
|
21
|
-
import { orcaCli, orcaTrouble } from './orca.js';
|
|
21
|
+
import { orcaCli, orcaTrouble, RELOAD_LINE } from './orca.js';
|
|
22
22
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
23
23
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
24
24
|
import { restartSessions } from './restart.js';
|
|
@@ -83,7 +83,8 @@ Usage:
|
|
|
83
83
|
a bot: close its tabs, remove its Orca project and
|
|
84
84
|
move its folder to retired/. It will not retire a
|
|
85
85
|
bot whose Orca project holds a tab your book does
|
|
86
|
-
not name
|
|
86
|
+
not name, and moves the folder only once Orca no
|
|
87
|
+
longer lists the project.
|
|
87
88
|
obk rules build --bots <path> [--bot <bot>]
|
|
88
89
|
Build every bot's AGENTS.md from its charter and
|
|
89
90
|
the rule units it carries, or just the one you
|
|
@@ -103,7 +104,10 @@ Usage:
|
|
|
103
104
|
skills folder, or any path, and take away a link
|
|
104
105
|
it made that no list names any more. What you put
|
|
105
106
|
in a bot's skills directory yourself is left alone
|
|
106
|
-
and shown as yours.
|
|
107
|
+
and shown as yours. A bot whose links changed has
|
|
108
|
+
its running sessions told: Claude Code gets
|
|
109
|
+
/reload-skills typed in, and Codex takes the
|
|
110
|
+
change at its next turn.
|
|
107
111
|
obk skills fetch --bots <path> [--source <name>]
|
|
108
112
|
Clone the online sources skills.yaml lists, beside
|
|
109
113
|
your bots folder and never inside it, each at the
|
|
@@ -425,19 +429,19 @@ const commands = {
|
|
|
425
429
|
const seeded = initBots(bots, values.harness);
|
|
426
430
|
// The typed path was for making the folder and naming it in a refusal; from
|
|
427
431
|
// here on it is the fleet, and the fleet is its real path (#164).
|
|
428
|
-
const { tabs, rules, skills, paused } = await bringUp(sameFleet(seeded.bots), { bot: BOT_FATHER });
|
|
432
|
+
const { tabs, rules, skills, paused, projects } = await bringUp(sameFleet(seeded.bots), { bot: BOT_FATHER });
|
|
429
433
|
// Setup is the other place the PRD asks for Orca's own launch arguments to
|
|
430
434
|
// be looked at (6.5), and the one where the user is still standing in front
|
|
431
435
|
// of the fleet they are making. Only that one check: a folder init has just
|
|
432
436
|
// made has nothing else to say about itself.
|
|
433
|
-
const answer = { bots: seeded.bots, created: seeded.created, completed: seeded.completed, rules, skills, tabs, paused, found: orcaSettingFindings() };
|
|
437
|
+
const answer = { bots: seeded.bots, created: seeded.created, completed: seeded.completed, rules, skills, tabs, paused, projects, found: orcaSettingFindings() };
|
|
434
438
|
return { answer, lines: tabLines(answer, `Bot Father is up in Orca. Your bots folder: ${seeded.bots}`) };
|
|
435
439
|
},
|
|
436
440
|
|
|
437
441
|
async up(bots, values) {
|
|
438
442
|
refuseWhenOrcaIsDown();
|
|
439
|
-
const { tabs, rules, skills, paused } = await bringUp(bots, { bot: values.bot, session: values.session });
|
|
440
|
-
const answer = { bots, created: [], completed: [], rules, skills, tabs, paused };
|
|
443
|
+
const { tabs, rules, skills, paused, projects } = await bringUp(bots, { bot: values.bot, session: values.session });
|
|
444
|
+
const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, projects };
|
|
441
445
|
const up = [...new Set(tabs.map((tab) => tab.bot))];
|
|
442
446
|
const summary = up.length === 0
|
|
443
447
|
? `Nothing was brought up in Orca. Your bots folder: ${bots}`
|
|
@@ -447,11 +451,12 @@ const commands = {
|
|
|
447
451
|
|
|
448
452
|
health(bots, values) {
|
|
449
453
|
refuseWhenOrcaIsDown();
|
|
450
|
-
const found = checkHealth(bots, { bot: values.bot });
|
|
454
|
+
const { found, sessions } = checkHealth(bots, { bot: values.bot });
|
|
451
455
|
return {
|
|
452
|
-
answer: { bots, found },
|
|
456
|
+
answer: { bots, found, sessions },
|
|
453
457
|
lines: [
|
|
454
458
|
...foundLines(found),
|
|
459
|
+
...sessions.map(settingsLine),
|
|
455
460
|
found.length === 0
|
|
456
461
|
? `Nothing to report: everything the kit keeps is where it should be. Your bots folder: ${bots}`
|
|
457
462
|
: `${found.length} thing${found.length === 1 ? '' : 's'} to look at above. What to do about each is yours to decide. Your bots folder: ${bots}`,
|
|
@@ -464,8 +469,8 @@ const commands = {
|
|
|
464
469
|
|
|
465
470
|
async restart(bots, values) {
|
|
466
471
|
refuseWhenOrcaIsDown();
|
|
467
|
-
const { closed, tabs, rules, skills, paused } = await restartSessions(bots, { bot: values.bot, session: values.session });
|
|
468
|
-
const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, closed };
|
|
472
|
+
const { closed, tabs, rules, skills, paused, projects } = await restartSessions(bots, { bot: values.bot, session: values.session });
|
|
473
|
+
const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, projects, closed };
|
|
469
474
|
const what = values.session === undefined ? values.bot : `${values.bot} ${values.session}`;
|
|
470
475
|
return {
|
|
471
476
|
answer,
|
|
@@ -518,11 +523,25 @@ const commands = {
|
|
|
518
523
|
}
|
|
519
524
|
|
|
520
525
|
const retired = await retireBot(bots, { bot: values.bot });
|
|
526
|
+
if (retired.trouble !== undefined) {
|
|
527
|
+
return {
|
|
528
|
+
answer: { bots, ...retired },
|
|
529
|
+
lines: [
|
|
530
|
+
...closedLines(retired.closed),
|
|
531
|
+
`${'trouble'.padEnd(9)} Orca project ${retired.project}`,
|
|
532
|
+
` ${retired.trouble}`,
|
|
533
|
+
`Retire it again: ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${retired.bot}`,
|
|
534
|
+
],
|
|
535
|
+
code: 1,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
521
538
|
return {
|
|
522
539
|
answer: { bots, ...retired },
|
|
523
540
|
lines: [
|
|
524
541
|
...closedLines(retired.closed),
|
|
525
|
-
|
|
542
|
+
// After every removal, whatever the window was told: Orca's window can
|
|
543
|
+
// keep a removed project's row even after it re-reads (stablyai/orca#20102).
|
|
544
|
+
...(retired.project === undefined ? [] : [`removed Orca project ${retired.project}`, RELOAD_LINE]),
|
|
526
545
|
`retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
|
|
527
546
|
],
|
|
528
547
|
};
|
|
@@ -938,12 +957,15 @@ function usageLines(usage, bots) {
|
|
|
938
957
|
lines.push(`${'bot'.padEnd(9)} ${entry.bot}`);
|
|
939
958
|
for (const session of entry.sessions) {
|
|
940
959
|
lines.push(`${'session'.padEnd(9)} ${session.name}`);
|
|
960
|
+
const notCounted = notCountedLine(session.not_counted);
|
|
941
961
|
lines.push(...session.conversations.map(conversationLine));
|
|
942
|
-
if (session.conversations.length === 0) lines.push(' nothing on record');
|
|
962
|
+
if (session.conversations.length === 0 && notCounted.length === 0) lines.push(' nothing on record');
|
|
963
|
+
lines.push(...notCounted);
|
|
943
964
|
}
|
|
944
|
-
if (entry.unclaimed.length > 0) {
|
|
965
|
+
if (entry.unclaimed.length > 0 || notCountedLine(entry.unclaimed_not_counted).length > 0) {
|
|
945
966
|
lines.push(`${'unclaimed'.padEnd(9)} ${entry.bot}: no session of this bot claims these`);
|
|
946
967
|
lines.push(...entry.unclaimed.map(conversationLine));
|
|
968
|
+
lines.push(...notCountedLine(entry.unclaimed_not_counted));
|
|
947
969
|
}
|
|
948
970
|
}
|
|
949
971
|
|
|
@@ -953,6 +975,19 @@ function usageLines(usage, bots) {
|
|
|
953
975
|
return lines;
|
|
954
976
|
}
|
|
955
977
|
|
|
978
|
+
/** What could not be counted, as one line, or no line when everything was. */
|
|
979
|
+
function notCountedLine(gaps) {
|
|
980
|
+
const said = [
|
|
981
|
+
[gaps.unreadable_transcripts, 'transcript', 'transcripts', 'unreadable'],
|
|
982
|
+
[gaps.broken_lines, 'line', 'lines', 'broken'],
|
|
983
|
+
[gaps.records_without_numbers, 'record', 'records', 'with no usable number'],
|
|
984
|
+
[gaps.records_without_time, 'record', 'records', 'with no time'],
|
|
985
|
+
]
|
|
986
|
+
.filter(([count]) => count > 0)
|
|
987
|
+
.map(([count, one, many, why]) => `${count} ${count === 1 ? one : many} ${why}`);
|
|
988
|
+
return said.length === 0 ? [] : [` not counted: ${said.join(', ')}`];
|
|
989
|
+
}
|
|
990
|
+
|
|
956
991
|
/** One conversation: what it is, what it ran as, and what it used. */
|
|
957
992
|
function conversationLine(one) {
|
|
958
993
|
const ran = [...one.models, ...one.efforts].join(' ');
|
|
@@ -1084,9 +1119,24 @@ function skillsLines(skills) {
|
|
|
1084
1119
|
...(entry.trouble === undefined ? [] : [` ${'trouble'.padEnd(9)} ${entry.trouble}`]),
|
|
1085
1120
|
...entry.skills.map((skill) => ` ${(skill.managed ? 'linked' : 'yours').padEnd(9)} ${skill.name.padEnd(24)} ${skill.from ?? 'no list names it; the kit leaves it alone'}`),
|
|
1086
1121
|
...(entry.removed ?? []).map((name) => ` ${'removed'.padEnd(9)} ${name.padEnd(24)} no list names it now`),
|
|
1122
|
+
...(entry.sessions ?? []).map((one) => toldLine(entry.bot, one)),
|
|
1087
1123
|
]);
|
|
1088
1124
|
}
|
|
1089
1125
|
|
|
1126
|
+
/** What `skills build` did about one running session of a bot whose skills changed. */
|
|
1127
|
+
function toldLine(bot, { session, harness, state, read, blocked, trouble }) {
|
|
1128
|
+
const who = `${bot}/${session}`;
|
|
1129
|
+
const after = harness === 'claude' ? 'Once that is settled, type /reload-skills in its tab.' : 'Its next turn takes the change by itself.';
|
|
1130
|
+
const [word, says] = {
|
|
1131
|
+
reloaded: ['reloaded', '/reload-skills was typed into its tab; a busy session runs it when its turn ends'],
|
|
1132
|
+
'next-turn': ['next turn', `Codex takes the change at the start of its next turn. If a skill is not there then, a restart makes it appear${read?.length > 0 ? `; meanwhile it can read ${read.join(', ')}` : ''}`],
|
|
1133
|
+
'not-up': ['not told', 'not up; it reads its skills when it starts'],
|
|
1134
|
+
blocked: ['not told', `its tab is waiting for an answer (${blocked}), so nothing was typed. ${after}`],
|
|
1135
|
+
unknown: ['not told', `${trouble}. ${after}`],
|
|
1136
|
+
}[state];
|
|
1137
|
+
return ` ${word.padEnd(9)} ${who.padEnd(24)} ${says}`;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1090
1140
|
/**
|
|
1091
1141
|
* What a check found, two lines each: what kind of trouble it is and the one
|
|
1092
1142
|
* thing to go and look at, then the sentence about it. The same shape wherever
|
|
@@ -1095,7 +1145,26 @@ function skillsLines(skills) {
|
|
|
1095
1145
|
const foundLines = (found) =>
|
|
1096
1146
|
found.flatMap((one) => [`${one.kind.padEnd(9)} ${one.where}`, ` ${one.says}`]);
|
|
1097
1147
|
|
|
1098
|
-
|
|
1148
|
+
/**
|
|
1149
|
+
* What one running session runs on, in a line: each setting's state, with what
|
|
1150
|
+
* was asked for and what the harness recorded, and its rules.
|
|
1151
|
+
*/
|
|
1152
|
+
function settingsLine({ bot, session, running, settings, rules }) {
|
|
1153
|
+
const each = Object.entries(settings).map(([name, one]) => {
|
|
1154
|
+
const values = [
|
|
1155
|
+
...(one.configured === undefined ? [] : [`asked ${one.configured}`]),
|
|
1156
|
+
...(one.observed === undefined ? [] : [`runs ${one.observed}`]),
|
|
1157
|
+
];
|
|
1158
|
+
return `${name} ${one.state}${values.length === 0 ? '' : ` (${values.join(', ')})`}`;
|
|
1159
|
+
});
|
|
1160
|
+
// A session whose harness the kit cannot see in its tab is not called running.
|
|
1161
|
+
const [word, who] = running === 'yes'
|
|
1162
|
+
? ['running', `${bot} ${session}`]
|
|
1163
|
+
: ['unsure', `${bot} ${session} cannot tell whether its harness is running:`];
|
|
1164
|
+
return `${word.padEnd(9)} ${who} ${each.join(' ')} rules ${rules.state}`;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function tabLines({ bots, created, completed, rules, skills, tabs, paused = [], projects = [], found = [] }, summary) {
|
|
1099
1168
|
const lines = [
|
|
1100
1169
|
...created.map((entry) => `created ${entry}`),
|
|
1101
1170
|
...completed.map((entry) => `completed ${entry}`),
|
|
@@ -1115,7 +1184,10 @@ function tabLines({ bots, created, completed, rules, skills, tabs, paused = [],
|
|
|
1115
1184
|
|
|
1116
1185
|
// Last before the summary, because what a check found is about the setup the
|
|
1117
1186
|
// run has just left behind rather than about any one thing it did.
|
|
1118
|
-
lines.push(...foundLines(found)
|
|
1187
|
+
lines.push(...foundLines(found));
|
|
1188
|
+
// Hedged, because nothing here can see whether the window was told (#224).
|
|
1189
|
+
if (projects.length > 0) lines.push(RELOAD_LINE);
|
|
1190
|
+
lines.push(summary);
|
|
1119
1191
|
return lines;
|
|
1120
1192
|
}
|
|
1121
1193
|
|
|
@@ -1141,11 +1213,21 @@ function harnessLines(tab, bots) {
|
|
|
1141
1213
|
' the harness was typed in, and no session came up in the tab.',
|
|
1142
1214
|
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1143
1215
|
...ANSWER_IT,
|
|
1216
|
+
...promptLines(tab),
|
|
1144
1217
|
];
|
|
1145
1218
|
}
|
|
1146
1219
|
|
|
1220
|
+
// Orca naming no reason does not mean nothing is on screen: on 1.4.209 it
|
|
1221
|
+
// names none for Codex's trust and hooks screens, nor for Claude Code's trust
|
|
1222
|
+
// list (tech notes, section 1, #288). So the kit says what it cannot see.
|
|
1147
1223
|
const lines = tab.blockedReason === undefined
|
|
1148
|
-
? [
|
|
1224
|
+
? [
|
|
1225
|
+
...how,
|
|
1226
|
+
' the harness was typed in and is running. The kit cannot see whether a screen',
|
|
1227
|
+
' in it is waiting for an answer.',
|
|
1228
|
+
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1229
|
+
...ANSWER_IT,
|
|
1230
|
+
]
|
|
1149
1231
|
: [
|
|
1150
1232
|
...how,
|
|
1151
1233
|
` the harness was typed in and came up, waiting on: ${tab.blockedReason}`,
|
|
@@ -1153,14 +1235,21 @@ function harnessLines(tab, bots) {
|
|
|
1153
1235
|
...ANSWER_IT,
|
|
1154
1236
|
];
|
|
1155
1237
|
|
|
1156
|
-
|
|
1157
|
-
if (tab.promptSent === false) {
|
|
1158
|
-
lines.push(' the start prompt was not typed in: the tab was not ready for it.');
|
|
1159
|
-
}
|
|
1238
|
+
lines.push(...promptLines(tab));
|
|
1160
1239
|
if (tab.unclaimed !== undefined) lines.push(...unclaimedLines(tab, bots));
|
|
1161
1240
|
return lines;
|
|
1162
1241
|
}
|
|
1163
1242
|
|
|
1243
|
+
/**
|
|
1244
|
+
* Whether the session got its start prompt, as its own record has it (#274):
|
|
1245
|
+
* received, or not confirmed yet. Nothing for a session that was told nothing.
|
|
1246
|
+
*/
|
|
1247
|
+
function promptLines(tab) {
|
|
1248
|
+
if (tab.promptReceived === true) return [' the start prompt was received: the session\'s own record holds it.'];
|
|
1249
|
+
if (tab.promptReceived === false) return [' the start prompt is not confirmed: the session\'s own record does not hold it yet.'];
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1164
1253
|
try {
|
|
1165
1254
|
process.exitCode = await run(process.argv.slice(2));
|
|
1166
1255
|
} catch (error) {
|
package/src/conversations.js
CHANGED
|
@@ -32,12 +32,17 @@ const codexDir = () => path.join(homedir(), '.codex', 'sessions');
|
|
|
32
32
|
* then that it does not know, which is the honest one.
|
|
33
33
|
*/
|
|
34
34
|
export function conversationsIn(harness, home, since) {
|
|
35
|
-
|
|
35
|
+
// A subagent's conversation ran in the folder too, but no session had it: the
|
|
36
|
+
// harness says it is a helper's, so it is never offered to one to claim.
|
|
37
|
+
return transcriptsIn(harness, home, since)
|
|
38
|
+
.filter((one) => !one.subagent)
|
|
39
|
+
.map((one) => ({ id: one.id, at: one.at }));
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
39
43
|
* The same conversations, each with the file the harness keeps it in, for a
|
|
40
|
-
* caller that has to read what is inside one rather than only know it is there
|
|
44
|
+
* caller that has to read what is inside one rather than only know it is there,
|
|
45
|
+
* and whether the harness marks it as a subagent's.
|
|
41
46
|
*/
|
|
42
47
|
export function transcriptsIn(harness, home, since) {
|
|
43
48
|
const from = since === undefined ? 0 : Date.parse(since);
|
|
@@ -45,7 +50,47 @@ export function transcriptsIn(harness, home, since) {
|
|
|
45
50
|
return found
|
|
46
51
|
.filter((one) => Number.isNaN(from) || one.at >= from)
|
|
47
52
|
.sort((left, right) => left.at - right.at)
|
|
48
|
-
.map((one) => ({ id: one.id, at: new Date(one.at).toISOString(), file: one.file }));
|
|
53
|
+
.map((one) => ({ id: one.id, at: new Date(one.at).toISOString(), file: one.file, subagent: one.subagent === true }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether a conversation's own record holds `text` as a turn of the user's:
|
|
58
|
+
* what the harness itself wrote down as said to it, not a screen that was up.
|
|
59
|
+
*
|
|
60
|
+
* Only the user's turns count. Claude Code writes them as `user` lines, and its
|
|
61
|
+
* own meta lines and tool results in that same shape are not the user's. Codex
|
|
62
|
+
* writes each as a `user` message item; its `user_message` event is missing
|
|
63
|
+
* from many rollouts, and the item is in every one (tech notes, section 3).
|
|
64
|
+
* Its AGENTS.md goes in as a `user` item too, and cannot be the text asked about.
|
|
65
|
+
*/
|
|
66
|
+
export function heldAsUserTurn(harness, file, text) {
|
|
67
|
+
let lines;
|
|
68
|
+
try {
|
|
69
|
+
lines = readFileSync(file, 'utf8').split('\n');
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const wanted = text.trim();
|
|
74
|
+
return lines.some((line) => userTexts(harness, line).some((said) => said.trim() === wanted));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The texts of one record line, when it is a turn of the user's. */
|
|
78
|
+
function userTexts(harness, line) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(line);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (harness === 'claude') {
|
|
86
|
+
if (parsed?.type !== 'user' || parsed.isMeta === true) return [];
|
|
87
|
+
const content = parsed.message?.content;
|
|
88
|
+
if (typeof content === 'string') return [content];
|
|
89
|
+
return Array.isArray(content) ? content.filter((block) => block?.type === 'text').map((block) => String(block.text)) : [];
|
|
90
|
+
}
|
|
91
|
+
const item = parsed?.type === 'response_item' ? parsed.payload : undefined;
|
|
92
|
+
if (item?.type !== 'message' || item.role !== 'user' || !Array.isArray(item.content)) return [];
|
|
93
|
+
return item.content.filter((block) => block?.type === 'input_text').map((block) => String(block.text));
|
|
49
94
|
}
|
|
50
95
|
|
|
51
96
|
function claudeConversations(home) {
|
|
@@ -87,7 +132,7 @@ function codexConversations(home, from) {
|
|
|
87
132
|
.flatMap((file) => {
|
|
88
133
|
const meta = sessionMeta(file);
|
|
89
134
|
return meta?.cwd === home && typeof meta.id === 'string'
|
|
90
|
-
? [{ id: meta.id, at: Date.parse(meta.timestamp ?? '') || startedAt(file) || 0, file }]
|
|
135
|
+
? [{ id: meta.id, at: Date.parse(meta.timestamp ?? '') || startedAt(file) || 0, file, subagent: isSubagent(meta) }]
|
|
91
136
|
: [];
|
|
92
137
|
});
|
|
93
138
|
}
|
|
@@ -113,6 +158,15 @@ function sessionMeta(file) {
|
|
|
113
158
|
}
|
|
114
159
|
}
|
|
115
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Codex marks a conversation it ran as a subagent under `source`: its own
|
|
163
|
+
* auto-review is `{ subagent: { other: 'guardian' } }`, and one a session spawned
|
|
164
|
+
* is `{ subagent: { thread_spawn: … } }`. A session's own is a plain word, such as
|
|
165
|
+
* `cli` or `exec` (tech notes, section 3).
|
|
166
|
+
*/
|
|
167
|
+
const isSubagent = (meta) =>
|
|
168
|
+
meta.source !== null && typeof meta.source === 'object' && 'subagent' in meta.source;
|
|
169
|
+
|
|
116
170
|
/** When a file was last written, for leaving out what is plainly too old. */
|
|
117
171
|
function lastTouched(file) {
|
|
118
172
|
try {
|