@assuredloop/orca-bot-kit 0.26.1 → 0.28.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 +68 -41
- package/SETUP.md +26 -13
- package/package.json +1 -1
- package/rules/mail.md +8 -8
- package/skills/obk-bot-building/SKILL.md +24 -15
- package/skills/obk-grooming/SKILL.md +4 -3
- package/src/book.js +143 -23
- package/src/bot.js +246 -118
- package/src/clear.js +7 -3
- package/src/cli.js +129 -130
- package/src/commands.js +21 -14
- package/src/conversations.js +73 -40
- package/src/groom.js +27 -9
- package/src/health.js +79 -11
- package/src/hooks.js +42 -64
- package/src/init.js +36 -29
- package/src/launch.js +221 -20
- package/src/message.js +83 -335
- package/src/name.js +1 -1
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +35 -42
- package/src/permissions.js +30 -16
- package/src/pi.js +55 -0
- package/src/processes.js +392 -0
- package/src/retire.js +43 -40
- package/src/rules.js +5 -2
- package/src/skills.js +51 -35
- package/src/sources.js +73 -4
- package/src/temp.js +65 -20
- package/src/up.js +80 -14
- package/src/unread.js +0 -160
package/src/cli.js
CHANGED
|
@@ -12,21 +12,21 @@ 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';
|
|
19
19
|
import { initBots } from './init.js';
|
|
20
20
|
import { nameSession } from './name.js';
|
|
21
21
|
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
22
|
-
import { checkMail,
|
|
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';
|
|
28
28
|
import { clearSession, compactSession } from './clear.js';
|
|
29
|
-
import { retireBot, retireSession
|
|
29
|
+
import { retireBot, retireSession } from './retire.js';
|
|
30
30
|
import { sentWarning } from './sent.js';
|
|
31
31
|
import { readRoster } from './roster.js';
|
|
32
32
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
@@ -43,18 +43,19 @@ Usage:
|
|
|
43
43
|
Create your bots folder: a git repo holding your
|
|
44
44
|
bots' configuration, with Bot Father in it, and
|
|
45
45
|
open Bot Father in Orca.
|
|
46
|
-
obk bot create --bots <path> --name <bot> --harness claude|codex
|
|
46
|
+
obk bot create --bots <path> --name <bot> --harness claude|codex|pi
|
|
47
47
|
[--charter <text>]
|
|
48
48
|
Write a new bot in your bots folder. Nothing is
|
|
49
49
|
opened in Orca until you run obk up.
|
|
50
50
|
obk session add --bots <path> --bot <bot> --name <session>
|
|
51
|
-
[--harness claude|codex] [--model <m>] [--effort <e>]
|
|
51
|
+
[--harness claude|codex|pi] [--model <m>] [--effort <e>]
|
|
52
52
|
[--context <c>]
|
|
53
53
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
54
54
|
[--extra-arg=<arg>]
|
|
55
55
|
Add a session to a bot. Anything left out is the
|
|
56
56
|
harness's own default; approval is auto, and only
|
|
57
|
-
obk permission approval changes it. A
|
|
57
|
+
obk permission approval changes it. A session on
|
|
58
|
+
Pi is dangerously-skip, the one level Pi has. A long
|
|
58
59
|
start prompt lives in a file in the bot home, and
|
|
59
60
|
--prompt-file names it.
|
|
60
61
|
A value of your own that starts with a dash is
|
|
@@ -87,15 +88,18 @@ Usage:
|
|
|
87
88
|
the bot's temporary sessions may be made at. A
|
|
88
89
|
running session takes it when it next starts.
|
|
89
90
|
obk session change --bots <path> --bot <bot> --session <session>
|
|
91
|
+
[--harness claude|codex|pi]
|
|
90
92
|
[--model <m>] [--effort <e>] [--context <c>]
|
|
91
93
|
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
92
94
|
[--extra-arg=<arg>]
|
|
93
95
|
Change a session's settings. What you leave out
|
|
94
96
|
stays as it is; a setting given empty, --model=,
|
|
95
97
|
goes back to the harness's own default. A running
|
|
96
|
-
session takes the change when it next starts.
|
|
97
|
-
|
|
98
|
-
and
|
|
98
|
+
session takes the change when it next starts. On
|
|
99
|
+
another harness it starts a new conversation, the
|
|
100
|
+
book keeps the old one in its history, and model,
|
|
101
|
+
effort and context go back to the new harness's
|
|
102
|
+
defaults unless given too.
|
|
99
103
|
obk pause --bots <path> --bot <bot> [--session <name>]
|
|
100
104
|
Stop a bot, or one of its sessions, for now: close
|
|
101
105
|
its tabs and have obk up leave it closed. The book
|
|
@@ -109,7 +113,9 @@ Usage:
|
|
|
109
113
|
End a session: close its tab and take it off the
|
|
110
114
|
bot, keeping its conversations in the book, and
|
|
111
115
|
first do the same for the temporary sessions it
|
|
112
|
-
made, and theirs.
|
|
116
|
+
made, and theirs. It stops what the session left
|
|
117
|
+
running in its work dir, and names each process.
|
|
118
|
+
Or end a bot: close its tabs,
|
|
113
119
|
remove its Orca project and move its folder to
|
|
114
120
|
retired/. It will not retire a bot whose Orca
|
|
115
121
|
project holds a tab your book does not name, and
|
|
@@ -207,13 +213,18 @@ Usage:
|
|
|
207
213
|
the Orca mailbox. Nothing is sent.
|
|
208
214
|
obk message send --bots <path> --to <bot>[/<session>] [--from <bot>/<session>]
|
|
209
215
|
--subject <text> [--text <text> | --text-file <path>]
|
|
210
|
-
[--thread <id>]
|
|
211
|
-
Put a message in that session's Orca mailbox
|
|
212
|
-
|
|
216
|
+
[--thread <id>] [--interrupt]
|
|
217
|
+
Put a message in that session's Orca mailbox. Orca
|
|
218
|
+
tells it to look once it is idle. --interrupt is
|
|
219
|
+
for urgent mail: when the session is busy, the kit
|
|
220
|
+
presses Escape once in its tab first, to end its
|
|
221
|
+
turn, so it looks now. A message too long to travel
|
|
213
222
|
as itself is written to a file beside your bots
|
|
214
223
|
folder and named in the message. A pair the
|
|
215
224
|
harness's own messaging reaches is not carried:
|
|
216
225
|
the address to write to is answered instead.
|
|
226
|
+
In a session's own tab it sends as that session,
|
|
227
|
+
and --from may name only that one.
|
|
217
228
|
obk message check --bots <path> [--bot <bot>] [--session <name>] [--peek]
|
|
218
229
|
Read what is waiting for a session and mark it
|
|
219
230
|
read. --peek leaves it unread. Run in a session's
|
|
@@ -269,7 +280,8 @@ Usage:
|
|
|
269
280
|
only while its tab is up in Orca. --run-on codex
|
|
270
281
|
makes each run a temporary Codex session of the
|
|
271
282
|
grooming session's, launched with --model, --effort
|
|
272
|
-
and --extra-arg, which
|
|
283
|
+
and --extra-arg, which carries the grooming
|
|
284
|
+
session's own prompt, reports back by the kit's
|
|
273
285
|
mail and is retired.
|
|
274
286
|
obk usage --bots <path> [--bot <bot>] [--session <name>] [--since <time>] [--until <time>]
|
|
275
287
|
Say what your sessions have used: the conversations
|
|
@@ -305,17 +317,6 @@ Usage:
|
|
|
305
317
|
Claude session's native message, it warns the
|
|
306
318
|
session when the address was none of this bots
|
|
307
319
|
folder's own. It never stops a message.
|
|
308
|
-
obk session nudge --bots <path> --bot <bot>
|
|
309
|
-
For the kit's own hook, not for typing: after a
|
|
310
|
-
Codex session's shell command, it decides the mail
|
|
311
|
-
nudges a send there left because it could not
|
|
312
|
-
tell from inside Codex's sandbox, and types each
|
|
313
|
-
one or says why not.
|
|
314
|
-
obk session mail --bots <path> --bot <bot>
|
|
315
|
-
For the kit's own hook, not for typing: at the end
|
|
316
|
-
of a Claude session's turn, it tells the session
|
|
317
|
-
once about each fleet mail the kit sent it that is
|
|
318
|
-
still unread. It types nothing into the tab.
|
|
319
320
|
obk session name --bots <path> --bot <bot>
|
|
320
321
|
For the kit's own hook, not for typing: after a
|
|
321
322
|
Codex session's turn, it names the session's thread
|
|
@@ -409,6 +410,7 @@ async function run(argv) {
|
|
|
409
410
|
'text-file': { type: 'string' },
|
|
410
411
|
thread: { type: 'string' },
|
|
411
412
|
peek: { type: 'boolean' },
|
|
413
|
+
interrupt: { type: 'boolean' },
|
|
412
414
|
charter: { type: 'string' },
|
|
413
415
|
allow: { type: 'string', multiple: true },
|
|
414
416
|
disallow: { type: 'string', multiple: true },
|
|
@@ -440,7 +442,7 @@ async function run(argv) {
|
|
|
440
442
|
}
|
|
441
443
|
|
|
442
444
|
// A permission rule narrows a command to one bots folder by its first words,
|
|
443
|
-
// whatever comes after them (ADR
|
|
445
|
+
// whatever comes after them (ADR 0050), and the last of two `--bots` would
|
|
444
446
|
// win: so a second one is refused before anything runs.
|
|
445
447
|
if (tokens.filter((token) => token.kind === 'option' && token.name === 'bots').length > 1) {
|
|
446
448
|
throw new Error('--bots is given more than once. Give it once: one command works on one bots folder. Nothing was done.');
|
|
@@ -476,8 +478,6 @@ async function run(argv) {
|
|
|
476
478
|
// The send hook is quiet whatever it is given, a bots folder that is not
|
|
477
479
|
// there included, so it goes before anything that can complain (ADR 0032).
|
|
478
480
|
if (command === SENT) return sent(path.resolve(values.bots));
|
|
479
|
-
if (command === NUDGE) return nudgeLeft();
|
|
480
|
-
if (command === MAIL) return mailHook(path.resolve(values.bots), values.bot);
|
|
481
481
|
if (command === NAME) return nameThread(path.resolve(values.bots), values.bot);
|
|
482
482
|
|
|
483
483
|
// One fleet, one identity, whatever spelling of its path was given (#164).
|
|
@@ -548,8 +548,6 @@ const ANSWER_IT = [
|
|
|
548
548
|
/** The commands a harness runs rather than a person: the kit's hooks. */
|
|
549
549
|
const RECORD = 'session record';
|
|
550
550
|
const SENT = 'session sent';
|
|
551
|
-
const NUDGE = 'session nudge';
|
|
552
|
-
const MAIL = 'session mail';
|
|
553
551
|
const NAME = 'session name';
|
|
554
552
|
|
|
555
553
|
/**
|
|
@@ -569,45 +567,6 @@ function sent(bots) {
|
|
|
569
567
|
return 0;
|
|
570
568
|
}
|
|
571
569
|
|
|
572
|
-
/**
|
|
573
|
-
* What the kit's Codex hook does after a shell command: decide the nudges a
|
|
574
|
-
* send in this tab left for it, outside Codex's sandbox, and say to the session
|
|
575
|
-
* what became of each, as Codex's `additionalContext` (#350, ADR 0034). Silent
|
|
576
|
-
* when nothing was left, never a decision, and never anything but exit 0.
|
|
577
|
-
*/
|
|
578
|
-
async function nudgeLeft() {
|
|
579
|
-
try {
|
|
580
|
-
const said = JSON.parse(readFileSync(0, 'utf8'));
|
|
581
|
-
if (said?.hook_event_name !== 'PostToolUse') return 0;
|
|
582
|
-
const decided = await decideLeftNudges(process.env[TAB_ENV]);
|
|
583
|
-
if (decided.length > 0) {
|
|
584
|
-
const words = decided.map((one) => `Your mail "${one.subject}" to ${one.to}: ${one.nudged ? "the kit's hook nudged its tab, from outside Codex's sandbox:" : "the kit's hook looked at its tab from outside Codex's sandbox, and"} ${nudgeLine(one, one.to).trim()}`);
|
|
585
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: words.join('\n') } })}\n`);
|
|
586
|
-
}
|
|
587
|
-
} catch {
|
|
588
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
589
|
-
}
|
|
590
|
-
return 0;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* What the kit's Claude Code Stop hook does at a turn end (#509, ADR 0035): tell
|
|
595
|
-
* the session once about fleet mail still unread, as the Stop event's
|
|
596
|
-
* additional context, which the session goes on with. Not a block: Claude Code
|
|
597
|
-
* 2.1.296 draws a block's reason in red as "Stop hook error", and its additional
|
|
598
|
-
* context as "Stop hook feedback". Nothing typed into the tab; never anything
|
|
599
|
-
* but exit 0; silent whenever it cannot be sure.
|
|
600
|
-
*/
|
|
601
|
-
function mailHook(bots, bot) {
|
|
602
|
-
try {
|
|
603
|
-
const reason = stillUnread(sameFleet(bots), bot, JSON.parse(readFileSync(0, 'utf8')), process.env[TAB_ENV], process.env[TERMINAL_ENV]);
|
|
604
|
-
if (reason !== undefined) process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: reason } })}\n`);
|
|
605
|
-
} catch {
|
|
606
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
607
|
-
}
|
|
608
|
-
return 0;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
570
|
/**
|
|
612
571
|
* What the kit's Codex Stop hook does at a turn end: name the session's thread
|
|
613
572
|
* after its bot and session, once it is idle (#480). Silent, never a decision,
|
|
@@ -643,6 +602,8 @@ async function record(bots, bot) {
|
|
|
643
602
|
|
|
644
603
|
const commands = {
|
|
645
604
|
async init(bots, values) {
|
|
605
|
+
// Bot Father on Pi is not in the first slice of #547.
|
|
606
|
+
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.');
|
|
646
607
|
// Asked before anything is written, so an Orca that is down leaves the disk
|
|
647
608
|
// exactly as it was and the caller can simply run the command again.
|
|
648
609
|
refuseWhenOrcaIsDown();
|
|
@@ -737,7 +698,7 @@ const commands = {
|
|
|
737
698
|
...withLines(retired.retiredWith, retired.session),
|
|
738
699
|
...closedLines(retired.closed),
|
|
739
700
|
`retired ${retired.bot} ${retired.session}: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
740
|
-
...
|
|
701
|
+
...processLines(retired.processes),
|
|
741
702
|
...leftLines(retired.promptsLeft),
|
|
742
703
|
],
|
|
743
704
|
};
|
|
@@ -749,6 +710,7 @@ const commands = {
|
|
|
749
710
|
answer: { bots, ...retired },
|
|
750
711
|
lines: [
|
|
751
712
|
...closedLines(retired.closed),
|
|
713
|
+
...processLines(retired.processes),
|
|
752
714
|
`${'trouble'.padEnd(9)} Orca project ${retired.project}`,
|
|
753
715
|
` ${retired.trouble}`,
|
|
754
716
|
`Retire it again: ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${retired.bot}`,
|
|
@@ -767,7 +729,7 @@ const commands = {
|
|
|
767
729
|
retired.windowReloaded ? "reloaded Orca's window, so its sidebar no longer shows the project" : RELOAD_LINE,
|
|
768
730
|
]),
|
|
769
731
|
`retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
|
|
770
|
-
...(retired.
|
|
732
|
+
...processLines(retired.processes),
|
|
771
733
|
...leftLines(retired.promptsLeft),
|
|
772
734
|
],
|
|
773
735
|
};
|
|
@@ -824,7 +786,7 @@ const commands = {
|
|
|
824
786
|
...withLines(retired.retiredWith, retired.session),
|
|
825
787
|
...closedLines(retired.closed),
|
|
826
788
|
`retired ${retired.bot} ${retired.session}, a temporary session of ${retired.maker}'s: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
827
|
-
...
|
|
789
|
+
...processLines(retired.processes),
|
|
828
790
|
...leftLines(retired.promptsLeft),
|
|
829
791
|
],
|
|
830
792
|
};
|
|
@@ -874,7 +836,9 @@ const commands = {
|
|
|
874
836
|
},
|
|
875
837
|
|
|
876
838
|
'bot create'(bots, values) {
|
|
877
|
-
|
|
839
|
+
// The kit's default rules (ADR 0050) are in the bot's first bot.yaml.
|
|
840
|
+
const allow = botDefaults(bots, { harness: values.harness, sessions: [] });
|
|
841
|
+
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter, allow });
|
|
878
842
|
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
|
879
843
|
// that a new bot's file and a rebuilt one are written by the same code.
|
|
880
844
|
const rules = [buildAgents(bots, made.home, readBot(made.home))];
|
|
@@ -885,9 +849,10 @@ const commands = {
|
|
|
885
849
|
// instructions, so `up` will not start it, and saying "give it a session"
|
|
886
850
|
// would send the caller past the thing that needs settling first.
|
|
887
851
|
const trouble = rules[0].trouble !== undefined;
|
|
888
|
-
// The
|
|
889
|
-
// with no instructions gets them at the rules build that gives
|
|
890
|
-
|
|
852
|
+
// The default rules go into the harness's files for a bot that is set up
|
|
853
|
+
// to run: one with no instructions gets them at the rules build that gives
|
|
854
|
+
// it some. They were added by this command, so its report names them.
|
|
855
|
+
const permissions = trouble ? [] : writePermissions(bots, made.home, readBot(made.home)).map((entry) => ({ ...entry, defaults: allow }));
|
|
891
856
|
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills, permissions };
|
|
892
857
|
return {
|
|
893
858
|
answer,
|
|
@@ -909,7 +874,7 @@ const commands = {
|
|
|
909
874
|
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.`);
|
|
910
875
|
}
|
|
911
876
|
// Permission changes have commands of their own, which keep the user's yes
|
|
912
|
-
// (ADR
|
|
877
|
+
// (ADR 0051): the old spelling refuses, and changes nothing.
|
|
913
878
|
const moved = [['allow', 'permission allow'], ['disallow', 'permission disallow']].filter(([flag]) => values[flag] !== undefined);
|
|
914
879
|
if (moved.length > 0) {
|
|
915
880
|
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.`);
|
|
@@ -1116,7 +1081,7 @@ const commands = {
|
|
|
1116
1081
|
...(values.effort === undefined ? {} : { effort: values.effort }),
|
|
1117
1082
|
...(values['extra-arg'] === undefined ? {} : { extra_args: values['extra-arg'] }),
|
|
1118
1083
|
};
|
|
1119
|
-
// A run's launch may not widen its approval (ADR
|
|
1084
|
+
// A run's launch may not widen its approval (ADR 0051).
|
|
1120
1085
|
if (values['run-on'] === 'codex') refuseApprovalArgs('codex', values['extra-arg']);
|
|
1121
1086
|
const groom = grooming(bots, { at: values.at, ask, run });
|
|
1122
1087
|
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
@@ -1179,7 +1144,8 @@ const commands = {
|
|
|
1179
1144
|
},
|
|
1180
1145
|
|
|
1181
1146
|
async 'message send'(bots, values) {
|
|
1182
|
-
|
|
1147
|
+
// With --from too: in one of the fleet's tabs, the tab says who sends (#570).
|
|
1148
|
+
const tab = callerTab(bots, true);
|
|
1183
1149
|
refuseWhenOrcaIsDown();
|
|
1184
1150
|
const answer = await sendMessage(bots, {
|
|
1185
1151
|
to: values.to,
|
|
@@ -1189,6 +1155,7 @@ const commands = {
|
|
|
1189
1155
|
text: values.text,
|
|
1190
1156
|
textFile: values['text-file'],
|
|
1191
1157
|
thread: values.thread,
|
|
1158
|
+
interrupt: values.interrupt === true,
|
|
1192
1159
|
});
|
|
1193
1160
|
const where = `${answer.to.bot}/${answer.to.session}`;
|
|
1194
1161
|
|
|
@@ -1207,7 +1174,7 @@ const commands = {
|
|
|
1207
1174
|
...(answer.file === undefined
|
|
1208
1175
|
? []
|
|
1209
1176
|
: [` it was too long to travel as itself, so it went as a file: ${answer.file}`]),
|
|
1210
|
-
...
|
|
1177
|
+
...sentLines(answer, where),
|
|
1211
1178
|
],
|
|
1212
1179
|
};
|
|
1213
1180
|
},
|
|
@@ -1250,6 +1217,7 @@ const commands = {
|
|
|
1250
1217
|
|
|
1251
1218
|
'session add'(bots, values) {
|
|
1252
1219
|
refuseApproval(bots, values, 'session add');
|
|
1220
|
+
refuseOutsideWorkDir(bots, values);
|
|
1253
1221
|
const added = addSession(bots, values.bot, settingsOf(values));
|
|
1254
1222
|
const found = workDirFound(values, added.bot, added.home, added.session.name);
|
|
1255
1223
|
const answer = { bots, bot: added.bot, home: added.home, session: added.session, found };
|
|
@@ -1279,21 +1247,23 @@ const commands = {
|
|
|
1279
1247
|
},
|
|
1280
1248
|
|
|
1281
1249
|
'session change'(bots, values) {
|
|
1282
|
-
if (values.harness !== undefined) {
|
|
1283
|
-
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.`);
|
|
1284
|
-
}
|
|
1285
1250
|
refuseApproval(bots, values, 'session change');
|
|
1286
1251
|
const { name, ...settings } = settingsOf(values);
|
|
1287
1252
|
const changed = changeSession(bots, values.bot, values.session, settings);
|
|
1288
1253
|
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
|
|
1289
1254
|
const found = workDirFound(values, changed.bot, changed.home, values.session);
|
|
1255
|
+
const { moved } = changed;
|
|
1290
1256
|
return {
|
|
1291
|
-
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart, found },
|
|
1257
|
+
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, ...(moved === undefined ? {} : { moved }), restart, found },
|
|
1292
1258
|
lines: [
|
|
1293
1259
|
`changed session ${values.session} in ${path.join('bots', changed.bot, 'bot.yaml')}`,
|
|
1294
1260
|
...Object.entries(changed.session)
|
|
1295
1261
|
.filter(([key]) => key !== 'name')
|
|
1296
1262
|
.map(([key, value]) => ` ${key} ${oneLine(value)}`),
|
|
1263
|
+
...(moved === undefined ? [] : [
|
|
1264
|
+
`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.`,
|
|
1265
|
+
...(moved.defaults.length === 0 ? [] : [` ${moved.defaults.join(', ')} went back to ${moved.to}'s own default${moved.defaults.length === 1 ? '' : 's'}.`]),
|
|
1266
|
+
]),
|
|
1297
1267
|
...foundLines(found),
|
|
1298
1268
|
`A running session takes this when it next starts: ${restart}`,
|
|
1299
1269
|
],
|
|
@@ -1336,7 +1306,7 @@ function toLines(answer, bots, where) {
|
|
|
1336
1306
|
// says what to do instead, before the bot needs it (#521).
|
|
1337
1307
|
return [
|
|
1338
1308
|
` Write to ${answer.address} with your own harness's messaging. The kit does not carry that road.`,
|
|
1339
|
-
" 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
|
|
1309
|
+
" 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.",
|
|
1340
1310
|
];
|
|
1341
1311
|
}
|
|
1342
1312
|
return [
|
|
@@ -1348,52 +1318,49 @@ function toLines(answer, bots, where) {
|
|
|
1348
1318
|
}
|
|
1349
1319
|
|
|
1350
1320
|
/**
|
|
1351
|
-
* What
|
|
1352
|
-
*
|
|
1353
|
-
* about the message.
|
|
1321
|
+
* What the send says after `sent`: the road it took, and what the interrupt
|
|
1322
|
+
* came to when one was asked for (#555).
|
|
1354
1323
|
*/
|
|
1355
|
-
function
|
|
1356
|
-
const
|
|
1357
|
-
if (answer.
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
const why = {
|
|
1364
|
-
'no-turn': `no turn started in its tab in ${WATCH_SECONDS} s, so the kit's line was typed`,
|
|
1365
|
-
busy: "it is busy with a turn, and Codex takes a line into the turn it is having, so the kit's line was typed",
|
|
1366
|
-
'other-turn': "a turn of other work started in its tab, not Orca's notice, so the kit's line was typed",
|
|
1367
|
-
}[answer.because];
|
|
1368
|
-
const head = why === undefined ? ' ' : ` ${why}: `;
|
|
1369
|
-
if (answer.nudged && answer.nudgeWatched === false) {
|
|
1370
|
-
return `${head}its tab was typed into, and Orca could not say whether it was taken: ${answer.nudgeUnseen} The message waits in its mailbox for its next check either way.`;
|
|
1371
|
-
}
|
|
1372
|
-
if (answer.nudged && answer.nudgeUnseen !== undefined) {
|
|
1373
|
-
return `${head}its tab was typed into, but Orca did not see the line start a turn: it may be queued behind the work in hand, or lost. The message waits in its mailbox for its next check either way.`;
|
|
1324
|
+
function sentLines(answer, where) {
|
|
1325
|
+
const road = noticeLines(answer);
|
|
1326
|
+
if (answer.interrupted === true) {
|
|
1327
|
+
return [
|
|
1328
|
+
...(road.length === 1 ? [road[0].replace('once it is idle', 'now')] : road),
|
|
1329
|
+
` 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.' : ''}`,
|
|
1330
|
+
];
|
|
1374
1331
|
}
|
|
1375
|
-
if (answer.
|
|
1376
|
-
|
|
1377
|
-
return ` its tab has something waiting to be answered (${answer.blocked}), so nothing was typed into it. Settle that, and the mail is there.`;
|
|
1332
|
+
if (answer.interrupted === false) {
|
|
1333
|
+
return [...road, answer.idle ? ` it is idle, so it was not interrupted.` : ` it was not interrupted: ${answer.interruptTrouble}.`];
|
|
1378
1334
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1335
|
+
return road;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Whether Orca's notice will tell the receiver of its letter. It will not while
|
|
1340
|
+
* Orca lists the receiver's tab as orphaned, as after a window restart: the
|
|
1341
|
+
* window has not loaded the tab (#579). A switch to the tab loads it, and is
|
|
1342
|
+
* named as a fact with its cost, not run and not urged: it moves the user's view.
|
|
1343
|
+
*/
|
|
1344
|
+
function noticeLines(answer) {
|
|
1345
|
+
if (answer.orphaned === true) {
|
|
1346
|
+
return [
|
|
1347
|
+
" 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.",
|
|
1348
|
+
` ${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.`,
|
|
1349
|
+
];
|
|
1381
1350
|
}
|
|
1382
|
-
if (answer.
|
|
1383
|
-
return ` it is
|
|
1351
|
+
if (answer.loadTrouble !== undefined) {
|
|
1352
|
+
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.`];
|
|
1384
1353
|
}
|
|
1385
|
-
return
|
|
1354
|
+
return [" it is in its Orca mailbox, and Orca's own notice tells it to look once it is idle."];
|
|
1386
1355
|
}
|
|
1387
1356
|
|
|
1388
|
-
/** The watch before the kit's line, as the send says it (#509, ADR 0035). */
|
|
1389
|
-
const WATCH_SECONDS = WATCH_MS / 1000;
|
|
1390
|
-
|
|
1391
1357
|
/**
|
|
1392
1358
|
* What there is to say about the work dir this command was given: one finding
|
|
1393
1359
|
* when it leads out of the bot home, through `..` or a link or as a path
|
|
1394
1360
|
* elsewhere, and nothing otherwise (#222). A bot's work and its clones belong
|
|
1395
|
-
* under its own `work/`.
|
|
1396
|
-
*
|
|
1361
|
+
* under its own `work/`. For a Claude or Pi session it is said, not refused: a
|
|
1362
|
+
* work dir is an instruction there (PRD 6.4), and it is written as given. A
|
|
1363
|
+
* Codex session's is refused before this (refuseCodexOutside).
|
|
1397
1364
|
*/
|
|
1398
1365
|
function workDirFound(values, bot, home, session) {
|
|
1399
1366
|
const workDir = workDirOf({ work_dir: values['work-dir'] }, home);
|
|
@@ -1405,16 +1372,35 @@ function workDirFound(values, bot, home, session) {
|
|
|
1405
1372
|
}];
|
|
1406
1373
|
}
|
|
1407
1374
|
|
|
1375
|
+
/**
|
|
1376
|
+
* Refuse a new Codex session a work dir outside its bot home (ADR 0045). A
|
|
1377
|
+
* changed session is judged in changeSession, on what it writes (#585).
|
|
1378
|
+
*/
|
|
1379
|
+
function refuseOutsideWorkDir(bots, values) {
|
|
1380
|
+
if (values['work-dir'] === undefined) return;
|
|
1381
|
+
let bot;
|
|
1382
|
+
try {
|
|
1383
|
+
bot = readBot(botDir(bots, values.bot), values.bot);
|
|
1384
|
+
} catch {
|
|
1385
|
+
// A bot that is not there is the command's own to refuse.
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
const session = { name: values.name, harness: values.harness, work_dir: values['work-dir'] };
|
|
1389
|
+
refuseCodexOutside(values.bot, session, undefined, bot.harness, botDir(bots, values.bot), 'session add');
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1408
1392
|
/**
|
|
1409
1393
|
* Refuse what would change a session's approval through `session add` or
|
|
1410
|
-
* `session change` (ADR
|
|
1394
|
+
* `session change` (ADR 0051): `--approval`, and an extra argument that sets
|
|
1411
1395
|
* it on the session's harness. Only `permission approval` changes it.
|
|
1412
1396
|
*/
|
|
1413
1397
|
function refuseApproval(bots, values, command) {
|
|
1414
1398
|
if (values.approval !== undefined) {
|
|
1415
1399
|
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.`);
|
|
1416
1400
|
}
|
|
1417
|
-
|
|
1401
|
+
// A move to another harness keeps the extra arguments the session has, and
|
|
1402
|
+
// one of them can set approval on the new harness (#565).
|
|
1403
|
+
if (values['extra-arg'] === undefined && values.harness === undefined) return;
|
|
1418
1404
|
let bot;
|
|
1419
1405
|
try {
|
|
1420
1406
|
bot = readBot(botDir(bots, values.bot), values.bot);
|
|
@@ -1423,7 +1409,7 @@ function refuseApproval(bots, values, command) {
|
|
|
1423
1409
|
return;
|
|
1424
1410
|
}
|
|
1425
1411
|
const session = bot.sessions.find((one) => one.name === (values.session ?? values.name)) ?? {};
|
|
1426
|
-
refuseApprovalArgs(harnessOf({ ...session, ...(values.harness === undefined ? {} : { harness: values.harness }) }, bot.harness), values['extra-arg']);
|
|
1412
|
+
refuseApprovalArgs(harnessOf({ ...session, ...(values.harness === undefined ? {} : { harness: values.harness }) }, bot.harness), values['extra-arg'] ?? (command === 'session change' ? session.extra_args : undefined));
|
|
1427
1413
|
}
|
|
1428
1414
|
|
|
1429
1415
|
/**
|
|
@@ -1459,12 +1445,19 @@ const closedLines = (closed) => closed.map((tab) => `closed ${tab.bot} ${tab
|
|
|
1459
1445
|
const withLines = (retiredWith, session) => retiredWith.flatMap((gone) => [
|
|
1460
1446
|
...closedLines(gone.closed),
|
|
1461
1447
|
`retired ${gone.bot} ${gone.session}, a temporary session of ${gone.maker}'s, along with ${session}: off ${path.join('bots', gone.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
1462
|
-
...
|
|
1448
|
+
...processLines(gone.processes),
|
|
1463
1449
|
...leftLines(gone.promptsLeft),
|
|
1464
1450
|
]);
|
|
1465
1451
|
|
|
1466
|
-
/**
|
|
1467
|
-
|
|
1452
|
+
/** What a retire stopped of what its sessions left running, and what it named and left (#537). */
|
|
1453
|
+
function processLines(processes) {
|
|
1454
|
+
if (processes === undefined) return [];
|
|
1455
|
+
if (processes.unreadable !== undefined) return [`processes the kit cannot tell which processes were left running in the work dir, so it stopped none: ${processes.unreadable}`];
|
|
1456
|
+
return [
|
|
1457
|
+
...processes.stopped.map((one) => `stopped pid ${one.pid} of ${one.session}, with ${one.signal}: ${one.command}${one.cwd === null ? '' : ` in ${one.cwd}`}`),
|
|
1458
|
+
...processes.left.map((one) => `left pid ${one.pid} in ${one.cwd ?? 'a folder lsof did not name'}: ${one.command}. Not stopped: ${one.why}.`),
|
|
1459
|
+
];
|
|
1460
|
+
}
|
|
1468
1461
|
|
|
1469
1462
|
/** What a retire could not remove, and how to: nothing reads these files now (#393). */
|
|
1470
1463
|
const leftLines = (left = []) => left.flatMap(({ file, reason }) => [
|
|
@@ -1499,6 +1492,12 @@ function refuseWhenOrcaIsDown() {
|
|
|
1499
1492
|
/** The one line that says which conversation this tab was given, and from where. */
|
|
1500
1493
|
const howLines = (tab) => {
|
|
1501
1494
|
if (tab.resumed === true) return [' it was told to resume the session the book holds, with its conversation.'];
|
|
1495
|
+
if (tab.harnessChanged !== undefined) {
|
|
1496
|
+
return [
|
|
1497
|
+
` it was told to start a new session: the book's conversation ${tab.harnessChanged} ran on ${tab.ranOn},`,
|
|
1498
|
+
" and the session has moved from it, so it went into the session's history.",
|
|
1499
|
+
];
|
|
1500
|
+
}
|
|
1502
1501
|
if (tab.noConversation === undefined) return [' it was told to start a new session: the book holds none for this one yet.'];
|
|
1503
1502
|
return [
|
|
1504
1503
|
` it was told to start a new session: the book's conversation ${tab.noConversation} had nothing on record`,
|
|
@@ -1720,7 +1719,7 @@ function rosterLines(roster, bots) {
|
|
|
1720
1719
|
}
|
|
1721
1720
|
|
|
1722
1721
|
lines.push(roster.length === 0
|
|
1723
|
-
? `No bots yet. Make one: ${shellWord(ownCli())} bot create --bots ${shellWord(bots)} --name <name> --harness claude|codex`
|
|
1722
|
+
? `No bots yet. Make one: ${shellWord(ownCli())} bot create --bots ${shellWord(bots)} --name <name> --harness claude|codex|pi`
|
|
1724
1723
|
: `${roster.length} bot${roster.length === 1 ? '' : 's'}. Your bots folder: ${bots}`);
|
|
1725
1724
|
return lines;
|
|
1726
1725
|
}
|
|
@@ -1787,9 +1786,9 @@ function charterRulesLines(bots, bot, rules) {
|
|
|
1787
1786
|
|
|
1788
1787
|
/**
|
|
1789
1788
|
* What became of each bot's permission rules: each of the kit's default rules
|
|
1790
|
-
* the run added to the bot, word for word (ADR
|
|
1789
|
+
* the run added to the bot, word for word (ADR 0050), each rule of a command no
|
|
1791
1790
|
* longer in the set that the bot still holds, with the command that takes it
|
|
1792
|
-
* back (ADR
|
|
1791
|
+
* back (ADR 0051), and the file the allowed ones were written into. Nothing for
|
|
1793
1792
|
* a bot with nothing added, held or written.
|
|
1794
1793
|
*/
|
|
1795
1794
|
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. */
|
|
@@ -37,8 +37,6 @@ export const COMMANDS = {
|
|
|
37
37
|
'message check': ['bots'],
|
|
38
38
|
'session record': ['bots', 'bot'],
|
|
39
39
|
'session sent': ['bots', 'bot'],
|
|
40
|
-
'session nudge': ['bots', 'bot'],
|
|
41
|
-
'session mail': ['bots', 'bot'],
|
|
42
40
|
'session name': ['bots', 'bot'],
|
|
43
41
|
'session mailbox': ['bots', 'bot', 'session'],
|
|
44
42
|
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
@@ -52,33 +50,42 @@ export const COMMANDS = {
|
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
52
|
* The commands every bot is allowed by default, each narrowed to its bots
|
|
55
|
-
* folder (ADR
|
|
53
|
+
* folder (ADR 0050). Only a command named here is in the default set; one that
|
|
56
54
|
* runs whatever its arguments say never is.
|
|
57
55
|
*/
|
|
58
56
|
export const DEFAULT_COMMANDS = [
|
|
59
57
|
'up', 'unpause', 'health', 'groom', 'roster', 'usage',
|
|
60
|
-
'
|
|
58
|
+
'rules build',
|
|
61
59
|
'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
|
|
62
|
-
'session
|
|
60
|
+
'session clear', 'session compact',
|
|
63
61
|
'message to', 'message send', 'message check',
|
|
64
|
-
'session record', 'session sent', 'session
|
|
62
|
+
'session record', 'session sent', 'session name', 'session mailbox',
|
|
65
63
|
'session trust-hooks', 'session answer',
|
|
66
64
|
'temp make', 'temp roles', 'temp retire', 'temp trust-hooks', 'temp answer',
|
|
67
65
|
];
|
|
68
66
|
|
|
69
67
|
/**
|
|
70
68
|
* The commands kept out of the default permission set, which keep the user's
|
|
71
|
-
* yes (ADR
|
|
72
|
-
* session, make the bots folder, or make a bot
|
|
73
|
-
* Father (#548).
|
|
69
|
+
* yes (ADR 0051): the ones that change permissions, close a long-lived
|
|
70
|
+
* session, make the bots folder, or make or change a bot or its sessions,
|
|
71
|
+
* which the owner leaves to Bot Father (#548, #615).
|
|
74
72
|
*/
|
|
75
73
|
export const KEPT_BACK = [
|
|
76
74
|
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
75
|
+
'bot change', 'session add', 'session change',
|
|
77
76
|
'permission allow', 'permission disallow', 'permission approval',
|
|
78
77
|
];
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
|
-
* The commands kit
|
|
82
|
-
* (#548). A bot may still hold their rules, which the kit names and
|
|
80
|
+
* The commands the kit gave every bot by default and that are kept back now
|
|
81
|
+
* (#548, #615). A bot may still hold their rules, which the kit names and
|
|
82
|
+
* leaves.
|
|
83
83
|
*/
|
|
84
|
-
export const NO_LONGER_DEFAULT = ['restart', 'bot create'];
|
|
84
|
+
export const NO_LONGER_DEFAULT = ['restart', 'bot create', 'bot change', 'session add', 'session change'];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The commands kit 0.26.0 gave every bot by default and that the kit no longer
|
|
88
|
+
* has (#555). A bot may still hold their rules, which the kit names and leaves
|
|
89
|
+
* (#564).
|
|
90
|
+
*/
|
|
91
|
+
export const REMOVED = ['session mail', 'session nudge'];
|