@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/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, decideLeftNudges, lookUp, noMailboxYet, sendMessage, sessionInTab, stillUnread, WATCH_MS } from './message.js';
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, unreadWords } from './retire.js';
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 long
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. A
97
- session keeps its harness: to move it, retire it
98
- and add another.
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. Or end a bot: close its tabs,
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 and
212
- tell its tab to look. A message too long to travel
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 reports back by the kit's
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 0036), and the last of two `--bots` would
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
- ...unreadLines(retired.unread),
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.unread ?? []).flatMap((one) => unreadLines(one, `${retired.bot}/${one.session}`)),
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
- ...unreadLines(retired.unread),
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
- const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
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 kit's default rules (ADR 0036), for a bot that is set up to run: one
889
- // with no instructions gets them at the rules build that gives it some.
890
- const permissions = trouble ? [] : writePermissions(bots, made.home, readBot(made.home));
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 0041): the old spelling refuses, and changes nothing.
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 0041).
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
- const tab = callerTab(bots, values.from === undefined);
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
- ...[nudgeLine(answer, where)],
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, which they can add through the kit after the user's yes.",
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 became of the line that tells the receiver to look. The message is in
1352
- * its mailbox whatever this says, so each of these is about the tab and not
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 nudgeLine(answer, where) {
1356
- const seconds = answer.watchedMs === undefined ? '' : ` in ${Math.max(Math.round(answer.watchedMs / 1000), 1)} s`;
1357
- if (answer.signal === 'orca') return ` Orca's notice reached it${seconds}, so no line was typed: it reads the mail from there.`;
1358
- if (answer.signal === 'hook') {
1359
- return answer.because === 'busy'
1360
- ? " it is busy with a turn, so nothing was typed into its tab: its own hook tells it about the mail when that turn ends."
1361
- : ` a turn of other work started in its tab${seconds}, not Orca's notice, so nothing was typed: its own hook tells it about the mail when that turn ends.`;
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.nudged) return `${head}its tab was told to look; it will read it when it is done with what it is doing.`;
1376
- if (answer.blocked !== undefined) {
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
- if (answer.nudgeLeft) {
1380
- return ` it is queued, and its tab could not be told to look from here: ${answer.nudgeTrouble}. So the nudge is left for this session's hook, which looks at the tab from outside Codex's sandbox when this command is done, and says what it did.`;
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.nudgeTrouble !== undefined) {
1383
- return ` it is queued, and its tab could not be told to look: ${answer.nudgeTrouble}`;
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 ` ${where} is not up, so nothing was typed anywhere: the message waits in its mailbox.`;
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/`. It is said, not refused: the user may ask for a folder
1396
- * anywhere (PRD 6.3), and it is written as they gave it.
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 0041): `--approval`, and an extra argument that sets
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
- if (values['extra-arg'] === undefined) return;
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
- ...unreadLines(gone.unread),
1448
+ ...processLines(gone.processes),
1463
1449
  ...leftLines(gone.promptsLeft),
1464
1450
  ]);
1465
1451
 
1466
- /** The mail a retired session did not read, as far as the kit knows (#509). */
1467
- const unreadLines = (unread, who = 'it') => (unread === undefined ? [] : [`unread ${unreadWords(unread, who)}`]);
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 0036), each rule of a command no
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 0041), and the file the allowed ones were written into. Nothing for
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 0036), and those kept back
4
- // for the user's yes (ADR 0041). A new command is on neither until someone
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 0036). Only a command named here is in the default set; one that
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
- 'bot change', 'rules build',
58
+ 'rules build',
61
59
  'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
62
- 'session add', 'session change', 'session clear', 'session compact',
60
+ 'session clear', 'session compact',
63
61
  'message to', 'message send', 'message check',
64
- 'session record', 'session sent', 'session nudge', 'session mail', 'session name', 'session mailbox',
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 0041): the ones that change permissions, close a long-lived
72
- * session, make the bots folder, or make a bot, which the owner leaves to 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 0.26.0 gave every bot by default and that are kept back now
82
- * (#548). A bot may still hold their rules, which the kit names and leaves.
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'];