@assuredloop/orca-bot-kit 0.24.1 → 0.25.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
@@ -200,6 +200,7 @@ waiting for:
200
200
  | 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 |
201
201
  | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
202
202
  | 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 |
203
+ | Claude Code 2.1.289's `Teach auto mode about your environment?`, a numbered list: `1. Yes`, `2. Not now`, `3. Don't show again`, and `Enter to confirm · Esc to cancel`, above the input box | `\x1b[B\r` | down, then return, to **2. Not now**: its selection starts on **1. Yes**. Read the screen after the down arrow, and send the return only with the selection on **2. Not now**. Never **1. Yes**, which starts the teach scan, and never **3. Don't show again** | Not yet proven: the list was seen live on 2.1.289 with its selection on 1. Yes, in #489's live run 1, and was not answered there |
203
204
 
204
205
  Each key counts on where the selection starts, as it did on the version named.
205
206
  Read the screen in front of you before you send: if the selection is somewhere
@@ -221,6 +222,27 @@ Codex's `/new` question (seen on 0.156.1, 0.157.1, 0.158.0 and 0.160.0) is alway
221
222
  home. The kit never makes a git worktree, so
222
223
  `2. New worktree` is never the answer.
223
224
 
225
+ A bot's own temporary sessions are answered by their maker, through the kit, so
226
+ that one narrow permission rule allows it rather than a rule to type into any
227
+ tab. On Codex's hooks review, the maker runs
228
+ `obk temp trust-hooks --bots <their path> --name <session>`. On Claude Code's
229
+ `Teach auto mode` screen, it runs `obk temp answer --bots <their path> --name <session>`.
230
+ That command reads the screen and answers only when it holds one of the two
231
+ shapes in the table exactly as the kit knows it: Esc for the 2.1.283 form, and
232
+ **2. Not now** for the 2.1.289 list. On the list it sends the arrows, reads the
233
+ screen again, and sends the return only with the selection on **2. Not now**.
234
+ It refuses any other screen, types nothing then, and prints what it saw.
235
+ Afterwards it checks that the screen has gone. In
236
+ auto mode the maker's bot needs this rule, with the kit's full CLI path for
237
+ `<kit>`, as the command's own output prints it:
238
+
239
+ ```text
240
+ Bash(<kit> temp answer:*)
241
+ ```
242
+
243
+ Ask the user, and only after their yes run
244
+ `obk bot change --bots <their path> --bot <bot> --allow 'Bash(<kit> temp answer:*)'`.
245
+
224
246
  **Anything you do not recognise: type nothing.** Tell them what is on the
225
247
  screen and which tab it is in, and wait. A keypress into a menu you have not read
226
248
  is how a harness quits back to the shell.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -294,6 +294,7 @@ otherwise `/Applications/Orca.app/Contents/Resources/bin/orca`. A bare
294
294
  | Codex's update offer, `1. Update now` | `\r` | **Update now**, where it starts | Codex 0.156.1 |
295
295
  | `[oh-my-zsh] Would you like to update?` | `n` | no: the user updates it | oh-my-zsh bf77e35 |
296
296
  | Claude Code's `Teach auto mode…` | `\x1b` | Esc: Not now; no return | Not yet proven, below |
297
+ | The same, 2.1.289's list | `\x1b[B\r` | down, return: **2. Not now** | Not yet proven, below |
297
298
 
298
299
  Each key counts on where the selection starts on the version named. Read the
299
300
  screen before you send: if the selection is elsewhere or the options have
@@ -309,7 +310,10 @@ Claude Code's `Teach auto mode` on 2.1.283 is a form, not a list, ending
309
310
  Enter is Continue, which scans the project, recent sessions and the machine's
310
311
  shell history. Esc cancels it (Not now) in its code; that is not yet proven
311
312
  live, though the form was seen live twice on 2.1.283. On 2.1.278 it was a
312
- numbered list, answered with `2. Not now`. Codex's hooks
313
+ numbered list, answered with `2. Not now`, and on 2.1.289 it is one again:
314
+ `1. Yes`, `2. Not now`, `3. Don't show again`, above the input box, its
315
+ selection on 1. Yes (seen live, not answered, in #489). Go down to 2. Not now,
316
+ read the screen, and only then return. Codex's hooks
313
317
  question matters most. The kit's hook is how the book learns which
314
318
  conversation the session is running, and until it is answered the
315
319
  conversation has not started. Codex's trust applies to the repository root,
@@ -323,7 +327,13 @@ exception: answer it with `obk temp trust-hooks --bots <bots> --name
323
327
  <session>`, not with the keys. It reads the screen, chooses **Trust all and
324
328
  continue** only if the review is there, and checks that the review went. A
325
329
  permission rule can allow that one command, where it cannot allow keys sent
326
- into any tab.
330
+ into any tab. The same goes for Claude Code's `Teach auto mode` screen on a
331
+ temporary session you made: answer it with `obk temp answer --bots <bots>
332
+ --name <session>`. It answers only a shape in the table exactly as the kit
333
+ knows it (Esc on the 2.1.283 form, 2. Not now on the 2.1.289 list, the return
334
+ only after a second look), refuses any other screen, and checks that it went. Its
335
+ rule is `Bash(<kit> temp answer:*)`, `<kit>` the kit's CLI path as the
336
+ command prints it; ask the user before `obk bot change --allow` adds it.
327
337
 
328
338
  Where the kit says no session came up, the shell swallowed the launch line,
329
339
  usually while it was asking its own question. Answer the shell, then close
package/src/cli.js CHANGED
@@ -31,7 +31,7 @@ import { readRoster } from './roster.js';
31
31
  import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
32
32
  import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
33
33
  import { addSource, fetchSources } from './sources.js';
34
- import { listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
34
+ import { answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
35
35
  import { readUsage } from './usage.js';
36
36
  import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
37
37
 
@@ -140,6 +140,12 @@ Usage:
140
140
  all and continue", and check the review went. It
141
141
  refuses anything else on that screen, and types
142
142
  nothing then.
143
+ obk temp answer --bots <path> --name <session>
144
+ Run in the maker's own tab: answer Claude Code's
145
+ "Teach auto mode" screen on a temporary session it
146
+ made with Not now, in a shape the kit knows, and
147
+ check the screen went. It refuses any other
148
+ screen, and types nothing then.
143
149
  obk rules build --bots <path> [--bot <bot>]
144
150
  Build every bot's AGENTS.md from its charter and
145
151
  the rule units it carries, or just the one you
@@ -329,6 +335,7 @@ const COMMANDS = {
329
335
  'temp roles': ['bots'],
330
336
  'temp retire': ['bots', 'name'],
331
337
  'temp trust-hooks': ['bots', 'name'],
338
+ 'temp answer': ['bots', 'name'],
332
339
  };
333
340
 
334
341
  /** What each flag is for, in the sentence a caller reads when it is missing. */
@@ -792,6 +799,19 @@ const commands = {
792
799
  };
793
800
  },
794
801
 
802
+ async 'temp answer'(bots, values) {
803
+ const tab = callerTab(bots, true);
804
+ refuseWhenOrcaIsDown();
805
+ const answered = await answerTemp(bots, { tab, name: values.name });
806
+ return {
807
+ answer: { bots, ...answered },
808
+ lines: [
809
+ `answered ${answered.bot} ${answered.session}, a temporary session of ${answered.maker}'s: sent ${answered.sent} to its "Teach auto mode about your environment?" screen, and the screen has gone`,
810
+ `rule a maker's bot runs this under one permission rule, which the user approves: Bash(${shellWord(ownCli())} temp answer:*)`,
811
+ ],
812
+ };
813
+ },
814
+
795
815
  'bot create'(bots, values) {
796
816
  const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
797
817
  // The bot's AGENTS.md is the rules build's, here as everywhere else, so
package/src/orca.js CHANGED
@@ -420,8 +420,9 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
420
420
  * Every one seen is a numbered list of choices with the harness's pointer on
421
421
  * one (tech notes, section 1; ADR 0034). The same pointer starts the harness's
422
422
  * input line and its echo of the user's past turns, and the input line is the
423
- * lowest of them whenever it is on screen, so only the lowest pointer row is
424
- * asked about: a question counts while it stands in the input line's place.
423
+ * lowest of them whenever it is on screen, so the lowest pointer row is asked
424
+ * about: a question counts while it stands in the input line's place, or
425
+ * while it is the last thing drawn right above the input box (#491).
425
426
  * Unnumbered lists do not count. Codex puts a status row right under its input
426
427
  * line, lined up with it, and by its layout that is one.
427
428
  */
@@ -434,6 +435,37 @@ export function questionIn(rows) {
434
435
  // there is none: under old words of one in the history, the input line is
435
436
  // the lowest pointer, and nothing below it is a foot.
436
437
  if (rows.slice(Math.max(at, 0)).some((row) => FORM_FOOT.test(row))) return true;
438
+ if (pointedChoiceAt(rows, at)) return true;
439
+ // Claude Code 2.1.289 draws its Teach list above its input box, whose own
440
+ // `❯` stays the lowest pointer row (#491). A question there is the last
441
+ // thing drawn above the box's top rule: its foot, or its numbered choices
442
+ // with the pointer on one, under it a foot that a narrow pane wraps onto
443
+ // rows of its own. The same words with anything after them are history.
444
+ if (at < 1 || !RULE_ROW.test(rows[at - 1])) return false;
445
+ let end = rows.slice(0, at - 1).findLastIndex((row) => row.trim() !== '');
446
+ if (end < 0) return false;
447
+ if (FORM_FOOT.test(rows[end])) return true;
448
+ while (end > 0 && FOOT_PART.test(rows[end])) end -= 1;
449
+ if (!NUMBERED.test(rows[end])) return false;
450
+ let first = end;
451
+ while (first > 0 && NUMBERED.test(rows[first - 1])) first -= 1;
452
+ for (let row = first; row <= end; row += 1) {
453
+ if (pointedChoiceAt(rows, row)) return true;
454
+ }
455
+ return false;
456
+ }
457
+
458
+ /** A rule Claude Code draws right across, as the top of its input box: `─` or `▔`. */
459
+ const RULE_ROW = /^ *[─▔]{8,}/;
460
+
461
+ /** A numbered choice, with the pointer on it or not. */
462
+ const NUMBERED = /^ *(?:[›❯] +)?\d+\. /;
463
+
464
+ /** A part of a foot, as a narrow pane wraps it: `Enter to confirm ·`, `Esc to cancel`. */
465
+ const FOOT_PART = /\b(?:Enter to (?:continue|confirm|select)|Esc to cancel)\b/i;
466
+
467
+ /** Whether `rows[at]` has the pointer on a numbered choice, with another lined up right above or below it. */
468
+ function pointedChoiceAt(rows, at) {
437
469
  const pointer = at < 0 ? null : ON_A_CHOICE.exec(rows[at]);
438
470
  if (pointer === null) return false;
439
471
  const column = pointer[1].length;
package/src/temp.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // `obk temp make` and `obk temp retire`: a session's own temporary sessions
2
- // (PRD 6.4, #227). And `obk temp trust-hooks`, the one answer a maker gives its
3
- // Codex run's hooks review (#238).
2
+ // (PRD 6.4, #227). And `obk temp trust-hooks` and `obk temp answer`, the
3
+ // answers a maker gives its Codex run's hooks review (#238) and its Claude
4
+ // session's Teach auto mode form (#489).
4
5
  //
5
6
  // Any long-lived session can make a temporary session of its own bot for a
6
7
  // piece of work, and retire it when the work is done, without Bot Father, who
@@ -348,59 +349,232 @@ export async function trustHooks(bots, { tab, name }) {
348
349
  if (harness !== 'codex') {
349
350
  throw new Error(`${run} runs on ${harness}, and temp trust-hooks answers a Codex run's "${HOOKS_REVIEW}" alone. ${nothing}`);
350
351
  }
352
+ await answerScreen(caller, name, {
353
+ what: 'hooks review',
354
+ mark: HOOKS_REVIEW,
355
+ done: `"${TRUST_ALL}" was chosen on its hooks review`,
356
+ answer(rows, { send }) {
357
+ const keys = keysToTrustAll(rows);
358
+ if (keys === undefined) {
359
+ throw new Error(`${run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(rows)}. ${nothing}`);
360
+ }
361
+ send(keys);
362
+ },
363
+ });
364
+ return { bot: caller.bot, session: name, maker: caller.session };
365
+ }
366
+
367
+ /**
368
+ * The keys that take Codex's hooks review to "2. Trust all and continue" and
369
+ * answer it, from wherever its pointer is: arrows, then return, never a digit
370
+ * (a digit and return once took whatever was highlighted). Undefined when the
371
+ * rows show no such review with the pointer on one of its choices.
372
+ */
373
+ function keysToTrustAll(rows) {
374
+ if (!rows.some((row) => row.includes(HOOKS_REVIEW))) return undefined;
375
+ if (!rows.some((row) => new RegExp(`^ *(?:› +)?2\\. ${TRUST_ALL}`).test(row))) return undefined;
376
+ const pointer = rows.map((row) => /^ *› +(\d+)\. /.exec(row)).findLast((found) => found !== null);
377
+ if (pointer === undefined) return undefined;
378
+ const moves = 2 - Number(pointer[1]);
379
+ return (moves > 0 ? '\x1b[B'.repeat(moves) : '\x1b[A'.repeat(-moves)) + '\r';
380
+ }
381
+
382
+ /** What a screen shows, on one line: its rows with words in them, cut short. */
383
+ function shown(rows) {
384
+ const text = rows.map((row) => row.trim()).filter((row) => row !== '').join(' ⏎ ');
385
+ return text.length > 500 ? `${text.slice(0, 500)}…` : text;
386
+ }
387
+
388
+ /**
389
+ * Answer a screen of `name`, a temporary session the caller made: under the
390
+ * session's turn for a line (#482), read its screen and hand it to
391
+ * `answer(rows, { send, look })`, which sends keys with `send`, may read the
392
+ * screen again with `look`, and throws when the screen is not its to answer.
393
+ * Then wait for `mark` to leave the screen. `what` names the screen and `done`
394
+ * says what was sent, for the sentences that refuse or fail; `done` may be a
395
+ * function of what `answer` returned.
396
+ */
397
+ async function answerScreen(caller, name, { what, mark, done, answer }) {
398
+ const run = `${caller.bot}/${name}`;
399
+ const nothing = 'Nothing was typed.';
351
400
  const tabId = readBook(caller.home).sessions[name]?.tab;
352
401
  const handle = tabId === undefined ? undefined : tabs(caller.home).find((one) => one.tabId === tabId)?.handle;
353
402
  if (handle === undefined) {
354
- throw new Error(`${run} has no tab open in Orca, so there is no review of its to answer. ${nothing}`);
403
+ throw new Error(`${run} has no tab open in Orca, so there is no ${what} of its to answer. ${nothing}`);
355
404
  }
356
- // The session's turn for a line (#482), for the look and the keys.
357
405
  const turn = takeLineTurn(caller.home, name, TYPING_WAIT_MS);
358
406
  if (turn === undefined) throw new Error(`${run}: ${TYPING_HELD}.`);
407
+ let answered;
359
408
  try {
360
409
  const seen = screenRows(handle);
361
410
  if (seen.rows === undefined) {
362
- throw new Error(`${run}'s screen could not be read (${seen.unreadable}), so the kit cannot tell whether its hooks review is there. ${nothing}`);
411
+ throw new Error(`${run}'s screen could not be read (${seen.unreadable}), so the kit cannot tell whether its ${what} is there. ${nothing}`);
363
412
  }
364
- const keys = keysToTrustAll(seen.rows);
365
- if (keys === undefined) {
366
- throw new Error(`${run}'s screen shows no "${HOOKS_REVIEW}" with its choices, so there is nothing for this to answer. It shows: ${shown(seen.rows)}. ${nothing}`);
367
- }
368
- // The return is inside the text: `--enter` would be a second key.
369
- orca(['terminal', 'send', '--terminal', handle, '--text', keys]);
413
+ answered = answer(seen.rows, {
414
+ // The return, where there is one, is inside the text: `--enter` would be a second key.
415
+ send: (keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys]),
416
+ look: () => screenRows(handle),
417
+ });
370
418
  } finally {
371
419
  turn.release();
372
420
  }
373
421
  for (let waited = 0; ; waited += 500) {
374
422
  const after = screenRows(handle);
375
- if (after.rows !== undefined && !after.rows.some((row) => row.includes(HOOKS_REVIEW))) break;
423
+ if (after.rows !== undefined && !after.rows.some((row) => row.includes(mark))) return answered;
376
424
  if (waited >= REVIEW_GONE_MS) {
377
- const still = after.rows === undefined ? `its screen could not be read again (${after.unreadable})` : `its screen still shows "${HOOKS_REVIEW}"`;
378
- throw new Error(`${run}: "${TRUST_ALL}" was chosen on its hooks review, but ${REVIEW_GONE_MS / 1000} seconds later ${still}. Look at its tab.`);
425
+ const still = after.rows === undefined ? `its screen could not be read again (${after.unreadable})` : `its screen still shows "${mark}"`;
426
+ throw new Error(`${run}: ${typeof done === 'function' ? done(answered) : done}, but ${REVIEW_GONE_MS / 1000} seconds later ${still}. Look at its tab.`);
379
427
  }
380
428
  await pause(500);
381
429
  }
382
- return { bot: caller.bot, session: name, maker: caller.session };
383
430
  }
384
431
 
432
+ /** The title of Claude Code's "Teach auto mode" screen, in each known shape. */
433
+ const TEACH_TITLE = 'Teach auto mode about your environment?';
434
+
435
+ /** The foot that ends a Claude Code form, below which its input box sits. */
436
+ const TEACH_FOOT = /Enter to .*· Esc to cancel/;
437
+
385
438
  /**
386
- * The keys that take Codex's hooks review to "2. Trust all and continue" and
387
- * answer it, from wherever its pointer is: arrows, then return, never a digit
388
- * (a digit and return once took whatever was highlighted). Undefined when the
389
- * rows show no such review with the pointer on one of its choices.
439
+ * The known shapes of the Teach screen, from its title to its foot, each as
440
+ * captured live, with what answers it. Rows are compared trimmed, the pointer
441
+ * `❯` taken out, so the pointer may sit on any one of them.
442
+ *
443
+ * Claude Code 2.1.283's form (#261's live test, #416) is answered with Esc,
444
+ * which cancels it (Not now), never a return, which is Continue. Claude Code
445
+ * 2.1.289's numbered list (#489's live run 1) is answered with "2. Not now",
446
+ * never "1. Yes" or "3. Don't show again" (the architect's ruling on #489).
390
447
  */
391
- function keysToTrustAll(rows) {
392
- if (!rows.some((row) => row.includes(HOOKS_REVIEW))) return undefined;
393
- if (!rows.some((row) => new RegExp(`^ *(?:› +)?2\\. ${TRUST_ALL}`).test(row))) return undefined;
394
- const pointer = rows.map((row) => /^ *› +(\d+)\. /.exec(row)).findLast((found) => found !== null);
395
- if (pointer === undefined) return undefined;
396
- const moves = 2 - Number(pointer[1]);
397
- return (moves > 0 ? '\x1b[B'.repeat(moves) : '\x1b[A'.repeat(-moves)) + '\r';
448
+ const TEACH_SHAPES = [
449
+ {
450
+ name: 'the 2.1.283 form',
451
+ rows: [
452
+ TEACH_TITLE,
453
+ 'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
454
+ 'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
455
+ 'How you use Claude here Mixed',
456
+ 'Also scan shell history true',
457
+ 'Also scan your other repos false',
458
+ 'Continue',
459
+ '←/→ to change usage · Enter to continue · Esc to cancel',
460
+ ],
461
+ },
462
+ {
463
+ name: 'the 2.1.289 list',
464
+ rows: [
465
+ TEACH_TITLE,
466
+ 'Auto mode works better when it knows your environment. Takes about a minute.',
467
+ '1. Yes',
468
+ '2. Not now',
469
+ "3. Don't show again",
470
+ 'Enter to confirm · Esc to cancel',
471
+ ],
472
+ },
473
+ ];
474
+
475
+ /** The 2.1.289 list's choice this kit takes, and the row it is on. */
476
+ const NOT_NOW_CHOICE = '2. Not now';
477
+
478
+ /** A row as a shape is compared: trimmed, with its pointer, wherever it sits, taken out. */
479
+ const unpointed = (row) => row.replace('❯', ' ').trim();
480
+
481
+ /** The full-width rule Claude Code draws right above the screen's title: `─` in 2.1.289, `▔` in 2.1.283. */
482
+ const ABOVE_TITLE = /^(?:─+|▔+)$/;
483
+
484
+ /**
485
+ * The Teach screen on `rows`, from its title to its foot, the rows below the
486
+ * foot (the input box) left out: `{ shape, block }` when its non-blank rows
487
+ * are one known shape's, in its order and nothing else, with the pointer on
488
+ * exactly one; otherwise `{ misfit }`, why not.
489
+ *
490
+ * It counts only in the frame Claude Code drew it in, in both live captures:
491
+ * a full-width rule as the non-blank row right above the title, and, right
492
+ * below the foot, nothing or the input box's top rule. A quote of the screen
493
+ * in a turn, with the turn's own rows around it, is refused (the review of
494
+ * #490). A quote of the frame as well would still look the same: the frame
495
+ * narrows what passes, and is no proof that the screen is live.
496
+ */
497
+ function teachScreen(rows) {
498
+ const from = rows.findIndex((row) => row.trim() === TEACH_TITLE);
499
+ if (from < 0) return { misfit: `it shows no "${TEACH_TITLE}"` };
500
+ const foot = rows.findIndex((row, at) => at > from && TEACH_FOOT.test(row));
501
+ if (foot < 0) return { misfit: `it shows "${TEACH_TITLE}" with no foot row ending "Esc to cancel" under it` };
502
+ const above = rows.slice(0, from).findLast((row) => row.trim() !== '');
503
+ if (above === undefined || !ABOVE_TITLE.test(above.trim())) {
504
+ return { misfit: `the row right above its title is ${above === undefined ? 'nothing' : `"${above.trim()}"`}, not the full-width rule Claude Code draws there` };
505
+ }
506
+ const below = rows.slice(foot + 1).find((row) => row.trim() !== '');
507
+ if (below !== undefined && !below.trimStart().startsWith('─')) {
508
+ return { misfit: `the row right below its foot is "${below.trim()}", not the top rule of the input box` };
509
+ }
510
+ const block = rows.slice(from, foot + 1).filter((row) => row.trim() !== '');
511
+ const misfits = TEACH_SHAPES.map((shape) => {
512
+ const pointers = block.filter((row) => row.includes('❯')).length;
513
+ if (pointers !== 1) return `${pointers} rows with the pointer ❯, where ${shape.name} has one`;
514
+ const odd = block.find((row) => !shape.rows.includes(unpointed(row)));
515
+ if (odd !== undefined) return `a row ${shape.name} does not have: ${odd.trim()}`;
516
+ const missing = shape.rows.find((row) => !block.map(unpointed).includes(row));
517
+ if (missing !== undefined) return `no row ${missing}, which ${shape.name} has`;
518
+ if (block.length !== shape.rows.length) return `${block.length} rows, where ${shape.name} has ${shape.rows.length}`;
519
+ const moved = block.findIndex((row, at) => unpointed(row) !== shape.rows[at]);
520
+ if (moved >= 0) return `the row ${block[moved].trim()} where ${shape.name} has ${shape.rows[moved]}`;
521
+ return undefined;
522
+ });
523
+ const fits = misfits.findIndex((misfit) => misfit === undefined);
524
+ if (fits >= 0) return { shape: TEACH_SHAPES[fits], block };
525
+ return { misfit: `its Teach screen is no shape the kit knows: it has ${misfits.join('; and it has ')}` };
398
526
  }
399
527
 
400
- /** What a screen shows, on one line: its rows with words in them, cut short. */
401
- function shown(rows) {
402
- const text = rows.map((row) => row.trim()).filter((row) => row !== '').join(' ⏎ ');
403
- return text.length > 500 ? `${text.slice(0, 500)}…` : text;
528
+ /** The whole screen, a row to a line: what decides is often at its foot. */
529
+ const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row) => ` ${row}`).join('\n');
530
+
531
+ /**
532
+ * Answer the first-run screen of a temporary session the caller made (#489):
533
+ * Claude Code's Teach auto mode screen, in one of the shapes the kit knows,
534
+ * matched exactly. The 2.1.283 form gets Esc. The 2.1.289 list gets arrows to
535
+ * "2. Not now" where needed, a second look that the pointer is there and the
536
+ * list is otherwise as it was, and only then a return. Anything else is refused with
537
+ * nothing typed. Like `trustHooks`, it is one command a permission rule of its
538
+ * own can allow. Returns `{ bot, session, maker, sent }`, `sent` saying what
539
+ * answered it.
540
+ */
541
+ export async function answerTemp(bots, { tab, name }) {
542
+ const caller = callerIn(bots, tab, 'answer', "answer a session's first-run screen");
543
+ const nothing = 'Nothing was typed.';
544
+ ownTemp(caller, name, 'answer for', nothing);
545
+ const run = `${caller.bot}/${name}`;
546
+ const sent = await answerScreen(caller, name, {
547
+ what: `"${TEACH_TITLE}"`,
548
+ mark: TEACH_TITLE,
549
+ done: (said) => `${said} was sent to its Teach auto mode screen`,
550
+ answer(rows, { send, look }) {
551
+ const seen = teachScreen(rows);
552
+ if (seen.misfit !== undefined) {
553
+ const hooks = rows.some((row) => row.includes(HOOKS_REVIEW)) ? ` A Codex run's "${HOOKS_REVIEW}" is answered with temp trust-hooks.` : '';
554
+ throw new Error(`${run}'s screen is not one temp answer answers: ${seen.misfit}.${hooks} ${nothing} It shows:\n${wholeScreen(rows)}`);
555
+ }
556
+ if (seen.shape === TEACH_SHAPES[0]) {
557
+ send('\x1b');
558
+ return 'Esc (Not now)';
559
+ }
560
+ const at = seen.block.findIndex((row) => row.includes('❯'));
561
+ const moves = seen.block.findIndex((row) => unpointed(row) === NOT_NOW_CHOICE) - at;
562
+ if (moves !== 0) send(moves > 0 ? '\x1b[B'.repeat(moves) : '\x1b[A'.repeat(-moves));
563
+ // The guard, before every return, arrows or none: the list again, its
564
+ // rows in the same order (an exact shape), and the pointer on "2. Not now".
565
+ const again = look();
566
+ const now = again.rows === undefined ? { misfit: `it could not be read again (${again.unreadable})` } : teachScreen(again.rows);
567
+ const on = now.block?.find((row) => row.includes('❯'));
568
+ if (now.shape !== seen.shape || unpointed(on) !== NOT_NOW_CHOICE) {
569
+ const why = now.misfit ?? `its pointer is on ${unpointed(on)}, not ${NOT_NOW_CHOICE}`;
570
+ const sent = moves === 0 ? 'Nothing was typed' : `The arrows to "${NOT_NOW_CHOICE}" went in`;
571
+ throw new Error(`${run}: ${sent}, but then ${why}, so no return was sent. Look at its tab.${again.rows === undefined ? '' : ` It shows:\n${wholeScreen(again.rows)}`}`);
572
+ }
573
+ send('\r');
574
+ return `"${NOT_NOW_CHOICE}"`;
575
+ },
576
+ });
577
+ return { bot: caller.bot, session: name, maker: caller.session, sent };
404
578
  }
405
579
 
406
580
  /**