@assuredloop/orca-bot-kit 0.14.0 → 0.15.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/SETUP.md CHANGED
@@ -199,7 +199,7 @@ waiting for:
199
199
  | Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home) | Codex 0.157.1 |
200
200
  | Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; 0.157.1 has had no newer release to offer |
201
201
  | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
202
- | Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which is **Not now**: it would write settings of theirs | Not yet proven: Esc is Not now in Claude Code 2.1.283's code; last seen live on 2.1.278, answered with 2. Not now |
202
+ | Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which cancels it. 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 on 2.1.283 (#416); 2.1.278 showed a numbered list, answered with 2. Not now |
203
203
 
204
204
  Each key counts on where the selection starts, as it did on the version named.
205
205
  Read the screen in front of you before you send: if the selection is somewhere
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -289,7 +289,7 @@ otherwise `/Applications/Orca.app/Contents/Resources/bin/orca`. A bare
289
289
  | Codex's `Where should the new conversation run?` | `\r` | **Current checkout** | Codex 0.157.1 |
290
290
  | Codex's update offer, `1. Update now` | `\r` | **Update now**, where it starts | Codex 0.156.1 |
291
291
  | `[oh-my-zsh] Would you like to update?` | `n` | no: the user updates it | oh-my-zsh bf77e35 |
292
- | Claude Code's `Teach auto mode…` | `\x1b` | Esc, **Not now** | Not yet proven, below |
292
+ | Claude Code's `Teach auto mode…` | `\x1b` | Esc cancels; no return | Not yet proven, below |
293
293
 
294
294
  Each key counts on where the selection starts on the version named. Read the
295
295
  screen before you send: if the selection is elsewhere or the options have
@@ -298,9 +298,11 @@ return: on Codex's update offer that took the highlighted **Update now**,
298
298
  whatever the digit was. Codex's trust said `1. Yes, continue` on older
299
299
  versions. Its hooks review sometimes starts on `2` already: then return alone.
300
300
  0.157.1 has had no newer release to offer, so its update offer was proven on
301
- 0.156.1. Claude Code's `Teach auto mode` has not been seen live on 2.1.283:
302
- Esc is Not now in its code, and 2.1.278 was answered with `2. Not now`.
303
- Not now, because Yes writes the user's settings. Codex's hooks
301
+ 0.156.1. Claude Code's `Teach auto mode` on 2.1.283 is a form, not a list,
302
+ ending `Enter to continue · Esc to cancel`: never send it a return, since
303
+ Enter is Continue, which scans the project, recent sessions and the machine's
304
+ shell history. Esc cancels in its code; that is not yet proven live. On 2.1.278
305
+ it was a numbered list, answered with `2. Not now`. Codex's hooks
304
306
  question matters most. The kit's hook is how the book learns which
305
307
  conversation the session is running, and until it is answered the
306
308
  conversation has not started. Codex's trust applies to the repository root,
@@ -379,12 +379,16 @@ Write the regression test before the fix, at a seam where it exercises the bug
379
379
  the way it actually happened; if there is no correct seam, that is itself the
380
380
  finding.
381
381
 
382
- The test is the separate author's to write, as it is for any other behaviour,
383
- and your reproduction is what the brief is made from: `obk-tdd` has what that
384
- costs and how the brief goes. Two are often better than one: one at the
385
- interface saying what a caller should have got, and the smallest one that shows
386
- the fault where it lives. A test that now looks wrong goes back to its author;
387
- it is not edited to fit the fix.
382
+ The test is the separate author's to write, as it is for any other behaviour:
383
+ someone with a context of their own, such as a fresh subagent, another session
384
+ or a person, never a fork of this conversation. Give them the requirement and
385
+ what the correct result is, the public interface, and your reproduction as the
386
+ worked example; not your fix, your diff or your plan. They return the tests, the
387
+ command, and the failing output, failing because the bug is there rather than
388
+ because of a typo. `obk-tdd` has more on the brief, if you can load it. Two are
389
+ often better than one: one at the interface saying what a caller should have
390
+ got, and the smallest one that shows the fault where it lives. A test that now
391
+ looks wrong goes back to its author; it is not edited to fit the fix.
388
392
 
389
393
  Watch it fail. Fix. Watch it pass. Then run the original, unminimised scenario
390
394
  again, on the same surface the bug appeared on. A different surface, or an
@@ -227,6 +227,18 @@ ask for and you do it.
227
227
  A grooming report is input to this review, not its end: check what it flags
228
228
  against the fleet, and recommend from there.
229
229
 
230
+ When a grooming report names temporary sessions a bot left behind, offer the
231
+ user one rule for that bot's charter, with the words ready to accept, such as:
232
+ "Retire each temporary session you make as soon as its work is done, and
233
+ before you end a piece of work, check that none of yours is left." On a yes,
234
+ read the bot's charter, add the line, and set the whole with `obk bot change
235
+ --bots <folder> --bot <bot> --charter <text>`, which replaces the charter, so
236
+ what you pass is all of it. On a no, nothing changes, and it is not offered
237
+ again for the same sessions: write the declined offer in grooming's open
238
+ findings, next to those sessions, so it outlasts this conversation. Retire
239
+ none of them yourself, and send nothing to their makers: the hint is for the
240
+ user, and only a maker knows when its work is done.
241
+
230
242
  Sort the problem before you write it up. Something the kit does wrong, or
231
243
  cannot do, is filed where the kit's problems are filed, as one issue, in one
232
244
  step: no draft to approve first. Something a bot is doing wrong goes back to
@@ -73,7 +73,7 @@ words where they are quoted here:
73
73
  against the session log (19:12:40, 19:15:38), which is the fix the skill
74
74
  gives.
75
75
 
76
- Three deliberate departures from `grilling` and `domain-modeling`, so they are
76
+ Five deliberate departures from `grilling` and `domain-modeling`, so they are
77
77
  not mistaken for consolidation:
78
78
 
79
79
  - **A round is three questions, not the whole frontier.** `grilling` says to
@@ -185,6 +185,25 @@ Never edit a memory yourself, not even to cut it. The memory is the bot's, the
185
185
  harness keeps it, and a pass that rewrites what a bot remembers is a pass that
186
186
  decides what the bot knows.
187
187
 
188
+ ## Temporary sessions left behind
189
+
190
+ A temporary session is one a bot's own session made for a piece of work with
191
+ `obk temp make`, and its maker retires it when the work is done. Each bot's
192
+ book, `sessions.yaml` in its folder, marks one on its entry as `temporary`,
193
+ with its `maker` and when it was `made`. Read the books every run, a quiet one
194
+ included: a session left behind does no work, so the usage index never shows
195
+ it.
196
+
197
+ Count one as left behind when its maker is no longer one of the bot's
198
+ sessions, or when it was made more than a day ago. The report names each: the
199
+ bot and session, its maker, saying so where the maker is gone, and how old it
200
+ is. Put them in the open findings, so the next run says whether each is still
201
+ there rather than reporting it as new.
202
+
203
+ Retire none of them. Only the maker knows when the work is done: a review
204
+ session can be waiting on an open pull request for a re-review. What to do
205
+ about a bot that leaves them behind is the management session's to offer.
206
+
188
207
  ## Say whether this run was worth making
189
208
 
190
209
  Every run, record which of three things happened: it found something that
package/src/bot.js CHANGED
@@ -8,6 +8,7 @@
8
8
  // writes nothing, rather than leave a bot half made or a bot.yaml half edited.
9
9
 
10
10
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, statSync, writeFileSync } from 'node:fs';
11
+ import os from 'node:os';
11
12
  import path from 'node:path';
12
13
  import { isDeepStrictEqual } from 'node:util';
13
14
  import { parse, parseDocument, stringify } from 'yaml';
@@ -114,6 +115,25 @@ function whereItLeads(target) {
114
115
  export const displayName = (name) =>
115
116
  name.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
116
117
 
118
+ /**
119
+ * What Orca is asked to call a bot's project. A fleet whose bots folder is under
120
+ * the system's temp folder, as a system test's or a live check's is, gets a mark
121
+ * and the name of the first folder under the temp folder, so its Bot Father is
122
+ * never taken for the owner's in Orca's sidebar (#401). Every other fleet's
123
+ * project is called what it always was. Tab titles keep `displayName` either way.
124
+ */
125
+ export function projectName(bots, name) {
126
+ let under;
127
+ try {
128
+ under = path.relative(realpathSync(os.tmpdir()), realpathSync(bots));
129
+ } catch {
130
+ return displayName(name);
131
+ }
132
+ // The temp folder itself, or anywhere outside it, is not a throwaway fleet's.
133
+ if (under === '' || under === '..' || under.startsWith(`..${path.sep}`) || path.isAbsolute(under)) return displayName(name);
134
+ return `${displayName(name)} · temp fleet ${under.split(path.sep)[0]}`;
135
+ }
136
+
117
137
  /**
118
138
  * A bot's own file: which harness its sessions run on unless they say
119
139
  * otherwise, and the sessions it keeps.
@@ -1,5 +1,5 @@
1
1
  -- Run by the kit as `osascript orca-reload.applescript <Orca.app path>`, after
2
- -- a project was removed (#343, ADR 0025).
2
+ -- a project was removed (#343, ADR 0031).
3
3
  --
4
4
  -- Orca's window keeps a removed project in its sidebar until the window is
5
5
  -- rebuilt (stablyai/orca#23224; once a release fixes it, this can go behind a
@@ -2,7 +2,7 @@
2
2
  // CLI: `ELECTRON_RUN_AS_NODE=1 <Orca> orca-runtime.cjs <client> <method> <params> <wait>`.
3
3
  //
4
4
  // It makes one call to Orca's runtime, through Orca's own client, for a method
5
- // Orca's CLI does not offer (ADR 0025): `project.update`, which makes Orca's
5
+ // Orca's CLI does not offer (ADR 0031): `project.update`, which makes Orca's
6
6
  // window read its projects again (#224), and `terminal.inspectProcess`, which
7
7
  // says who is in front of a tab where `ps` cannot (#298). `params` is JSON. It
8
8
  // prints the runtime's answer as JSON and exits 0 only when the runtime took
package/src/orca.js CHANGED
@@ -1,4 +1,4 @@
1
- // Every call the kit makes to Orca goes through here (ADR 0025). Orca's CLI
1
+ // Every call the kit makes to Orca goes through here (ADR 0031). Orca's CLI
2
2
  // changes often, so the kit reads `--json` and never the human text, and keeps
3
3
  // the parsing in one place.
4
4
  //
@@ -13,6 +13,9 @@ import { homedir } from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
 
16
+ import { SHELL_ENV } from './launch.js';
17
+ import { TAB_ENV } from './record.js';
18
+
16
19
  // The Orca that works for a normal user: `/usr/local/bin/orca` is a root-only
17
20
  // symlink on this machine (tech notes, section 1). OBK_ORCA overrides it, for a
18
21
  // machine that keeps Orca somewhere else.
@@ -40,10 +43,14 @@ export function orca(args, { timeoutMs } = {}) {
40
43
  /** What an error carries when Orca was given a time and did not answer in it. */
41
44
  export const TIMED_OUT = 'orca_timed_out';
42
45
 
43
- /** Orca's refusal as an error, with its code on it, so a caller can tell a stale handle from the rest (#294). */
46
+ /**
47
+ * Orca's refusal as an error, with its code on it, so a caller can tell a stale
48
+ * handle from the rest (#294), and its own message as `reason`, so a caller
49
+ * that goes by the message reads Orca's words rather than the kit's (#405).
50
+ */
44
51
  const refusal = (args, answer) => Object.assign(
45
52
  new Error(`Orca refused ${args.join(' ')}: ${answer.error?.message ?? 'no reason given'}`),
46
- { code: answer.error?.code },
53
+ { code: answer.error?.code, reason: answer.error?.message },
47
54
  );
48
55
 
49
56
  /** One Orca command, and the whole envelope back, refusals included. */
@@ -195,7 +202,7 @@ const CLIENT_KILL_MS = 3000;
195
202
  *
196
203
  * Orca's window re-reads only when its runtime says the projects changed, and
197
204
  * `setup-update` and `setup-delete` do not say so. `project.update` with no
198
- * changes does, and Orca's CLI does not offer it (ADR 0025). The caller prints
205
+ * changes does, and Orca's CLI does not offer it (ADR 0031). The caller prints
199
206
  * `RELOAD_LINE` either way. It does not take a removed project out of the
200
207
  * sidebar: `reloadWindow` does.
201
208
  */
@@ -206,7 +213,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
206
213
  * runtime's answer, or undefined when there is none to be had.
207
214
  *
208
215
  * It goes through Orca's own runtime client out of the installed app, run by
209
- * Orca's binary the way its `bin/orca` runs its CLI (ADR 0025). None of that is
216
+ * Orca's binary the way its `bin/orca` runs its CLI (ADR 0031). None of that is
210
217
  * Orca's published interface, so anything that goes wrong is a quiet
211
218
  * undefined: it never throws and is never tried twice.
212
219
  */
@@ -243,7 +250,7 @@ const RELOAD_KILL_MS = 5000;
243
250
 
244
251
  /**
245
252
  * Have Orca's window force-reload itself, after a project was removed: true
246
- * only when it did (#343, ADR 0025).
253
+ * only when it did (#343, ADR 0031).
247
254
  *
248
255
  * The window keeps a removed project in its sidebar until it is rebuilt, and
249
256
  * `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
@@ -369,6 +376,9 @@ function screenOf(handle) {
369
376
  /** A row the harness starts with its selection pointer: `›` on Codex, `❯` on Claude Code. */
370
377
  const POINTER_ROW = /^ *[›❯]/;
371
378
 
379
+ /** How a harness's form says it is answered, as Claude Code 2.1.283 draws its foot (#416). */
380
+ const FORM_FOOT = /Enter to (?:continue|confirm|select)\b.*Esc to cancel/i;
381
+
372
382
  /** That pointer on a numbered choice, and the column its number starts in. */
373
383
  const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
374
384
 
@@ -376,7 +386,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
376
386
  * Whether the rows of a rendered screen hold a question of the harness's own.
377
387
  *
378
388
  * Every one seen is a numbered list of choices with the harness's pointer on
379
- * one (tech notes, section 1; ADR 0025). The same pointer starts the harness's
389
+ * one (tech notes, section 1; ADR 0031). The same pointer starts the harness's
380
390
  * input line and its echo of the user's past turns, and the input line is the
381
391
  * lowest of them whenever it is on screen, so only the lowest pointer row is
382
392
  * asked about: a question counts while it stands in the input line's place.
@@ -385,6 +395,13 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
385
395
  */
386
396
  export function questionIn(rows) {
387
397
  const at = rows.findLastIndex((row) => POINTER_ROW.test(row));
398
+ // A form of the harness's own has no numbers but a foot that says how to
399
+ // answer it: Claude Code 2.1.283's "Teach auto mode" ends `Enter to continue
400
+ // · Esc to cancel`, and a return there is Continue, which starts a scan
401
+ // (#416). It counts on or below the lowest pointer row, or anywhere when
402
+ // there is none: under old words of one in the history, the input line is
403
+ // the lowest pointer, and nothing below it is a foot.
404
+ if (rows.slice(Math.max(at, 0)).some((row) => FORM_FOOT.test(row))) return true;
388
405
  const pointer = at < 0 ? null : ON_A_CHOICE.exec(rows[at]);
389
406
  if (pointer === null) return false;
390
407
  const column = pointer[1].length;
@@ -431,9 +448,12 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
431
448
  // there. The program in front has to be the agent Orca names. With no name
432
449
  // it may be a harness a few seconds into its launch; under another name it
433
450
  // may be a pager started after the harness quit, under an identity Orca has
434
- // not let go of (tech notes, section 1). A harness run through a wrapper such
435
- // as `node` lands here too, until #261.
436
- if (seen.front === undefined || seen.agent === undefined || seen.command !== seen.agent) {
451
+ // not let go of (tech notes, section 1). A harness run under another name,
452
+ // such as `node` for an npm install, passes on the kit's own mark instead
453
+ // (#261).
454
+ // Either way Orca has to name an agent there: a harness it has not named yet
455
+ // may be seconds into its launch and not ready for a line.
456
+ if (seen.front === undefined || seen.agent === undefined || (seen.command !== seen.agent && !launchedHere(seen.pid, tabId))) {
437
457
  const why = seen.front === undefined
438
458
  ? seen.unreadable
439
459
  : `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
@@ -449,6 +469,25 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
449
469
  return { handle: live.handle, agent: seen.agent };
450
470
  }
451
471
 
472
+ /**
473
+ * Whether the process `pid`, in front of the tab `tabId`, is the harness the
474
+ * kit's launch line started there, whatever it is called (#261). The line sets
475
+ * `OBK_TAB_SHELL` to the tab's shell's pid, for the harness alone and not
476
+ * exported, so the harness carries it, with its parent that shell. A program
477
+ * the shell starts later, such as `less` after the harness quit (#232), has no
478
+ * mark. A program the harness starts carries the mark, and its parent is the
479
+ * harness. A tab Orca restored by itself has no mark at all. Only whole words
480
+ * of the environment count, and anything unread is a no.
481
+ */
482
+ function launchedHere(pid, tabId) {
483
+ if (pid === undefined) return false;
484
+ const words = wordsOfProcess(pid);
485
+ if (words === undefined || !words.includes(`${TAB_ENV}=${tabId}`)) return false;
486
+ const marks = words.filter((word) => new RegExp(`^${SHELL_ENV}=[0-9]+$`).test(word));
487
+ const own = psLine(pid);
488
+ return marks.length === 1 && own !== undefined && Number(marks[0].slice(SHELL_ENV.length + 1)) === own.ppid;
489
+ }
490
+
452
491
  /**
453
492
  * Who holds the terminal of the tab `handle`, as `frontOf` answers, without
454
493
  * waiting on the tab: for a caller that asks about every tab and has nothing
@@ -475,7 +514,7 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
475
514
  *
476
515
  * `ps` is asked first. Where it cannot read the tab, as inside Codex's
477
516
  * sandbox, where it does not start at all, Orca's runtime is asked instead
478
- * (#298, ADR 0025).
517
+ * (#298, ADR 0031).
479
518
  */
480
519
  function frontOf(handle, ptyId) {
481
520
  const read = frontByPs(ptyId);
@@ -511,7 +550,7 @@ function frontByOrca(handle) {
511
550
  * Who holds the terminal of the pane `ptyId`, as `ps` says it.
512
551
  *
513
552
  * Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
514
- * gives that pid's terminal's foreground process group (ADR 0025).
553
+ * gives that pid's terminal's foreground process group (ADR 0031).
515
554
  * On macOS the pane is `login` with the shell as its child, so the shell is in
516
555
  * front when the group is the pane's own or that of a child of a `login` pane.
517
556
  * `diagnostics memory` is a diagnostics command and may change, so everything
@@ -563,14 +602,25 @@ function psLine(pid) {
563
602
  /**
564
603
  * Close one tab, by the handle Orca issued for it.
565
604
  *
566
- * The only place in the kit that takes anything away, and it takes away one
567
- * tab: `--terminal <handle> --tab`. Orca's other form, `--worktree <sel> --all`,
605
+ * With `closeTerminal` below, the only place in the kit that takes anything
606
+ * away, and each takes away one tab: `--terminal <handle> --tab`. Orca's other
607
+ * form, `--worktree <sel> --all`,
568
608
  * closes every tab of a project with its layouts and resume records, which are
569
609
  * the user's work and not the kit's to end (tech notes, section 1).
570
610
  */
571
611
  export const closeTab = (handle) =>
572
612
  orca(['terminal', 'close', '--terminal', handle, '--tab']).close;
573
613
 
614
+ /**
615
+ * Close one terminal without `--tab`: for a tab of one pane, as every tab the
616
+ * kit opens is, Orca then closes that tab by its id and stops its process (read
617
+ * in the 1.4.215 bundle). Only for a tab `--tab` could not find (#405): Orca
618
+ * looks a `--tab` close up in a tab snapshot of its own, which a tab it still
619
+ * lists can be missing from after a restart, and answers `tab_not_found`.
620
+ */
621
+ export const closeTerminal = (handle) =>
622
+ orca(['terminal', 'close', '--terminal', handle]).close;
623
+
574
624
  /**
575
625
  * Type `text` into a tab and press return.
576
626
  *
package/src/pause.js CHANGED
@@ -14,7 +14,7 @@ import { realpathSync } from 'node:fs';
14
14
 
15
15
  import { botDir, markPaused, readBot } from './bot.js';
16
16
  import { DAILY_SESSION } from './init.js';
17
- import { closeTabs, tabsToClose } from './restart.js';
17
+ import { closeTabs, commandLine, tabsToClose } from './restart.js';
18
18
  import { BOT_FATHER, botsNamed, bringUp, sessionsOf } from './up.js';
19
19
 
20
20
  /**
@@ -30,7 +30,7 @@ export async function pauseSessions(bots, { bot, session }) {
30
30
  // they were.
31
31
  const going = tabsToClose(bots, bot, home, sessions);
32
32
  const changed = markPaused(bots, bot, session, true);
33
- const closed = await closeTabs(home, going, bots, bot);
33
+ const closed = await closeTabs(home, going, bots, bot, commandLine('pause', bots, bot, session));
34
34
  return { bot, session, changed, closed };
35
35
  }
36
36
 
package/src/restart.js CHANGED
@@ -34,7 +34,7 @@ import { setTimeout as pause } from 'node:timers/promises';
34
34
  import { bookFile, readBook } from './book.js';
35
35
  import { botDir, readBot } from './bot.js';
36
36
  import { ownCli, shellWord } from './launch.js';
37
- import { closeTab, findProject, frontOfTab, tabs } from './orca.js';
37
+ import { closeTab, closeTerminal, findProject, frontOfTab, orcaCli, tabs } from './orca.js';
38
38
  import { botsNamed, bringUp, prepareBots, sessionsOf } from './up.js';
39
39
 
40
40
  /**
@@ -68,7 +68,7 @@ export async function restartSessions(bots, { bot: name, session: onlySession }
68
68
  }
69
69
 
70
70
  const going = tabsToClose(bots, name, home, sessions.filter((session) => session.paused !== true), { quit: true });
71
- const closed = await closeTabs(home, going, bots, name);
71
+ const closed = await closeTabs(home, going, bots, name, commandLine('restart', bots, name, onlySession));
72
72
  return { closed, ...(await bringUp(bots, { bot: name, session: onlySession })) };
73
73
  }
74
74
 
@@ -113,12 +113,14 @@ export function tabsToClose(bots, name, home, sessions, { keepless = false, quit
113
113
  /**
114
114
  * Close the tabs `tabsToClose` gave, one at a time by their own handles, and
115
115
  * wait until Orca's listing agrees they are gone. Returns one entry per tab.
116
+ * `again` is the command that asked, as the user would run it again: what to do
117
+ * once a tab Orca would not close has been closed by hand.
116
118
  */
117
- export async function closeTabs(home, going, bots, name) {
119
+ export async function closeTabs(home, going, bots, name, again) {
118
120
  const closed = [];
119
121
  for (const { name: session, tab } of going) {
120
122
  try {
121
- closeTab(tab.handle);
123
+ closeWhole(tab.handle, again);
122
124
  } catch (error) {
123
125
  // A tab that would not close still holds its session, so nothing is
124
126
  // opened after this: two harnesses on one conversation is worse than a
@@ -132,6 +134,41 @@ export async function closeTabs(home, going, bots, name) {
132
134
  return closed;
133
135
  }
134
136
 
137
+ /**
138
+ * Close one session's tab. Orca finds a `--tab` close in a tab snapshot of its
139
+ * own, which a tab it still lists can be missing from after a restart, and then
140
+ * answers `tab_not_found` (#405, read in the 1.4.215 bundle). The same terminal
141
+ * closed without `--tab` closes its one-pane tab by id, so that is done instead,
142
+ * and `gone` checks the tab went as it does after any close. Every other
143
+ * refusal stops the restart as it did.
144
+ */
145
+ function closeWhole(handle, again) {
146
+ try {
147
+ closeTab(handle);
148
+ } catch (error) {
149
+ if (!notFound(error)) throw error;
150
+ try {
151
+ closeTerminal(handle);
152
+ } catch (second) {
153
+ // With the tab closed by hand there is nothing left for the command to
154
+ // close, so running it again finishes it, whichever command it was.
155
+ throw new Error(`${error.message}, and then ${second.message}. Close it by hand, then run the same command again: ${shellWord(orcaCli())} terminal close --terminal ${shellWord(handle)} then ${again}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Whether Orca refused a `--tab` close for a tab it could not find: its code,
162
+ * or its 1.4.215 form of code `runtime_error` with exactly that message. Orca's
163
+ * own words, never the kit's wrapped text, and no other form (#405).
164
+ */
165
+ const notFound = (error) =>
166
+ error.code === 'tab_not_found' || (error.code === 'runtime_error' && error.reason === 'tab_not_found');
167
+
168
+ /** A command of the kit's as the user would type it, for a message to name. */
169
+ export const commandLine = (verb, bots, bot, session) =>
170
+ `${shellWord(ownCli())} ${verb} --bots ${shellWord(bots)} --bot ${bot}${session === undefined ? '' : ` --session ${session}`}`;
171
+
135
172
  /** How long Orca is given to stop listing a tab it has closed, and how often it is asked. */
136
173
  const SETTLED_MS = 5000;
137
174
  const ASK_MS = 100;
package/src/retire.js CHANGED
@@ -26,7 +26,7 @@ import { readBook, tabIdsIn, updateBook } from './book.js';
26
26
  import { botDir, dropSession, readBot } from './bot.js';
27
27
  import { deleteProject, findProject, projects, reloadWindow, tabs } from './orca.js';
28
28
  import { fleetMember } from './pause.js';
29
- import { closeTabs, tabsToClose } from './restart.js';
29
+ import { closeTabs, commandLine, tabsToClose } from './restart.js';
30
30
  import { unlinkSkills } from './skills.js';
31
31
  import { promptPath, sessionsOf } from './up.js';
32
32
 
@@ -41,7 +41,7 @@ export async function retireSession(bots, { bot, session }) {
41
41
  const home = fleetMember(bots, bot, 'retire', session);
42
42
  const sessions = sessionsOf(readBot(home, bot), session);
43
43
 
44
- const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot);
44
+ const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot, commandLine('retire', bots, bot, session));
45
45
  dropSession(bots, bot, session);
46
46
 
47
47
  const at = new Date().toISOString();
@@ -86,7 +86,7 @@ export async function retireBot(bots, { bot }) {
86
86
  // taken away with one of them still open. `closeTabs` waits until Orca agrees
87
87
  // they are gone.
88
88
  const booked = Object.keys(readBook(home).sessions).map((name) => ({ name }));
89
- const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot);
89
+ const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot, commandLine('retire', bots, bot));
90
90
  let windowReloaded;
91
91
  if (project !== undefined) {
92
92
  deleteProject(project.id);
package/src/up.js CHANGED
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import { setTimeout as pause } from 'node:timers/promises';
10
10
 
11
11
  import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeMailboxTurn, updateBook, withUnclaimed } from './book.js';
12
- import { botDir, botNames, displayName, readBot } from './bot.js';
12
+ import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
13
13
  import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
14
14
  import { installHook } from './hooks.js';
15
15
  import { writePermissions } from './permissions.js';
@@ -211,7 +211,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
211
211
  // Orca is asked first and the book is written after: nothing that takes time
212
212
  // happens while the book is held, because a session's own hook may be writing
213
213
  // its id into that same file at any moment.
214
- const { change, ...orca } = orcaProject(home, title);
214
+ const { change, ...orca } = orcaProject(home, projectName(bots, name));
215
215
  await updateBook(home, (book) => { book.orca = orca; });
216
216
  // Orca's window does not see a project made or renamed on the command line
217
217
  // until it is told (#224).