@assuredloop/orca-bot-kit 0.27.0 → 0.29.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 +55 -15
- package/SETUP.md +30 -14
- package/package.json +1 -1
- package/rules/mail.md +3 -3
- package/skills/obk-bot-building/SKILL.md +29 -16
- package/skills/obk-grooming/SKILL.md +4 -3
- package/src/book.js +91 -10
- package/src/bot.js +246 -118
- package/src/clear.js +7 -3
- package/src/cli.js +116 -36
- package/src/commands.js +23 -11
- package/src/conversations.js +73 -3
- package/src/groom.js +27 -9
- package/src/health.js +89 -8
- package/src/hooks.js +44 -2
- package/src/init.js +36 -29
- package/src/launch.js +202 -18
- package/src/message.js +42 -2
- package/src/orca-reload.applescript +21 -22
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +47 -14
- package/src/permissions.js +80 -23
- package/src/pi.js +55 -0
- package/src/rules.js +18 -4
- package/src/run.js +155 -0
- package/src/skills.js +50 -34
- package/src/sources.js +73 -4
- package/src/temp.js +65 -20
- package/src/up.js +72 -15
package/src/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { parseArgs } from 'node:util';
|
|
14
14
|
|
|
15
|
-
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
|
|
15
|
+
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, refuseCodexOutside, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
|
|
16
16
|
import { COMMANDS } from './commands.js';
|
|
17
17
|
import { addCommand, groomCommand, grooming, upCommand } from './groom.js';
|
|
18
18
|
import { checkHealth, orcaSettingFindings } from './health.js';
|
|
@@ -21,7 +21,7 @@ import { nameSession } from './name.js';
|
|
|
21
21
|
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
22
22
|
import { checkMail, lookUp, noMailboxYet, sendMessage, sessionInTab } from './message.js';
|
|
23
23
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
24
|
-
import { allowIn, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
24
|
+
import { allowIn, beyondDefaults, botDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
25
25
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
26
26
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
27
27
|
import { restartSessions } from './restart.js';
|
|
@@ -34,6 +34,7 @@ import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
|
34
34
|
import { addSource, fetchSources } from './sources.js';
|
|
35
35
|
import { answerSession, answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks, trustSessionHooks } from './temp.js';
|
|
36
36
|
import { readUsage } from './usage.js';
|
|
37
|
+
import { parseRun, runBounded } from './run.js';
|
|
37
38
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
38
39
|
|
|
39
40
|
const USAGE = `obk — Orca Bot Kit.
|
|
@@ -43,18 +44,19 @@ Usage:
|
|
|
43
44
|
Create your bots folder: a git repo holding your
|
|
44
45
|
bots' configuration, with Bot Father in it, and
|
|
45
46
|
open Bot Father in Orca.
|
|
46
|
-
obk bot create --bots <path> --name <bot> --harness claude|codex
|
|
47
|
+
obk bot create --bots <path> --name <bot> --harness claude|codex|pi
|
|
47
48
|
[--charter <text>]
|
|
48
49
|
Write a new bot in your bots folder. Nothing is
|
|
49
50
|
opened in Orca until you run obk up.
|
|
50
51
|
obk session add --bots <path> --bot <bot> --name <session>
|
|
51
|
-
[--harness claude|codex] [--model <m>] [--effort <e>]
|
|
52
|
+
[--harness claude|codex|pi] [--model <m>] [--effort <e>]
|
|
52
53
|
[--context <c>]
|
|
53
54
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
54
55
|
[--extra-arg=<arg>]
|
|
55
56
|
Add a session to a bot. Anything left out is the
|
|
56
57
|
harness's own default; approval is auto, and only
|
|
57
|
-
obk permission approval changes it. A
|
|
58
|
+
obk permission approval changes it. A session on
|
|
59
|
+
Pi is dangerously-skip, the one level Pi has. A long
|
|
58
60
|
start prompt lives in a file in the bot home, and
|
|
59
61
|
--prompt-file names it.
|
|
60
62
|
A value of your own that starts with a dash is
|
|
@@ -87,15 +89,18 @@ Usage:
|
|
|
87
89
|
the bot's temporary sessions may be made at. A
|
|
88
90
|
running session takes it when it next starts.
|
|
89
91
|
obk session change --bots <path> --bot <bot> --session <session>
|
|
92
|
+
[--harness claude|codex|pi]
|
|
90
93
|
[--model <m>] [--effort <e>] [--context <c>]
|
|
91
94
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
92
95
|
[--extra-arg=<arg>]
|
|
93
96
|
Change a session's settings. What you leave out
|
|
94
97
|
stays as it is; a setting given empty, --model=,
|
|
95
98
|
goes back to the harness's own default. A running
|
|
96
|
-
session takes the change when it next starts.
|
|
97
|
-
|
|
98
|
-
and
|
|
99
|
+
session takes the change when it next starts. On
|
|
100
|
+
another harness it starts a new conversation, the
|
|
101
|
+
book keeps the old one in its history, and model,
|
|
102
|
+
effort and context go back to the new harness's
|
|
103
|
+
defaults unless given too.
|
|
99
104
|
obk pause --bots <path> --bot <bot> [--session <name>]
|
|
100
105
|
Stop a bot, or one of its sessions, for now: close
|
|
101
106
|
its tabs and have obk up leave it closed. The book
|
|
@@ -219,6 +224,8 @@ Usage:
|
|
|
219
224
|
folder and named in the message. A pair the
|
|
220
225
|
harness's own messaging reaches is not carried:
|
|
221
226
|
the address to write to is answered instead.
|
|
227
|
+
In a session's own tab it sends as that session,
|
|
228
|
+
and --from may name only that one.
|
|
222
229
|
obk message check --bots <path> [--bot <bot>] [--session <name>] [--peek]
|
|
223
230
|
Read what is waiting for a session and mark it
|
|
224
231
|
read. --peek leaves it unread. Run in a session's
|
|
@@ -274,7 +281,8 @@ Usage:
|
|
|
274
281
|
only while its tab is up in Orca. --run-on codex
|
|
275
282
|
makes each run a temporary Codex session of the
|
|
276
283
|
grooming session's, launched with --model, --effort
|
|
277
|
-
and --extra-arg, which
|
|
284
|
+
and --extra-arg, which carries the grooming
|
|
285
|
+
session's own prompt, reports back by the kit's
|
|
278
286
|
mail and is retired.
|
|
279
287
|
obk usage --bots <path> [--bot <bot>] [--session <name>] [--since <time>] [--until <time>]
|
|
280
288
|
Say what your sessions have used: the conversations
|
|
@@ -319,14 +327,28 @@ Usage:
|
|
|
319
327
|
For the kit's own launch line, not for typing: run
|
|
320
328
|
in the session's own tab, it gives the session its
|
|
321
329
|
mailbox, or binds the one it has, to that tab.
|
|
330
|
+
obk run --timeout <seconds> -- <command> [<arg> ...]
|
|
331
|
+
Run one command with a hard time limit, with no
|
|
332
|
+
shell, and exit with its exit code. When the time
|
|
333
|
+
runs out, it stops the command and everything it
|
|
334
|
+
started (SIGTERM, then SIGKILL after 3 s), says so
|
|
335
|
+
on stderr, and exits 124. A command it cannot find
|
|
336
|
+
exits 127. A rule can allow it for one command,
|
|
337
|
+
such as Bash(obk run --timeout 300 -- go test:*).
|
|
338
|
+
It needs no --bots, and takes no --json.
|
|
322
339
|
obk --version Print the kit's version.
|
|
323
340
|
obk --help Print this text.
|
|
324
341
|
|
|
325
342
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
326
343
|
that close a tab or take something away say so above. Every bot is allowed
|
|
327
|
-
the kit's commands by default, except obk permission, retire, pause and
|
|
328
|
-
which keep the user's yes.
|
|
344
|
+
the kit's commands by default, except obk permission, retire, pause, init and
|
|
345
|
+
run, which keep the user's yes.
|
|
329
346
|
Add --json to any of them for the same answer as JSON.
|
|
347
|
+
|
|
348
|
+
For another build of Orca, OBK_ORCA names its CLI, and OBK_ORCA_DATA the full
|
|
349
|
+
path of the folder it keeps its data in, such as
|
|
350
|
+
$HOME/Library/Application Support/orca-lean.
|
|
351
|
+
The kit reads Orca's launch arguments from the profiles folder there.
|
|
330
352
|
`;
|
|
331
353
|
|
|
332
354
|
|
|
@@ -374,6 +396,10 @@ function version() {
|
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
async function run(argv) {
|
|
399
|
+
// What follows `obk run --timeout <n> --` is the command's own, flags and
|
|
400
|
+
// all, so it is read by `run` alone, before the kit reads its flags (#532).
|
|
401
|
+
if (argv[0] === 'run') return runBounded(parseRun(argv.slice(1)));
|
|
402
|
+
|
|
377
403
|
const { values, positionals, tokens } = parseArgs({
|
|
378
404
|
args: argv,
|
|
379
405
|
allowPositionals: true,
|
|
@@ -435,7 +461,7 @@ async function run(argv) {
|
|
|
435
461
|
}
|
|
436
462
|
|
|
437
463
|
// A permission rule narrows a command to one bots folder by its first words,
|
|
438
|
-
// whatever comes after them (ADR
|
|
464
|
+
// whatever comes after them (ADR 0050), and the last of two `--bots` would
|
|
439
465
|
// win: so a second one is refused before anything runs.
|
|
440
466
|
if (tokens.filter((token) => token.kind === 'option' && token.name === 'bots').length > 1) {
|
|
441
467
|
throw new Error('--bots is given more than once. Give it once: one command works on one bots folder. Nothing was done.');
|
|
@@ -595,6 +621,8 @@ async function record(bots, bot) {
|
|
|
595
621
|
|
|
596
622
|
const commands = {
|
|
597
623
|
async init(bots, values) {
|
|
624
|
+
// Bot Father on Pi is not in the first slice of #547.
|
|
625
|
+
if (values.harness === 'pi') throw new Error('init does not take --harness pi yet: Bot Father runs on claude or codex, and its other bots may run on Pi. Nothing was written.');
|
|
598
626
|
// Asked before anything is written, so an Orca that is down leaves the disk
|
|
599
627
|
// exactly as it was and the caller can simply run the command again.
|
|
600
628
|
refuseWhenOrcaIsDown();
|
|
@@ -827,7 +855,9 @@ const commands = {
|
|
|
827
855
|
},
|
|
828
856
|
|
|
829
857
|
'bot create'(bots, values) {
|
|
830
|
-
|
|
858
|
+
// The kit's default rules (ADR 0050) are in the bot's first bot.yaml.
|
|
859
|
+
const allow = botDefaults(bots, { harness: values.harness, sessions: [] });
|
|
860
|
+
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter, allow });
|
|
831
861
|
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
|
832
862
|
// that a new bot's file and a rebuilt one are written by the same code.
|
|
833
863
|
const rules = [buildAgents(bots, made.home, readBot(made.home))];
|
|
@@ -838,9 +868,10 @@ const commands = {
|
|
|
838
868
|
// instructions, so `up` will not start it, and saying "give it a session"
|
|
839
869
|
// would send the caller past the thing that needs settling first.
|
|
840
870
|
const trouble = rules[0].trouble !== undefined;
|
|
841
|
-
// The
|
|
842
|
-
// with no instructions gets them at the rules build that gives
|
|
843
|
-
|
|
871
|
+
// The default rules go into the harness's files for a bot that is set up
|
|
872
|
+
// to run: one with no instructions gets them at the rules build that gives
|
|
873
|
+
// it some. They were added by this command, so its report names them.
|
|
874
|
+
const permissions = trouble ? [] : writePermissions(bots, made.home, readBot(made.home)).map((entry) => ({ ...entry, defaults: allow }));
|
|
844
875
|
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills, permissions };
|
|
845
876
|
return {
|
|
846
877
|
answer,
|
|
@@ -862,7 +893,7 @@ const commands = {
|
|
|
862
893
|
throw new Error(`bot change does not change a bot's harness: its sessions' conversations belong to the harness they ran on. To move to ${values.harness}, give it a session on ${values.harness} with obk session add, or retire the bot with obk retire and create a new one.`);
|
|
863
894
|
}
|
|
864
895
|
// Permission changes have commands of their own, which keep the user's yes
|
|
865
|
-
// (ADR
|
|
896
|
+
// (ADR 0051): the old spelling refuses, and changes nothing.
|
|
866
897
|
const moved = [['allow', 'permission allow'], ['disallow', 'permission disallow']].filter(([flag]) => values[flag] !== undefined);
|
|
867
898
|
if (moved.length > 0) {
|
|
868
899
|
throw new Error(`bot change no longer takes ${moved.map(([flag]) => `--${flag}`).join(' or ')}: permission rules change through ${moved.map(([, now]) => `${shellWord(ownCli())} ${now} --bots ${shellWord(bots)} --bot ${values.bot} --rule <rule>`).join(' and ')}, after the user's yes. Nothing was changed.`);
|
|
@@ -1069,7 +1100,7 @@ const commands = {
|
|
|
1069
1100
|
...(values.effort === undefined ? {} : { effort: values.effort }),
|
|
1070
1101
|
...(values['extra-arg'] === undefined ? {} : { extra_args: values['extra-arg'] }),
|
|
1071
1102
|
};
|
|
1072
|
-
// A run's launch may not widen its approval (ADR
|
|
1103
|
+
// A run's launch may not widen its approval (ADR 0051).
|
|
1073
1104
|
if (values['run-on'] === 'codex') refuseApprovalArgs('codex', values['extra-arg']);
|
|
1074
1105
|
const groom = grooming(bots, { at: values.at, ask, run });
|
|
1075
1106
|
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
@@ -1132,7 +1163,8 @@ const commands = {
|
|
|
1132
1163
|
},
|
|
1133
1164
|
|
|
1134
1165
|
async 'message send'(bots, values) {
|
|
1135
|
-
|
|
1166
|
+
// With --from too: in one of the fleet's tabs, the tab says who sends (#570).
|
|
1167
|
+
const tab = callerTab(bots, true);
|
|
1136
1168
|
refuseWhenOrcaIsDown();
|
|
1137
1169
|
const answer = await sendMessage(bots, {
|
|
1138
1170
|
to: values.to,
|
|
@@ -1204,6 +1236,7 @@ const commands = {
|
|
|
1204
1236
|
|
|
1205
1237
|
'session add'(bots, values) {
|
|
1206
1238
|
refuseApproval(bots, values, 'session add');
|
|
1239
|
+
refuseOutsideWorkDir(bots, values);
|
|
1207
1240
|
const added = addSession(bots, values.bot, settingsOf(values));
|
|
1208
1241
|
const found = workDirFound(values, added.bot, added.home, added.session.name);
|
|
1209
1242
|
const answer = { bots, bot: added.bot, home: added.home, session: added.session, found };
|
|
@@ -1233,21 +1266,23 @@ const commands = {
|
|
|
1233
1266
|
},
|
|
1234
1267
|
|
|
1235
1268
|
'session change'(bots, values) {
|
|
1236
|
-
if (values.harness !== undefined) {
|
|
1237
|
-
throw new Error(`session change does not change a session's harness: its conversations belong to the harness they ran on. To move it, retire it with obk retire and add one on ${values.harness} with obk session add.`);
|
|
1238
|
-
}
|
|
1239
1269
|
refuseApproval(bots, values, 'session change');
|
|
1240
1270
|
const { name, ...settings } = settingsOf(values);
|
|
1241
1271
|
const changed = changeSession(bots, values.bot, values.session, settings);
|
|
1242
1272
|
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
|
|
1243
1273
|
const found = workDirFound(values, changed.bot, changed.home, values.session);
|
|
1274
|
+
const { moved } = changed;
|
|
1244
1275
|
return {
|
|
1245
|
-
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart, found },
|
|
1276
|
+
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, ...(moved === undefined ? {} : { moved }), restart, found },
|
|
1246
1277
|
lines: [
|
|
1247
1278
|
`changed session ${values.session} in ${path.join('bots', changed.bot, 'bot.yaml')}`,
|
|
1248
1279
|
...Object.entries(changed.session)
|
|
1249
1280
|
.filter(([key]) => key !== 'name')
|
|
1250
1281
|
.map(([key, value]) => ` ${key} ${oneLine(value)}`),
|
|
1282
|
+
...(moved === undefined ? [] : [
|
|
1283
|
+
`moved from ${moved.from} to ${moved.to}: when it next starts, it starts a new conversation on ${moved.to}, and the book keeps its old one in the session's history.`,
|
|
1284
|
+
...(moved.defaults.length === 0 ? [] : [` ${moved.defaults.join(', ')} went back to ${moved.to}'s own default${moved.defaults.length === 1 ? '' : 's'}.`]),
|
|
1285
|
+
]),
|
|
1251
1286
|
...foundLines(found),
|
|
1252
1287
|
`A running session takes this when it next starts: ${restart}`,
|
|
1253
1288
|
],
|
|
@@ -1290,7 +1325,7 @@ function toLines(answer, bots, where) {
|
|
|
1290
1325
|
// says what to do instead, before the bot needs it (#521).
|
|
1291
1326
|
return [
|
|
1292
1327
|
` Write to ${answer.address} with your own harness's messaging. The kit does not carry that road.`,
|
|
1293
|
-
" If your harness refuses that call, do not send the message again by another road. Tell your maker or your user that it was refused, with the harness's reason. A refusal can mean a missing permission rule
|
|
1328
|
+
" If your harness refuses that call, do not send the message again by another road. Tell your maker or your user that it was refused, with the harness's reason. A refusal can mean a missing permission rule. Send the refusal to whoever adds your rules: your maker, your user or your fleet's manager.",
|
|
1294
1329
|
];
|
|
1295
1330
|
}
|
|
1296
1331
|
return [
|
|
@@ -1306,25 +1341,45 @@ function toLines(answer, bots, where) {
|
|
|
1306
1341
|
* came to when one was asked for (#555).
|
|
1307
1342
|
*/
|
|
1308
1343
|
function sentLines(answer, where) {
|
|
1309
|
-
const road =
|
|
1344
|
+
const road = noticeLines(answer);
|
|
1310
1345
|
if (answer.interrupted === true) {
|
|
1311
1346
|
return [
|
|
1312
|
-
|
|
1347
|
+
...(road.length === 1 ? [road[0].replace('once it is idle', 'now')] : road),
|
|
1313
1348
|
` it was busy, so the kit pressed Escape once in its tab to end its turn.${answer.to.harness === 'codex' ? ' Codex keeps a command it was running as a background terminal after an Escape.' : ''}`,
|
|
1314
1349
|
];
|
|
1315
1350
|
}
|
|
1316
1351
|
if (answer.interrupted === false) {
|
|
1317
|
-
return [road, answer.idle ? ` it is idle, so it was not interrupted.` : ` it was not interrupted: ${answer.interruptTrouble}.`];
|
|
1352
|
+
return [...road, answer.idle ? ` it is idle, so it was not interrupted.` : ` it was not interrupted: ${answer.interruptTrouble}.`];
|
|
1353
|
+
}
|
|
1354
|
+
return road;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Whether Orca's notice will tell the receiver of its letter. It will not while
|
|
1359
|
+
* Orca lists the receiver's tab as orphaned, as after a window restart: the
|
|
1360
|
+
* window has not loaded the tab (#579). A switch to the tab loads it, and is
|
|
1361
|
+
* named as a fact with its cost, not run and not urged: it moves the user's view.
|
|
1362
|
+
*/
|
|
1363
|
+
function noticeLines(answer) {
|
|
1364
|
+
if (answer.orphaned === true) {
|
|
1365
|
+
return [
|
|
1366
|
+
" it is in its Orca mailbox, and it waits there: Orca lists its tab as orphaned, because the Orca window has not loaded that tab, and Orca will not tell it of the letter until the window loads it.",
|
|
1367
|
+
` ${shellWord(orcaCli())} terminal switch --terminal ${shellWord(answer.handle)} loads the tab. It also moves the user's view to that tab, so what the user types can land in it.`,
|
|
1368
|
+
];
|
|
1369
|
+
}
|
|
1370
|
+
if (answer.loadTrouble !== undefined) {
|
|
1371
|
+
return [` it is in its Orca mailbox. The kit could not tell whether the Orca window has loaded its tab (${answer.loadTrouble}), and Orca tells it of the letter only once the window has loaded the tab.`];
|
|
1318
1372
|
}
|
|
1319
|
-
return [
|
|
1373
|
+
return [" it is in its Orca mailbox, and Orca's own notice tells it to look once it is idle."];
|
|
1320
1374
|
}
|
|
1321
1375
|
|
|
1322
1376
|
/**
|
|
1323
1377
|
* What there is to say about the work dir this command was given: one finding
|
|
1324
1378
|
* when it leads out of the bot home, through `..` or a link or as a path
|
|
1325
1379
|
* elsewhere, and nothing otherwise (#222). A bot's work and its clones belong
|
|
1326
|
-
* under its own `work/`.
|
|
1327
|
-
*
|
|
1380
|
+
* under its own `work/`. For a Claude or Pi session it is said, not refused: a
|
|
1381
|
+
* work dir is an instruction there (PRD 6.4), and it is written as given. A
|
|
1382
|
+
* Codex session's is refused before this (refuseCodexOutside).
|
|
1328
1383
|
*/
|
|
1329
1384
|
function workDirFound(values, bot, home, session) {
|
|
1330
1385
|
const workDir = workDirOf({ work_dir: values['work-dir'] }, home);
|
|
@@ -1336,16 +1391,35 @@ function workDirFound(values, bot, home, session) {
|
|
|
1336
1391
|
}];
|
|
1337
1392
|
}
|
|
1338
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Refuse a new Codex session a work dir outside its bot home (ADR 0045). A
|
|
1396
|
+
* changed session is judged in changeSession, on what it writes (#585).
|
|
1397
|
+
*/
|
|
1398
|
+
function refuseOutsideWorkDir(bots, values) {
|
|
1399
|
+
if (values['work-dir'] === undefined) return;
|
|
1400
|
+
let bot;
|
|
1401
|
+
try {
|
|
1402
|
+
bot = readBot(botDir(bots, values.bot), values.bot);
|
|
1403
|
+
} catch {
|
|
1404
|
+
// A bot that is not there is the command's own to refuse.
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const session = { name: values.name, harness: values.harness, work_dir: values['work-dir'] };
|
|
1408
|
+
refuseCodexOutside(values.bot, session, undefined, bot.harness, botDir(bots, values.bot), 'session add');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1339
1411
|
/**
|
|
1340
1412
|
* Refuse what would change a session's approval through `session add` or
|
|
1341
|
-
* `session change` (ADR
|
|
1413
|
+
* `session change` (ADR 0051): `--approval`, and an extra argument that sets
|
|
1342
1414
|
* it on the session's harness. Only `permission approval` changes it.
|
|
1343
1415
|
*/
|
|
1344
1416
|
function refuseApproval(bots, values, command) {
|
|
1345
1417
|
if (values.approval !== undefined) {
|
|
1346
1418
|
throw new Error(`${command} no longer takes --approval: a session's approval changes through ${shellWord(ownCli())} permission approval --bots ${shellWord(bots)} --bot ${values.bot} --session ${values.session ?? values.name} --approval <level>, after the user's yes. Nothing was changed.`);
|
|
1347
1419
|
}
|
|
1348
|
-
|
|
1420
|
+
// A move to another harness keeps the extra arguments the session has, and
|
|
1421
|
+
// one of them can set approval on the new harness (#565).
|
|
1422
|
+
if (values['extra-arg'] === undefined && values.harness === undefined) return;
|
|
1349
1423
|
let bot;
|
|
1350
1424
|
try {
|
|
1351
1425
|
bot = readBot(botDir(bots, values.bot), values.bot);
|
|
@@ -1354,7 +1428,7 @@ function refuseApproval(bots, values, command) {
|
|
|
1354
1428
|
return;
|
|
1355
1429
|
}
|
|
1356
1430
|
const session = bot.sessions.find((one) => one.name === (values.session ?? values.name)) ?? {};
|
|
1357
|
-
refuseApprovalArgs(harnessOf({ ...session, ...(values.harness === undefined ? {} : { harness: values.harness }) }, bot.harness), values['extra-arg']);
|
|
1431
|
+
refuseApprovalArgs(harnessOf({ ...session, ...(values.harness === undefined ? {} : { harness: values.harness }) }, bot.harness), values['extra-arg'] ?? (command === 'session change' ? session.extra_args : undefined));
|
|
1358
1432
|
}
|
|
1359
1433
|
|
|
1360
1434
|
/**
|
|
@@ -1437,6 +1511,12 @@ function refuseWhenOrcaIsDown() {
|
|
|
1437
1511
|
/** The one line that says which conversation this tab was given, and from where. */
|
|
1438
1512
|
const howLines = (tab) => {
|
|
1439
1513
|
if (tab.resumed === true) return [' it was told to resume the session the book holds, with its conversation.'];
|
|
1514
|
+
if (tab.harnessChanged !== undefined) {
|
|
1515
|
+
return [
|
|
1516
|
+
` it was told to start a new session: the book's conversation ${tab.harnessChanged} ran on ${tab.ranOn},`,
|
|
1517
|
+
" and the session has moved from it, so it went into the session's history.",
|
|
1518
|
+
];
|
|
1519
|
+
}
|
|
1440
1520
|
if (tab.noConversation === undefined) return [' it was told to start a new session: the book holds none for this one yet.'];
|
|
1441
1521
|
return [
|
|
1442
1522
|
` it was told to start a new session: the book's conversation ${tab.noConversation} had nothing on record`,
|
|
@@ -1658,7 +1738,7 @@ function rosterLines(roster, bots) {
|
|
|
1658
1738
|
}
|
|
1659
1739
|
|
|
1660
1740
|
lines.push(roster.length === 0
|
|
1661
|
-
? `No bots yet. Make one: ${shellWord(ownCli())} bot create --bots ${shellWord(bots)} --name <name> --harness claude|codex`
|
|
1741
|
+
? `No bots yet. Make one: ${shellWord(ownCli())} bot create --bots ${shellWord(bots)} --name <name> --harness claude|codex|pi`
|
|
1662
1742
|
: `${roster.length} bot${roster.length === 1 ? '' : 's'}. Your bots folder: ${bots}`);
|
|
1663
1743
|
return lines;
|
|
1664
1744
|
}
|
|
@@ -1725,9 +1805,9 @@ function charterRulesLines(bots, bot, rules) {
|
|
|
1725
1805
|
|
|
1726
1806
|
/**
|
|
1727
1807
|
* What became of each bot's permission rules: each of the kit's default rules
|
|
1728
|
-
* the run added to the bot, word for word (ADR
|
|
1808
|
+
* the run added to the bot, word for word (ADR 0050), each rule of a command no
|
|
1729
1809
|
* longer in the set that the bot still holds, with the command that takes it
|
|
1730
|
-
* back (ADR
|
|
1810
|
+
* back (ADR 0051), and the file the allowed ones were written into. Nothing for
|
|
1731
1811
|
* a bot with nothing added, held or written.
|
|
1732
1812
|
*/
|
|
1733
1813
|
function permissionsLines(permissions, bots) {
|
package/src/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The kit's commands, each with the flags it cannot do without, which `obk`
|
|
2
2
|
// reads to parse a command line; and the two lists every command is on one of:
|
|
3
|
-
// those every bot is allowed by default (#527, ADR
|
|
4
|
-
// for the user's yes (ADR
|
|
3
|
+
// those every bot is allowed by default (#527, ADR 0050), and those kept back
|
|
4
|
+
// for the user's yes (ADR 0051). A new command is on neither until someone
|
|
5
5
|
// puts it on one, and a test holds every command to that choice.
|
|
6
6
|
|
|
7
7
|
/** The commands, and the flags each one cannot do without. */
|
|
@@ -46,18 +46,19 @@ export const COMMANDS = {
|
|
|
46
46
|
'temp retire': ['bots', 'name'],
|
|
47
47
|
'temp trust-hooks': ['bots', 'name'],
|
|
48
48
|
'temp answer': ['bots', 'name'],
|
|
49
|
+
run: ['timeout'],
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* The commands every bot is allowed by default, each narrowed to its bots
|
|
53
|
-
* folder (ADR
|
|
54
|
+
* folder (ADR 0050). Only a command named here is in the default set; one that
|
|
54
55
|
* runs whatever its arguments say never is.
|
|
55
56
|
*/
|
|
56
57
|
export const DEFAULT_COMMANDS = [
|
|
57
58
|
'up', 'unpause', 'health', 'groom', 'roster', 'usage',
|
|
58
|
-
'
|
|
59
|
+
'rules build',
|
|
59
60
|
'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
|
|
60
|
-
'session
|
|
61
|
+
'session clear', 'session compact',
|
|
61
62
|
'message to', 'message send', 'message check',
|
|
62
63
|
'session record', 'session sent', 'session name', 'session mailbox',
|
|
63
64
|
'session trust-hooks', 'session answer',
|
|
@@ -66,17 +67,28 @@ export const DEFAULT_COMMANDS = [
|
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* The commands kept out of the default permission set, which keep the user's
|
|
69
|
-
* yes (ADR
|
|
70
|
-
* session, make the bots folder, or make a bot
|
|
71
|
-
* Father (#548)
|
|
70
|
+
* yes (ADR 0051): the ones that change permissions, close a long-lived
|
|
71
|
+
* session, make the bots folder, or make or change a bot or its sessions,
|
|
72
|
+
* which the owner leaves to Bot Father (#548, #615); and `run`, which runs
|
|
73
|
+
* whatever its arguments say, so a rule for it names its command (#532).
|
|
72
74
|
*/
|
|
73
75
|
export const KEPT_BACK = [
|
|
74
76
|
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
77
|
+
'bot change', 'session add', 'session change',
|
|
75
78
|
'permission allow', 'permission disallow', 'permission approval',
|
|
79
|
+
'run',
|
|
76
80
|
];
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
* The commands kit
|
|
80
|
-
* (#548). A bot may still hold their rules, which the kit names and
|
|
83
|
+
* The commands the kit gave every bot by default and that are kept back now
|
|
84
|
+
* (#548, #615). A bot may still hold their rules, which the kit names and
|
|
85
|
+
* leaves.
|
|
81
86
|
*/
|
|
82
|
-
export const NO_LONGER_DEFAULT = ['restart', 'bot create'];
|
|
87
|
+
export const NO_LONGER_DEFAULT = ['restart', 'bot create', 'bot change', 'session add', 'session change'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The commands kit 0.26.0 gave every bot by default and that the kit no longer
|
|
91
|
+
* has (#555). A bot may still hold their rules, which the kit names and leaves
|
|
92
|
+
* (#564).
|
|
93
|
+
*/
|
|
94
|
+
export const REMOVED = ['session mail', 'session nudge'];
|
package/src/conversations.js
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// the kit never recorded must not be mistaken for a session that never had one,
|
|
7
7
|
// so when the book cannot say, the harness is asked instead.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// Every harness keeps its own record of every conversation, per working
|
|
10
10
|
// directory, and every session starts at its bot home. Read only, never
|
|
11
11
|
// written: these are the harness's files (tech notes, sections 2 and 3).
|
|
12
12
|
|
|
13
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
@@ -22,6 +22,38 @@ const claudeDir = (home) => path.join(homedir(), '.claude', 'projects', slug(hom
|
|
|
22
22
|
/** The folder's name is the working directory with everything else turned into a dash. */
|
|
23
23
|
const slug = (home) => home.replaceAll(/[^A-Za-z0-9]/g, '-');
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Pi's sessions: one folder per working directory, named from its real path
|
|
27
|
+
* with the leading `/` taken off and `/`, `\` and `:` turned into dashes,
|
|
28
|
+
* between `--`; one `<time>_<id>.jsonl` per conversation (Pi 1.1.0,
|
|
29
|
+
* `getDefaultSessionDirPath`, tech notes, section 6).
|
|
30
|
+
*/
|
|
31
|
+
const piDir = (home) => path.join(homedir(), '.pi', 'agent', 'sessions', `--${realHome(home).replace(/^[/\\]/, '').replaceAll(/[/\\:]/g, '-')}--`);
|
|
32
|
+
|
|
33
|
+
/** The folder as Pi sees it from inside: the real path, when there is one. */
|
|
34
|
+
function realHome(home) {
|
|
35
|
+
try {
|
|
36
|
+
return realpathSync(home);
|
|
37
|
+
} catch {
|
|
38
|
+
return home;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A Pi session file's name, `<time>_<id>.jsonl`: the time is Pi's ISO time
|
|
44
|
+
* with `:` and `.` turned into dashes, and the id may hold a `_` of its own.
|
|
45
|
+
*/
|
|
46
|
+
const PI_FILE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_(.+)\.jsonl$/;
|
|
47
|
+
|
|
48
|
+
/** The id a Pi session file's name carries, or undefined for a file that is not one. */
|
|
49
|
+
const piIdOf = (name) => PI_FILE.exec(name)?.[1];
|
|
50
|
+
|
|
51
|
+
/** Where Pi keeps the conversation `id` of this bot home, or undefined when it has none yet. */
|
|
52
|
+
const piSession = (home, id) => {
|
|
53
|
+
const name = files(piDir(home)).find((one) => piIdOf(one) === id);
|
|
54
|
+
return name === undefined ? undefined : path.join(piDir(home), name);
|
|
55
|
+
};
|
|
56
|
+
|
|
25
57
|
/** Codex's rollouts: a file per conversation, filed by the day it started. */
|
|
26
58
|
const codexDir = () => path.join(homedir(), '.codex', 'sessions');
|
|
27
59
|
|
|
@@ -52,6 +84,7 @@ export function conversationsIn(harness, home, since) {
|
|
|
52
84
|
*/
|
|
53
85
|
export function hasConversation(harness, home, id) {
|
|
54
86
|
if (harness === 'claude') return existsSync(claudeTranscript(home, id));
|
|
87
|
+
if (harness === 'pi') return piSession(home, id) !== undefined;
|
|
55
88
|
return codexRollout(id) !== undefined;
|
|
56
89
|
}
|
|
57
90
|
|
|
@@ -120,7 +153,9 @@ export function claudeSubagentTranscripts(file) {
|
|
|
120
153
|
*/
|
|
121
154
|
export function transcriptsIn(harness, home, since) {
|
|
122
155
|
const from = since === undefined ? 0 : Date.parse(since);
|
|
123
|
-
const found = harness === 'claude' ? claudeConversations(home)
|
|
156
|
+
const found = harness === 'claude' ? claudeConversations(home)
|
|
157
|
+
: harness === 'pi' ? piConversations(home)
|
|
158
|
+
: codexConversations(home, from);
|
|
124
159
|
return found
|
|
125
160
|
.filter((one) => Number.isNaN(from) || one.at >= from)
|
|
126
161
|
.sort((left, right) => left.at - right.at)
|
|
@@ -173,6 +208,14 @@ function userTexts(harness, line) {
|
|
|
173
208
|
if (typeof content === 'string') return [content];
|
|
174
209
|
return Array.isArray(content) ? content.filter((block) => block?.type === 'text').map((block) => String(block.text)) : [];
|
|
175
210
|
}
|
|
211
|
+
if (harness === 'pi') {
|
|
212
|
+
// A turn is a `message` line, and the user's are role `user` (Pi 1.1.0,
|
|
213
|
+
// docs/session-format.md).
|
|
214
|
+
const message = parsed?.type === 'message' ? parsed.message : undefined;
|
|
215
|
+
if (message?.role !== 'user') return [];
|
|
216
|
+
if (typeof message.content === 'string') return [message.content];
|
|
217
|
+
return Array.isArray(message.content) ? message.content.filter((block) => block?.type === 'text').map((block) => String(block.text)) : [];
|
|
218
|
+
}
|
|
176
219
|
const item = parsed?.type === 'response_item' ? parsed.payload : undefined;
|
|
177
220
|
if (item?.type !== 'message' || item.role !== 'user' || !Array.isArray(item.content)) return [];
|
|
178
221
|
return item.content.filter((block) => block?.type === 'input_text').map((block) => String(block.text));
|
|
@@ -191,6 +234,33 @@ function claudeConversations(home) {
|
|
|
191
234
|
});
|
|
192
235
|
}
|
|
193
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Pi's conversations in this bot home. The first line of each is its header,
|
|
239
|
+
* `{ type: 'session', id, timestamp, cwd }`, and says which conversation it is
|
|
240
|
+
* and when it began; the file's name says the id too.
|
|
241
|
+
*/
|
|
242
|
+
function piConversations(home) {
|
|
243
|
+
return files(piDir(home))
|
|
244
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
245
|
+
.flatMap((name) => {
|
|
246
|
+
const file = path.join(piDir(home), name);
|
|
247
|
+
const header = piHeader(file);
|
|
248
|
+
const id = typeof header?.id === 'string' ? header.id : piIdOf(name);
|
|
249
|
+
const at = Date.parse(header?.timestamp ?? '') || startedAt(file);
|
|
250
|
+
return at === undefined || id === undefined || id === '' ? [] : [{ id, at, file }];
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A Pi session's first line, when it is the header. */
|
|
255
|
+
function piHeader(file) {
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(firstLine(file) ?? '');
|
|
258
|
+
return parsed?.type === 'session' ? parsed : undefined;
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
194
264
|
/** When a transcript's first line says the conversation was, if it says at all. */
|
|
195
265
|
function said(file) {
|
|
196
266
|
try {
|
package/src/groom.js
CHANGED
|
@@ -44,7 +44,7 @@ import path from 'node:path';
|
|
|
44
44
|
import { botDir, readBot, requireBotsFolder } from './bot.js';
|
|
45
45
|
import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
46
46
|
import { claudeTranscript } from './conversations.js';
|
|
47
|
-
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
47
|
+
import { harnessOf, ownCli, promptFile, shellWord, startPrompt } from './launch.js';
|
|
48
48
|
import { tabs, tabToTypeInto, typeIntoTab } from './orca.js';
|
|
49
49
|
import { BOT_FATHER, promptPath } from './up.js';
|
|
50
50
|
import { lines } from './usage.js';
|
|
@@ -123,7 +123,7 @@ export function grooming(bots, { at, ask, run } = {}) {
|
|
|
123
123
|
? groomingJobs(ofThisProcess(home, entry), markerOf(bots), Date.now())
|
|
124
124
|
: [];
|
|
125
125
|
|
|
126
|
-
const line = ask === undefined ? undefined : lineFor(ask, { bots, session, jobs, at, run });
|
|
126
|
+
const line = ask === undefined ? undefined : lineFor(ask, { bots, home, found, session, jobs, at, run });
|
|
127
127
|
if (line !== undefined) typeInto(bots, home, session, tab, line);
|
|
128
128
|
|
|
129
129
|
// A move is turning it on at another time, and is typed as one.
|
|
@@ -135,7 +135,7 @@ export function grooming(bots, { at, ask, run } = {}) {
|
|
|
135
135
|
* nothing to ask. Everything that makes the ask impossible refuses here, before
|
|
136
136
|
* Orca is asked for anything.
|
|
137
137
|
*/
|
|
138
|
-
function lineFor(ask, { bots, session, jobs, at, run }) {
|
|
138
|
+
function lineFor(ask, { bots, home, found, session, jobs, at, run }) {
|
|
139
139
|
// Nothing to turn off is an answer, unless a session is there to be told: an
|
|
140
140
|
// `--on` typed a moment ago may still be waiting in its tab.
|
|
141
141
|
if (ask === 'off' && jobs.length === 0 && !(session?.harness === 'claude' && session.up)) return undefined;
|
|
@@ -165,7 +165,8 @@ function lineFor(ask, { bots, session, jobs, at, run }) {
|
|
|
165
165
|
// A move keeps what the runs are on, when every job there says the same.
|
|
166
166
|
const runs = [...new Set(jobs.map((job) => JSON.stringify(job.run ?? null)))];
|
|
167
167
|
const keep = ask === 'move' && runs.length === 1 ? JSON.parse(runs[0]) ?? undefined : undefined;
|
|
168
|
-
|
|
168
|
+
const onRun = run ?? keep;
|
|
169
|
+
return onLine(bots, cronOf(when), jobs, onRun === undefined ? undefined : { ...onRun, task: writeRunTask(bots, ownPrompt(home, found)) });
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/**
|
|
@@ -367,7 +368,7 @@ function runElsewhere(bots, onRun) {
|
|
|
367
368
|
...(onRun.model === undefined ? [] : [`--model ${shellWord(onRun.model)}`]),
|
|
368
369
|
...(onRun.effort === undefined ? [] : [`--effort ${shellWord(onRun.effort)}`]),
|
|
369
370
|
...(onRun.extra_args ?? []).map((arg) => `--extra-arg=${shellWord(arg)}`),
|
|
370
|
-
`--prompt-file ${shellWord(
|
|
371
|
+
`--prompt-file ${shellWord(onRun.task)}`,
|
|
371
372
|
].join(' ');
|
|
372
373
|
return `${markerOf(bots)}: the daily grooming for this fleet, run on ${onRun.harness} as a temporary session of yours `
|
|
373
374
|
+ `that does the work and reports back to you. First, if any of your temporary sessions whose names start with groom- `
|
|
@@ -390,13 +391,29 @@ function readReport(bots) {
|
|
|
390
391
|
+ `Then retire the run with ${retire}, its name in place of ${RUN_NAME}. Schedule nothing then: the job renewed itself when it fired.`;
|
|
391
392
|
}
|
|
392
393
|
|
|
394
|
+
/**
|
|
395
|
+
* The grooming session's own prompt, as the user wrote it, or undefined when it
|
|
396
|
+
* has none: the standing duties a run on Codex carries too (#566). Without the
|
|
397
|
+
* work-dir note the session is launched with, since a run has a work dir of its
|
|
398
|
+
* own. Read when the line is typed, so a prompt changed since reaches the runs
|
|
399
|
+
* at the next `--on`.
|
|
400
|
+
*/
|
|
401
|
+
function ownPrompt(home, session) {
|
|
402
|
+
try {
|
|
403
|
+
return startPrompt(session, { home });
|
|
404
|
+
} catch (error) {
|
|
405
|
+
throw new Error(`${BOT_FATHER}'s ${GROOMING} session's prompt file ${promptFile(session, home)} cannot be read (${error.code ?? error.message}), and each grooming run on Codex carries that prompt. Nothing was typed. Put the file back, or change the session's prompt, then run this again.`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
393
409
|
/**
|
|
394
410
|
* Write the task a Codex run is started with, and answer where: do the daily
|
|
395
|
-
* grooming with the obk-grooming skill,
|
|
396
|
-
*
|
|
397
|
-
*
|
|
411
|
+
* grooming with the obk-grooming skill, and the grooming session's own duties
|
|
412
|
+
* when its prompt has any, send the report to the grooming session by the
|
|
413
|
+
* kit's mail, and stop. It schedules nothing: the schedule is the grooming
|
|
414
|
+
* session's.
|
|
398
415
|
*/
|
|
399
|
-
function writeRunTask(bots) {
|
|
416
|
+
function writeRunTask(bots, own) {
|
|
400
417
|
const real = realHome(bots);
|
|
401
418
|
const kit = shellWord(ownCli());
|
|
402
419
|
const file = path.resolve(promptPath(real, BOT_FATHER, `${GROOMING}-run`));
|
|
@@ -405,6 +422,7 @@ function writeRunTask(bots) {
|
|
|
405
422
|
+ `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. `
|
|
406
423
|
+ 'Work out what it cost with obk-finops, and keep each bot\'s profile notes. '
|
|
407
424
|
+ `The kit here is ${kit}: run it wherever a skill says obk. The bots folder is ${real}. `
|
|
425
|
+
+ (own === undefined ? '' : `The ${GROOMING} session's own prompt is this run's too. It follows, word for word as the user wrote it:\n\n${own}\n\n`)
|
|
408
426
|
+ `When the grooming is done, send one short report to ${BOT_FATHER}/${GROOMING} by the kit's mail: `
|
|
409
427
|
+ `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. `
|
|
410
428
|
+ 'Then stop: schedule nothing, make no session, and say nothing more.\n';
|