@assuredloop/orca-bot-kit 0.25.3 → 0.25.4

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/SETUP.md CHANGED
@@ -201,7 +201,7 @@ waiting for:
201
201
  | Claude Code's folder trust list | `\x1b[B\r` | down, then return: its selection starts on **No, exit** | Claude Code 2.1.283 |
202
202
  | Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `\r` | return: its selection starts on **Trust and continue** | Codex 0.160.0 |
203
203
  | Codex's `Hooks need review` | `\x1b[B\r` | down, then return, to **Trust all and continue**; if the selection already starts there, return alone | Codex 0.160.0 |
204
- | Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home) | Codex 0.160.0 |
204
+ | Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home); on 0.162.0 it is called **1. Use current Git worktree** (Keep using the current working directory). Never **Create new Git worktree** | Codex 0.160.0; Codex 0.162.0 (#516) |
205
205
  | Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; seen on 0.157.1 (offering 0.158.0) with the selection on **Update now**, not pressed; 0.158.0 offered 0.160.0, taken on 2026-10-02 by Bot Father's restarts |
206
206
  | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
207
207
  | Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which cancels it (**Not now**). On 2.1.283 it is a form, not a list: `How you use Claude here`, `Also scan shell history`, `Also scan your other repos`, `Continue`, and `Enter to continue · Esc to cancel`. **Never send a return**: Enter is **Continue**, which starts a scan of the project, recent Claude sessions and, by default, the machine's shell history | Not yet proven: Esc cancels in Claude Code 2.1.283's code, not yet seen live (#370); the form was seen live twice on 2.1.283, in #261's live test and #239's live run 1 (#416); 2.1.278 showed a numbered list, answered with 2. Not now |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/clear.js CHANGED
@@ -38,9 +38,13 @@ const POINTER = { claude: '❯', codex: '›' };
38
38
  */
39
39
  const RECORD_LINE = 'obk: this conversation was started by obk session clear, and this line is only so the kit can record it. Reply "ok"; nothing else is asked.';
40
40
 
41
- /** What Codex asks after `/new`, and the one answer the kit gives it: the bot home it runs in. */
41
+ /**
42
+ * What Codex asks after `/new`, and the one answer the kit gives it: the bot
43
+ * home it runs in. Codex 0.162.0 calls it by another name, and its whole row
44
+ * has to be as captured, but for the padding before its description (#516).
45
+ */
42
46
  const WHERE_TO_RUN = 'Where should the new conversation run?';
43
- const CURRENT_CHECKOUT = /^ *› +1\. Current checkout\b/;
47
+ const CURRENT_CHECKOUT = /^ *› +1\. Current checkout\b|^ *› 1\. Use current Git worktree\s+Keep using the current working directory *$/;
44
48
 
45
49
  /** Codex's empty input line: its pointer alone, or with its placeholder. */
46
50
  const CODEX_EMPTY = ['›', '› Ask Codex to do anything'];
@@ -51,7 +55,8 @@ const CODEX_EMPTY = ['›', '› Ask Codex to do anything'];
51
55
  * selected row stands for what Return will run: with the menu open, Return
52
56
  * runs the selected command, and the menu is narrowed to what was typed
53
57
  * (codex-rs/tui/src/bottom_pane/chat_composer.rs, rust-v0.160.0). The
54
- * architect's ruling on #391, after live run 4.
58
+ * architect's ruling on #391, after live run 4. Only where Orca gives no
59
+ * draft: a draft is the line's text, and has to be the command (#516).
55
60
  */
56
61
  const CODEX_BARE_LINE = '0.160.0';
57
62
 
@@ -221,14 +226,13 @@ async function enter(it, verb, before = () => {}) {
221
226
  * when it was typed.
222
227
  */
223
228
  export async function typeCommand(it, command, { before = () => {}, check = menuWrong, cannot = () => '', deadline = Infinity } = {}) {
224
- // On Codex, what is typed cannot be read back once its menu is open, so
225
- // nothing may be in its input line before: no draft for the command to join.
226
- // On Claude Code, Orca gives a draft in the line beside the screen (#510).
229
+ // Nothing may be in the input line before: no draft for the command to join.
230
+ // Orca gives a draft in the line beside the screen, and the screen's line
231
+ // then reads the pointer alone (#510, #516). Where it gives none, Codex's
232
+ // line on the screen has to be empty.
227
233
  const emptyLine = ({ rows, draft }) => {
228
- if (it.harness !== 'codex') {
229
- if (draft !== undefined) throw new Error(`${it.name}: nothing was typed, because its input line holds a draft: it reads "${draft}".${shownEnd(rows)}`);
230
- return;
231
- }
234
+ if (draft !== undefined) throw new Error(`${it.name}: nothing was typed, because its input line holds a draft: it reads "${draft}".${shownEnd(rows)}`);
235
+ if (it.harness !== 'codex') return;
232
236
  const line = rows.findLast((row) => /^ *›/.test(row))?.trim();
233
237
  if (!CODEX_EMPTY.includes(line)) {
234
238
  throw new Error(`${it.name}: nothing was typed, because its input line is not empty: it reads "${line ?? 'nothing'}".${shownEnd(rows)}`);
@@ -415,10 +419,11 @@ function menuWrong(rows, harness, command, version, draft) {
415
419
  if (at < 0) return { why: 'its screen shows no input line' };
416
420
  // Claude Code 2.1.288 puts a non-breaking space after its pointer (live run 4).
417
421
  const line = rows[at].replaceAll('\u00a0', ' ').trim();
418
- if (harness === 'codex') return codexMenuWrong(rows, at, line, command, version);
419
- // Orca 1.4.223 gives the line's text as the draft, and Claude Code 2.1.296's
420
- // line on the screen then reads its pointer alone (#510, probe 4).
422
+ // Orca 1.4.223 gives the line's text as the draft, and the line on the
423
+ // screen then reads the pointer alone: Claude Code 2.1.296's (#510, probe 4)
424
+ // and Codex 0.162.0's (#516).
421
425
  const text = draft === undefined ? line : `${POINTER[harness]} ${draft}`;
426
+ if (harness === 'codex') return codexMenuWrong(rows, at, text, command, version);
422
427
  if (text !== `${POINTER[harness]} ${command}`) return { why: `its input line reads "${text}"` };
423
428
  return claudeMenuWrong(rows, at, command);
424
429
  }
@@ -445,11 +450,12 @@ function claudeMenuWrong(rows, at, command) {
445
450
 
446
451
  /**
447
452
  * Codex draws its slash menu above the input line: a row per command, the
448
- * selected one starting with its pointer, then a blank row, then the input
449
- * line (its own snapshot tests, chat_composer slash_popup_*.snap,
450
- * rust-v0.160.0; live run 4). The menu has to hold one command row, the
451
- * selected one, naming the command. The input line reads the command, or, on
452
- * the Codex whose line Orca shows bare, its pointer alone.
453
+ * selected one starting with its pointer, then a blank row on 0.160.0 and none
454
+ * on 0.162.0, then the input line (its own snapshot tests, chat_composer
455
+ * slash_popup_*.snap, rust-v0.160.0; live run 4; #516's probe). The menu has
456
+ * to hold one command row, the selected one, naming the command. The input
457
+ * line, Orca's draft where it gives one, reads the command, or, with no draft
458
+ * on the Codex whose line Orca shows bare, its pointer alone.
453
459
  */
454
460
  function codexMenuWrong(rows, at, line, command, version) {
455
461
  const bare = line === '›' && version === CODEX_BARE_LINE;
@@ -494,7 +500,7 @@ async function answerWhereToRun(it, handle, command) {
494
500
  const pointer = rows.findLast((row) => /^ *›/.test(row));
495
501
  if (pointer === undefined || !CURRENT_CHECKOUT.test(pointer)) {
496
502
  send(handle, '\x1b');
497
- throw new Error(`${it.name}: after ${command}, Codex asked "${WHERE_TO_RUN}" with its selection on "${pointer?.trim() ?? 'nothing'}" rather than "1. Current checkout", so it was backed out of with Esc and nothing else was typed. Look at its tab.`);
503
+ throw new Error(`${it.name}: after ${command}, Codex asked "${WHERE_TO_RUN}" with its selection on "${pointer?.trim() ?? 'nothing'}" rather than "1. Current checkout" (on Codex 0.162.0, "1. Use current Git worktree"), so it was backed out of with Esc and nothing else was typed. Look at its tab.`);
498
504
  }
499
505
  send(handle, '\r');
500
506
  return;
package/src/cli.js CHANGED
@@ -1203,6 +1203,7 @@ const commands = {
1203
1203
  const said = {
1204
1204
  made: `${who} has its mailbox ${answer.mailbox}, made in this tab.`,
1205
1205
  bound: `${who}'s mailbox ${answer.mailbox} is bound to this tab.`,
1206
+ replaced: `${who}'s mailbox ${answer.replaced} is not one this Orca has, as when the book was written on another machine, so it has a new one, ${answer.mailbox}, made in this tab and written into the book.`,
1206
1207
  none: `${who} gets no mailbox: it is a Codex session with its sandbox switch off, which could not read one.`,
1207
1208
  };
1208
1209
  return { answer, lines: [said[answer.change]] };
package/src/health.js CHANGED
@@ -26,7 +26,7 @@ import { botDir, botNames, botsDir, readBot, tempRoles, unknownKeys } from './bo
26
26
  import { transcriptsIn } from './conversations.js';
27
27
  import { hookTrouble } from './hooks.js';
28
28
  import { bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
29
- import { frontOfTab, orcaDefaultArgs, projects, tabs, wordsOfProcess } from './orca.js';
29
+ import { frontOfTab, orcaDefaultArgs, projects, runMissing, tabs, wordsOfProcess } from './orca.js';
30
30
  import { permissionsTrouble } from './permissions.js';
31
31
  import { TAB_ENV } from './record.js';
32
32
  import { agentsTrouble, rulesStamp } from './rules.js';
@@ -387,6 +387,21 @@ function inOrca(bots, home, bot, setups, sessions) {
387
387
  found.push(finding('session', entry.tab, `${bot.name}'s session ${name} is in the book with tab ${entry.tab}, and Orca has no tab of that id: the tab was closed, or the machine was restarted. The conversation is in the book, and obk up opens a tab and brings it back.`, bot.name));
388
388
  }
389
389
 
390
+ // A Run Orca does not have, as when the book was written on another
391
+ // machine: mail sent to it is refused. The session's next start makes it
392
+ // a new one (#508).
393
+ if (typeof entry?.mailbox === 'string' && runMissing(entry.mailbox)) {
394
+ // The bot's unpause leaves a session paused on its own as it is, so a
395
+ // session under both marks needs both commands.
396
+ const unpause = `${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${bot.name}`;
397
+ const steps = [
398
+ ...(bot.paused === true ? [unpause] : []),
399
+ ...(bot.sessions.some((session) => session.name === name && session.paused === true) ? [`${unpause} --session ${name}`] : []),
400
+ ];
401
+ const fix = steps.length > 0 ? steps.join(', then ') : `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${bot.name} --session ${name}`;
402
+ found.push(finding('session', bookFile(home), `${bot.name}'s session ${name} has the mailbox ${entry.mailbox} in the book, and Orca has no Run of that id: it was made by another Orca, as on another machine, so mail sent to it is refused. The session's next start makes it a new one: ${fix}`, bot.name));
403
+ }
404
+
390
405
  const unclaimed = (Array.isArray(entry?.unclaimed) ? entry.unclaimed : []).filter((id) => !helpers.has(id));
391
406
  if (unclaimed.length > 0) {
392
407
  found.push(finding('leftover', bookFile(home), `The book notes conversations under ${bot.name}'s session ${name} that no session of this bot claims: ${unclaimed.join(', ')}. They ran in this bot's folder, and the kit will not say whose they were. To give one back, write it into ${bookFile(home)} under ${name} as session: <id> and run obk up again.`, bot.name));
package/src/name.js CHANGED
@@ -125,12 +125,13 @@ function nameOf(thread) {
125
125
  * pointer and the command exactly, and no row of its slash menu stands above
126
126
  * it. With words after the command Codex has closed its menu (chat_composer.rs
127
127
  * `sync_command_popup`, rust-v0.160.0), so a menu still open means the line is
128
- * not what it seems.
128
+ * not what it seems. Where Orca gives a draft, the draft is the line's text:
129
+ * Codex 0.162.0's line on the screen then reads its pointer alone (#516).
129
130
  */
130
- function renameWrong(rows, _harness, command) {
131
+ function renameWrong(rows, _harness, command, _version, draft) {
131
132
  const at = rows.findLastIndex((row) => /^ *›/.test(row));
132
133
  if (at < 0) return { why: 'its screen shows no input line' };
133
- const line = rows[at].replaceAll(' ', ' ').trim();
134
+ const line = draft === undefined ? rows[at].replaceAll(' ', ' ').trim() : `› ${draft}`;
134
135
  if (line !== `› ${command}`) return { why: `its input line reads "${line}"` };
135
136
  const above = rows.slice(0, at).findLast((row) => row.trim() !== '');
136
137
  if (above !== undefined && /^ *(?:› +)?\/\S/.test(above)) return { why: `a menu of commands stands above its input line: "${above.trim()}"` };
package/src/orca.js CHANGED
@@ -1017,6 +1017,22 @@ export const useMailbox = (id, handle, options) =>
1017
1017
  export const coordinatorOf = (id, options) =>
1018
1018
  orca(['orchestration', 'run-show', '--id', id], options).run.coordinator_handle ?? undefined;
1019
1019
 
1020
+ /**
1021
+ * Whether this Orca has no Run of that id at all, as when the book was written
1022
+ * on another machine (#508). Only `run-show` refused `run_not_found` says so:
1023
+ * `run-use` gives a legacy Run the same code and words, and a Run shown, a
1024
+ * time-out or any other refusal is not a Run known to be missing (tech notes,
1025
+ * section 1).
1026
+ */
1027
+ export function runMissing(id, options) {
1028
+ try {
1029
+ orca(['orchestration', 'run-show', '--id', id], options);
1030
+ return false;
1031
+ } catch (error) {
1032
+ return error.code === 'run_not_found';
1033
+ }
1034
+ }
1035
+
1020
1036
  /** Queue one message. `to` and `from` are mailboxes, written `run:<id>`. */
1021
1037
  export function postMessage({ to, from, subject, body, type = 'status', thread }) {
1022
1038
  const args = ['orchestration', 'send', '--to', to, '--subject', subject, '--body', body, '--type', type];
package/src/up.js CHANGED
@@ -14,7 +14,7 @@ import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from
14
14
  import { installHook } from './hooks.js';
15
15
  import { writePermissions } from './permissions.js';
16
16
  import { addressOf, harnessOf, isAddressOf, isShortPrompt, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
17
- import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
17
+ import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
18
18
  import { TAB_ENV } from './record.js';
19
19
  import { buildAgents, rulesStamp } from './rules.js';
20
20
  import { linkSkills } from './skills.js';
@@ -648,7 +648,8 @@ async function writeAddress(home, session, address) {
648
648
  * a restart (tech notes, section 1). Orca offers no way to delete one, so this
649
649
  * makes one for a session whose book names none, and otherwise binds the one
650
650
  * the book names to this tab: after a restart, or a closed tab brought back,
651
- * it would stay with the tab that is gone (review of PR #248, finding 1).
651
+ * it would stay with the tab that is gone (review of PR #248, finding 1). A
652
+ * Run the book names and this Orca does not have at all is replaced (#508).
652
653
  *
653
654
  * Only inside an Orca tab. Asked from anywhere else, Orca picks a terminal
654
655
  * itself, and seen live that was not the asker's (tech notes, section 1), so
@@ -705,20 +706,34 @@ async function mailboxInTurn(home, botName, sessionName, who) {
705
706
  if (error.code === TIMED_OUT) {
706
707
  throw new Error(`${error.message}\n${who}'s mailbox ${held} may now be bound to this tab, or may still be bound where it was: Orca did not say which.`);
707
708
  }
708
- throw new Error(`${error.message}\n${who}'s mailbox ${held} is unchanged: it is still bound to ${boundTo(held)}.`);
709
+ // A Run this Orca has never had, as when the book was written on another
710
+ // machine, can never be bound, so every start would fail the same way.
711
+ // It is replaced as a session with none would be given one (#508).
712
+ if (!runMissing(held, STEP_WAIT)) {
713
+ throw new Error(`${error.message}\n${who}'s mailbox ${held} is unchanged: it is still bound to ${boundTo(held)}.`);
714
+ }
715
+ return newMailbox(home, botName, sessionName, who, here, held);
709
716
  }
710
717
  return { bot: botName, session: sessionName, mailbox: held, change: 'bound' };
711
718
  }
719
+ return newMailbox(home, botName, sessionName, who, here);
720
+ }
712
721
 
722
+ /**
723
+ * Make a mailbox in this tab and write it into the book, for a session whose
724
+ * book names none, or names `stale`, a Run this Orca does not have.
725
+ */
726
+ async function newMailbox(home, botName, sessionName, who, here, stale) {
713
727
  // Made outside the book's lock, which is held for one read and one write
714
728
  // (book.js).
715
729
  let made;
716
730
  try {
717
731
  made = makeMailbox(who, STEP_WAIT);
718
732
  } catch (error) {
733
+ const written = stale === undefined ? 'none is written down' : `the book still names ${stale}, which this Orca does not have`;
719
734
  const left = error.code === TIMED_OUT
720
- ? `None is written down for ${who}; a Run Orca made after the kit stopped waiting is left unused.`
721
- : `No mailbox was made for ${who}, and none is written down.`;
735
+ ? `For ${who}, ${written}; a Run Orca made after the kit stopped waiting is left unused.`
736
+ : `No mailbox was made for ${who}, and ${written}.`;
722
737
  throw new Error(`${error.message}\n${left} It gets one the next time the kit starts it.`);
723
738
  }
724
739
 
@@ -734,10 +749,11 @@ async function mailboxInTurn(home, botName, sessionName, who) {
734
749
  movedTo = now.tab ?? 'none';
735
750
  return undefined;
736
751
  }
737
- // Under the lock, and only if the book still has none: two runs at once
738
- // would each have made one, and a session with two mailboxes is a session
739
- // half its mail never reaches. The loser's Run is left unused.
740
- if (typeof now.mailbox !== 'string') now.mailbox = made;
752
+ // Under the lock, and only if the book still has none, or still the
753
+ // stale one: two runs at once would each have made one, and a session
754
+ // with two mailboxes is a session half its mail never reaches. The
755
+ // loser's Run is left unused.
756
+ if (typeof now.mailbox !== 'string' || now.mailbox === stale) now.mailbox = made;
741
757
  kept = now.mailbox;
742
758
  current.sessions[sessionName] = now;
743
759
  return undefined;
@@ -750,8 +766,13 @@ async function mailboxInTurn(home, botName, sessionName, who) {
750
766
  }
751
767
  // One terminal holds one Run, so making the loser took this tab off the one
752
768
  // the book kept. It goes back.
753
- if (kept !== made) useMailbox(kept, undefined, STEP_WAIT);
754
- return { bot: botName, session: sessionName, mailbox: kept, change: kept === made ? 'made' : 'bound' };
769
+ if (kept !== made) {
770
+ useMailbox(kept, undefined, STEP_WAIT);
771
+ return { bot: botName, session: sessionName, mailbox: kept, change: 'bound' };
772
+ }
773
+ return stale === undefined
774
+ ? { bot: botName, session: sessionName, mailbox: made, change: 'made' }
775
+ : { bot: botName, session: sessionName, mailbox: made, change: 'replaced', replaced: stale };
755
776
  }
756
777
 
757
778
  /**