@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 +22 -0
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +12 -2
- package/src/cli.js +21 -1
- package/src/orca.js +34 -2
- package/src/temp.js +204 -30
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
|
@@ -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
|
|
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
|
|
424
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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(
|
|
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 "${
|
|
378
|
-
throw new Error(`${run}:
|
|
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
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
/**
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
/**
|