@assuredloop/orca-bot-kit 0.23.0 → 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/book.js +94 -3
- package/src/clear.js +141 -42
- package/src/cli.js +45 -1
- package/src/groom.js +17 -10
- package/src/hooks.js +67 -31
- package/src/message.js +15 -1
- package/src/name.js +138 -0
- package/src/orca.js +34 -2
- package/src/skills.js +10 -1
- package/src/temp.js +208 -27
- package/src/up.js +49 -27
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/book.js
CHANGED
|
@@ -152,8 +152,13 @@ function takeLock(home) {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
155
|
+
/**
|
|
156
|
+
* The lock on `file`, waited for `waitMs` at most: see `takeLock` for why it is
|
|
157
|
+
* this one. `how` is SQLite's: `IMMEDIATE`, one holder at a time; `SHARED`,
|
|
158
|
+
* any number at once, none while an `EXCLUSIVE` is held or waited for; and
|
|
159
|
+
* `EXCLUSIVE`, alone, once every `SHARED` holder has let go.
|
|
160
|
+
*/
|
|
161
|
+
function lockOn(file, waitMs, how = 'IMMEDIATE') {
|
|
157
162
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
158
163
|
|
|
159
164
|
const db = new DatabaseSync(file);
|
|
@@ -161,7 +166,13 @@ function lockOn(file, waitMs) {
|
|
|
161
166
|
// A writer that arrives while another is working waits for it rather than
|
|
162
167
|
// failing at once, and gives up saying so rather than waiting for ever.
|
|
163
168
|
db.exec(`PRAGMA busy_timeout = ${waitMs}`);
|
|
164
|
-
|
|
169
|
+
if (how === 'SHARED') {
|
|
170
|
+
// A reading transaction holds its shared lock from its first read.
|
|
171
|
+
db.exec('BEGIN');
|
|
172
|
+
db.prepare('SELECT count(*) FROM sqlite_master').get();
|
|
173
|
+
} else {
|
|
174
|
+
db.exec(`BEGIN ${how}`);
|
|
175
|
+
}
|
|
165
176
|
} catch (error) {
|
|
166
177
|
db.close();
|
|
167
178
|
throw error;
|
|
@@ -233,6 +244,86 @@ export function takeMailboxTurn(home, session) {
|
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
/**
|
|
248
|
+
* A session's turn to have its Codex thread named (#480), beside its mailbox
|
|
249
|
+
* turn: `{ release }`, or undefined at once when another naming holds it, so
|
|
250
|
+
* that two turn ends close together type the name once.
|
|
251
|
+
*/
|
|
252
|
+
export const takeNameTurn = (home, session) => turnOn(home, session, 'name', 0);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A session's typing turn (#480, the architect's ruling), for what the kit
|
|
256
|
+
* types into the session's tab one key at a time: no line of the kit's may
|
|
257
|
+
* land in between and send what is there with its own return. Two locks
|
|
258
|
+
* beside its mailbox turn: `typing`, the gate, held for the whole typing, and
|
|
259
|
+
* `lines`, taken alone once the lines already on their way have gone in.
|
|
260
|
+
* Returns `{ release }`, or undefined when it did not come within `waitMs`,
|
|
261
|
+
* or the lines in flight did not finish within LINES_WAIT_MS.
|
|
262
|
+
*/
|
|
263
|
+
export function takeTypingTurn(home, session, waitMs) {
|
|
264
|
+
const gate = turnOn(home, session, 'typing', waitMs);
|
|
265
|
+
if (gate === undefined) return undefined;
|
|
266
|
+
let lines;
|
|
267
|
+
try {
|
|
268
|
+
lines = turnOn(home, session, 'lines', LINES_WAIT_MS, 'EXCLUSIVE');
|
|
269
|
+
} finally {
|
|
270
|
+
if (lines === undefined) gate.release();
|
|
271
|
+
}
|
|
272
|
+
if (lines === undefined) return undefined;
|
|
273
|
+
return {
|
|
274
|
+
release() {
|
|
275
|
+
try {
|
|
276
|
+
lines.release();
|
|
277
|
+
} finally {
|
|
278
|
+
gate.release();
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A session's turn for one whole line, sent with its return in one go, as the
|
|
286
|
+
* mail nudge is: through the typing turn's gate, waited for `waitMs` at most,
|
|
287
|
+
* and then held beside any other line, so whole lines never wait on each
|
|
288
|
+
* other, only on typing one key at a time. Returns `{ release }`, or undefined
|
|
289
|
+
* when the gate did not come within `waitMs`. Every whole line the kit types
|
|
290
|
+
* takes it: the nudge, the list line, the skills reload and the grooming line (#482).
|
|
291
|
+
*/
|
|
292
|
+
export function takeLineTurn(home, session, waitMs) {
|
|
293
|
+
const gate = turnOn(home, session, 'typing', waitMs);
|
|
294
|
+
if (gate === undefined) return undefined;
|
|
295
|
+
try {
|
|
296
|
+
return turnOn(home, session, 'lines', 0, 'SHARED');
|
|
297
|
+
} finally {
|
|
298
|
+
gate.release();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* How long a kit path waits for a session's typing turn before it types
|
|
304
|
+
* nothing (#480, #482): a naming holds it for seconds.
|
|
305
|
+
*/
|
|
306
|
+
export const TYPING_WAIT_MS = 5000;
|
|
307
|
+
|
|
308
|
+
/** What a kit path reports when the session's typing turn did not come within TYPING_WAIT_MS. */
|
|
309
|
+
export const TYPING_HELD = `the kit is typing into it, and it was still at it after ${TYPING_WAIT_MS / 1000} s, so nothing was typed`;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* How long a typing turn waits for the lines already on their way: a nudge
|
|
313
|
+
* holds its line's turn while Orca waits up to 5 s to see it start a turn.
|
|
314
|
+
*/
|
|
315
|
+
const LINES_WAIT_MS = 10_000;
|
|
316
|
+
|
|
317
|
+
/** A turn of the kit's own for one session, on a file beside its mailbox turn's. */
|
|
318
|
+
function turnOn(home, session, kind, waitMs, how) {
|
|
319
|
+
try {
|
|
320
|
+
return lockOn(mailboxLockFile(home, session).replace(/\.mailbox\.lock$/, `.${kind}.lock`), waitMs, how);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error.errcode === SQLITE_BUSY) return undefined;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
236
327
|
/**
|
|
237
328
|
* How long a step, a check or an `up` waits for a session's turn. Longer than a
|
|
238
329
|
* step can hold it — three Orca calls of twenty seconds each at most — and short
|
package/src/clear.js
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
import { realpathSync, statSync } from 'node:fs';
|
|
15
15
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
16
16
|
|
|
17
|
-
import { readBook } from './book.js';
|
|
17
|
+
import { readBook, takeTypingTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
18
18
|
import { botDir, readBot } from './bot.js';
|
|
19
19
|
import { claudeTranscript, codexRollout } from './conversations.js';
|
|
20
20
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
21
21
|
import { eachLine, firstLine } from './lines.js';
|
|
22
|
-
import { findProject, orca, QUESTION_ON_SCREEN, questionIn, screenRows, tabs, tabToTypeInto } from './orca.js';
|
|
22
|
+
import { findProject, orca, QUESTION_ON_SCREEN, questionIn, screenRows, tabs, tabToTypeInto, TIMED_OUT } from './orca.js';
|
|
23
23
|
import { botsNamed, sessionsOf, typeListLine } from './up.js';
|
|
24
24
|
|
|
25
25
|
/** The harness's own command for each, as typed into its input line. Codex's clear is `/new` (tech notes, section 3). */
|
|
@@ -58,6 +58,13 @@ const CODEX_BARE_LINE = '0.160.0';
|
|
|
58
58
|
/** How long the session is given to be idle before nothing is typed, and how long each look waits on it. */
|
|
59
59
|
const IDLE_WAIT_MS = 30_000;
|
|
60
60
|
const LOOK_MS = 2000;
|
|
61
|
+
/**
|
|
62
|
+
* How long each look between two characters waits on Orca's tui-idle, whose
|
|
63
|
+
* answer it does not use: an open slash menu keeps it from answering ok (live
|
|
64
|
+
* run 3 of #391). Short, so a naming holds the session's typing turn for
|
|
65
|
+
* seconds (#480).
|
|
66
|
+
*/
|
|
67
|
+
const CHAR_LOOK_MS = 250;
|
|
61
68
|
/** How long the screen is given to show the command typed, or Codex's question after `/new`. */
|
|
62
69
|
const SCREEN_MS = 3000;
|
|
63
70
|
/** How long the book is given to hold the new conversation, and a harness to record a compaction (the architect: up to 5 minutes). */
|
|
@@ -95,14 +102,20 @@ const AT_WORK = [/esc to interrupt/i, /^\s*\S\s+\S+…\s+\(\d/];
|
|
|
95
102
|
export async function clearSession(bots, { bot, session }) {
|
|
96
103
|
const it = sessionToType(bots, bot, session);
|
|
97
104
|
const was = readBook(it.home).sessions[session]?.session ?? null;
|
|
98
|
-
const { command, handle } = await enter(it, 'clear');
|
|
105
|
+
const { command, handle, typing } = await enter(it, 'clear');
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
107
|
+
// Codex's question and the line after it are part of the clear, typed in
|
|
108
|
+
// the typing turn it took for the command.
|
|
109
|
+
try {
|
|
110
|
+
if (it.harness === 'codex') {
|
|
111
|
+
await answerWhereToRun(it, handle, command);
|
|
112
|
+
const line = await typeListLine(it.home, it.session, it.tabId, { line: RECORD_LINE, held: true });
|
|
113
|
+
if (!line.typed) {
|
|
114
|
+
throw new Error(`${it.name}: ${command} went in, but the line that starts the new conversation's first turn was not typed (${line.why}), and Codex reports a new conversation only at its first turn. The book still holds ${was ?? 'no conversation'}. Look at its tab.`);
|
|
115
|
+
}
|
|
105
116
|
}
|
|
117
|
+
} finally {
|
|
118
|
+
typing.release();
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
const until = Date.now() + CLEAR_MS;
|
|
@@ -128,9 +141,10 @@ export async function compactSession(bots, { bot, session }) {
|
|
|
128
141
|
// that ends while the kit waits for the session to be idle is the one
|
|
129
142
|
// before it (review of PR #466). So only what is written after counts.
|
|
130
143
|
let record;
|
|
131
|
-
const { typedAt } = await enter(it, 'compact', () => {
|
|
144
|
+
const { typedAt, typing } = await enter(it, 'compact', () => {
|
|
132
145
|
record = recordAt(it.harness, it.home, readBook(it.home).sessions[session]?.session ?? null);
|
|
133
146
|
});
|
|
147
|
+
typing.release();
|
|
134
148
|
|
|
135
149
|
const until = typedAt + COMPACT_MS;
|
|
136
150
|
for (;;) {
|
|
@@ -150,7 +164,7 @@ export async function compactSession(bots, { bot, session }) {
|
|
|
150
164
|
* The session as something to type into, once everything that refuses before
|
|
151
165
|
* Orca is asked to type has been asked: `{ name, home, session, harness, tabId }`.
|
|
152
166
|
*/
|
|
153
|
-
function sessionToType(bots, bot, session) {
|
|
167
|
+
export function sessionToType(bots, bot, session) {
|
|
154
168
|
botsNamed(bots, bot);
|
|
155
169
|
const home = realpathSync(botDir(bots, bot));
|
|
156
170
|
const known = readBot(home, bot);
|
|
@@ -174,22 +188,58 @@ function sessionToType(bots, bot, session) {
|
|
|
174
188
|
* Type the harness's command for `verb` into the session's tab and press
|
|
175
189
|
* return, once it is idle and the screen shows the command typed and nothing
|
|
176
190
|
* else. `before` runs just before the command is typed. Returns the command,
|
|
177
|
-
* the handle it went into,
|
|
191
|
+
* the handle it went into, when it was typed, and the session's typing turn
|
|
192
|
+
* (#482), taken once it is idle, as the naming takes it, and held for the
|
|
193
|
+
* caller to release.
|
|
178
194
|
*/
|
|
179
195
|
async function enter(it, verb, before = () => {}) {
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
let typing;
|
|
197
|
+
try {
|
|
198
|
+
const typed = await typeCommand(it, COMMANDS[verb][it.harness], {
|
|
199
|
+
before: () => {
|
|
200
|
+
typing = takeTypingTurn(it.home, it.session, TYPING_WAIT_MS);
|
|
201
|
+
if (typing === undefined) throw new Error(`${it.name}: ${TYPING_HELD}. Run this again in a moment.`);
|
|
202
|
+
before();
|
|
203
|
+
},
|
|
204
|
+
cannot: (wrong) => (verb === 'compact' && wrong.menu ? ` ${harnessName(it.harness)} here cannot compact: its menu does not offer ${COMMANDS[verb][it.harness]}.` : ''),
|
|
205
|
+
});
|
|
206
|
+
return { ...typed, typing };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
typing?.release();
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Type `command` into the session's tab, `it` as `sessionToType` gives it, and
|
|
215
|
+
* press return, once it is idle and `check` finds nothing wrong with the
|
|
216
|
+
* screen: by default the slash menu's check, which a command with words after
|
|
217
|
+
* it does not pass, since Codex closes its menu at the space (#480). `before`
|
|
218
|
+
* runs just before the command is typed; `cannot` adds to a refusal what a
|
|
219
|
+
* wrong screen means; past `deadline`, a time, what was typed is taken back
|
|
220
|
+
* and nothing is entered. Returns the command, the handle it went into, and
|
|
221
|
+
* when it was typed.
|
|
222
|
+
*/
|
|
223
|
+
export async function typeCommand(it, command, { before = () => {}, check = menuWrong, cannot = () => '', deadline = Infinity } = {}) {
|
|
182
224
|
// On Codex, what is typed cannot be read back once its menu is open, so
|
|
183
225
|
// nothing may be in its input line before: no draft for the command to join.
|
|
184
|
-
|
|
226
|
+
const emptyLine = (rows) => {
|
|
227
|
+
if (it.harness !== 'codex') return;
|
|
185
228
|
const line = rows.findLast((row) => /^ *›/.test(row))?.trim();
|
|
186
229
|
if (!CODEX_EMPTY.includes(line)) {
|
|
187
230
|
throw new Error(`${it.name}: nothing was typed, because its input line is not empty: it reads "${line ?? 'nothing'}".${shownEnd(rows)}`);
|
|
188
231
|
}
|
|
189
|
-
}
|
|
232
|
+
};
|
|
233
|
+
emptyLine((await idleTab(it)).rows);
|
|
190
234
|
const version = it.harness === 'codex' ? codexVersion(readBook(it.home).sessions[it.session]?.session) : undefined;
|
|
191
235
|
|
|
192
236
|
before();
|
|
237
|
+
// What `before` waited for can have changed the session: one more look,
|
|
238
|
+
// with no wait, before the first key (#480 review).
|
|
239
|
+
const ready = lookAt(it);
|
|
240
|
+
if (ready.why !== undefined) throw new Error(`${it.name}: nothing was typed, because ${ready.why}.`);
|
|
241
|
+
emptyLine(ready.rows);
|
|
242
|
+
const { handle } = ready;
|
|
193
243
|
const typedAt = Date.now();
|
|
194
244
|
// One character a send, each after a look through the gate, so nothing goes
|
|
195
245
|
// in as one burst and nothing goes in once something asks a question
|
|
@@ -197,31 +247,63 @@ async function enter(it, verb, before = () => {}) {
|
|
|
197
247
|
// Between characters, and once more before the return, the look is the gate
|
|
198
248
|
// and the harness's own at-work row, not Orca's tui-idle: an open slash menu
|
|
199
249
|
// can stop it answering ok (live run 3).
|
|
250
|
+
// The characters Orca took, and so what is in the input line of ours.
|
|
200
251
|
let typed = 0;
|
|
252
|
+
// Taken back, one backspace a character, in one send, so the input line is
|
|
253
|
+
// as it was. Whatever stops the typing takes them back, a send Orca refused
|
|
254
|
+
// or a look that threw included (#480 review); a take-back Orca refuses
|
|
255
|
+
// too leaves nothing more the kit can do.
|
|
256
|
+
const takeBack = () => {
|
|
257
|
+
const keys = '\x7f'.repeat(typed);
|
|
258
|
+
typed = 0;
|
|
259
|
+
if (keys === '') return;
|
|
260
|
+
try {
|
|
261
|
+
send(handle, keys);
|
|
262
|
+
} catch {
|
|
263
|
+
// Said by the error that stopped the typing.
|
|
264
|
+
}
|
|
265
|
+
};
|
|
201
266
|
const lookAgain = async () => {
|
|
202
267
|
await pause(CHAR_GAP_MS);
|
|
203
268
|
const look = lookAt(it, { idle: false });
|
|
269
|
+
// Past the caller's deadline nothing more goes in: a hook stopped partway
|
|
270
|
+
// would leave the text in the input line (#480 review).
|
|
271
|
+
if (look.why === undefined && Date.now() > deadline) look.why = 'its time ran out';
|
|
204
272
|
if (look.why === undefined) return;
|
|
205
|
-
|
|
206
|
-
|
|
273
|
+
const sofar = command.slice(0, typed);
|
|
274
|
+
takeBack();
|
|
275
|
+
throw new Error(`${it.name}: ${command} was being typed, and after ${sofar} ${look.why}, so what was typed was taken back and nothing was entered.`);
|
|
207
276
|
};
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
277
|
+
try {
|
|
278
|
+
for (const char of command) {
|
|
279
|
+
if (typed > 0) await lookAgain();
|
|
280
|
+
try {
|
|
281
|
+
send(handle, char);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// A send Orca did not answer in time may have gone in all the same.
|
|
284
|
+
// Counted only where the line was empty before the first key, so the
|
|
285
|
+
// one backspace too many can take nothing of the user's (#480 review).
|
|
286
|
+
if (error.code === TIMED_OUT && it.harness === 'codex') typed += 1;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
typed += 1;
|
|
290
|
+
}
|
|
291
|
+
await lookAgain();
|
|
292
|
+
let wrong = await typedWrong(handle, it.harness, command, version, check);
|
|
293
|
+
if (wrong === undefined && Date.now() > deadline) wrong = { why: 'its time ran out' };
|
|
294
|
+
if (wrong !== undefined) {
|
|
295
|
+
takeBack();
|
|
296
|
+
throw new Error(`${it.name}: ${command} was typed but not entered, and was taken back, because ${wrong.why}.${cannot(wrong)}${shownEnd(wrong.rows)}`);
|
|
297
|
+
}
|
|
298
|
+
// A return of its own, not `--enter`: Orca's gate can refuse a line sent
|
|
299
|
+
// with `--enter` while it names a reason, and on Codex a return inside the
|
|
300
|
+
// text lands in the draft (helpers in the system tests, #329).
|
|
301
|
+
send(handle, '\r');
|
|
302
|
+
typed = 0;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
takeBack();
|
|
305
|
+
throw error;
|
|
220
306
|
}
|
|
221
|
-
// A return of its own, not `--enter`: Orca's gate can refuse a line sent
|
|
222
|
-
// with `--enter` while it names a reason, and on Codex a return inside the
|
|
223
|
-
// text lands in the draft (helpers in the system tests, #329).
|
|
224
|
-
send(handle, '\r');
|
|
225
307
|
return { command, handle, typedAt };
|
|
226
308
|
}
|
|
227
309
|
|
|
@@ -235,6 +317,7 @@ async function idleTab(it) {
|
|
|
235
317
|
const until = Date.now() + IDLE_WAIT_MS;
|
|
236
318
|
for (;;) {
|
|
237
319
|
const look = lookAt(it);
|
|
320
|
+
if (look.moved) throw new Error(`${it.name}: nothing was typed, because ${look.why}.`);
|
|
238
321
|
let why = look.why;
|
|
239
322
|
// The wait is a cutoff, not a count of looks: a yes that comes after it
|
|
240
323
|
// is too late (as for LIST_LINE, review of PR #421).
|
|
@@ -248,11 +331,23 @@ async function idleTab(it) {
|
|
|
248
331
|
/**
|
|
249
332
|
* One look through the typing gate at the session's tab: `{ handle }` when
|
|
250
333
|
* nothing on its screen asks a question and nothing says it is at work, and,
|
|
251
|
-
* with `idle`, Orca's tui-idle answered ok; or `{ why }`, naming the signal
|
|
334
|
+
* with `idle`, Orca's tui-idle answered ok; or `{ why }`, naming the signal,
|
|
335
|
+
* with `moved` when the book holds another tab for the session now, or, where
|
|
336
|
+
* `it.conversation` is given, another conversation.
|
|
252
337
|
* Throws when the tab holds no harness.
|
|
253
338
|
*/
|
|
254
339
|
function lookAt(it, { idle = true } = {}) {
|
|
255
|
-
|
|
340
|
+
// A restart can give the session a new tab while this waits or types, and
|
|
341
|
+
// a hook can outlive its Codex (#480): only the tab the book holds is the
|
|
342
|
+
// session's.
|
|
343
|
+
const entry = readBook(it.home).sessions[it.session];
|
|
344
|
+
if (entry?.tab !== it.tabId) return { moved: true, why: `the book no longer holds its tab ${it.tabId} for it, but ${entry?.tab ?? 'none'}` };
|
|
345
|
+
// And, for a caller typing for one conversation, a `/new` in the same tab
|
|
346
|
+
// gives the session another (#480 review).
|
|
347
|
+
if (it.conversation !== undefined && entry.session !== it.conversation) {
|
|
348
|
+
return { moved: true, why: `the book no longer holds the conversation ${it.conversation} for it, but ${entry.session ?? 'none'}` };
|
|
349
|
+
}
|
|
350
|
+
const found = tabToTypeInto(it.home, it.tabId, idle ? LOOK_MS : CHAR_LOOK_MS);
|
|
256
351
|
if (found.blocked !== undefined) {
|
|
257
352
|
return {
|
|
258
353
|
why: found.blocked === QUESTION_ON_SCREEN
|
|
@@ -274,13 +369,14 @@ function lookAt(it, { idle = true } = {}) {
|
|
|
274
369
|
|
|
275
370
|
/**
|
|
276
371
|
* Why the screen does not show `command` typed and ready, or undefined when it
|
|
277
|
-
* does:
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
372
|
+
* does, as `check` reads it: for a slash command alone, the input line, the
|
|
373
|
+
* lowest row the harness's pointer starts, reads the pointer and the command
|
|
374
|
+
* and nothing else, and the slash menu's selected row names the command.
|
|
375
|
+
* `menu` is set when the input line was right and the menu was not. Read
|
|
376
|
+
* again for SCREEN_MS while it is not so: a screen
|
|
281
377
|
* takes a moment to draw what was typed.
|
|
282
378
|
*/
|
|
283
|
-
async function typedWrong(handle, harness, command, version) {
|
|
379
|
+
async function typedWrong(handle, harness, command, version, check) {
|
|
284
380
|
const until = Date.now() + SCREEN_MS;
|
|
285
381
|
for (;;) {
|
|
286
382
|
const seen = screenRows(handle, READ_MS);
|
|
@@ -288,7 +384,7 @@ async function typedWrong(handle, harness, command, version) {
|
|
|
288
384
|
// a question that shows on it stops it at once (review of c1e5ba4).
|
|
289
385
|
const signal = seen.rows === undefined ? undefined : signalIn(seen.rows);
|
|
290
386
|
if (signal !== undefined) return { why: signal, rows: seen.rows };
|
|
291
|
-
const wrong = seen.rows === undefined ? { why: `its screen could not be read (${seen.unreadable})` } :
|
|
387
|
+
const wrong = seen.rows === undefined ? { why: `its screen could not be read (${seen.unreadable})` } : check(seen.rows, harness, command, version);
|
|
292
388
|
if (wrong === undefined || Date.now() >= until) return wrong && { ...wrong, rows: seen.rows };
|
|
293
389
|
await pause(ASK_MS);
|
|
294
390
|
}
|
|
@@ -449,7 +545,10 @@ function shownEnd(rows) {
|
|
|
449
545
|
/** How many rows of the screen a refusal shows. */
|
|
450
546
|
const SHOWN_ROWS = 12;
|
|
451
547
|
|
|
548
|
+
/** How long Orca is given to take one send: a send it never answers must not hold the caller (#480 review). */
|
|
549
|
+
const SEND_MS = 5000;
|
|
550
|
+
|
|
452
551
|
/** Type `keys` into the tab as they are, with no return of Orca's. */
|
|
453
|
-
const send = (handle, keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys]);
|
|
552
|
+
const send = (handle, keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys], { timeoutMs: SEND_MS });
|
|
454
553
|
|
|
455
554
|
const harnessName = (harness) => (harness === 'claude' ? 'Claude Code' : 'Codex');
|
package/src/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, c
|
|
|
16
16
|
import { addCommand, groomCommand, grooming, upCommand } from './groom.js';
|
|
17
17
|
import { checkHealth, orcaSettingFindings } from './health.js';
|
|
18
18
|
import { initBots } from './init.js';
|
|
19
|
+
import { nameSession } from './name.js';
|
|
19
20
|
import { APPROVALS, HARNESSES, ownCli, shellWord, workDirOf } from './launch.js';
|
|
20
21
|
import { checkMail, decideLeftNudges, lookUp, noMailboxYet, sendMessage, sessionInTab } from './message.js';
|
|
21
22
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
@@ -30,7 +31,7 @@ import { readRoster } from './roster.js';
|
|
|
30
31
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
31
32
|
import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
32
33
|
import { addSource, fetchSources } from './sources.js';
|
|
33
|
-
import { listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
|
|
34
|
+
import { answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks } from './temp.js';
|
|
34
35
|
import { readUsage } from './usage.js';
|
|
35
36
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
36
37
|
|
|
@@ -139,6 +140,12 @@ Usage:
|
|
|
139
140
|
all and continue", and check the review went. It
|
|
140
141
|
refuses anything else on that screen, and types
|
|
141
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.
|
|
142
149
|
obk rules build --bots <path> [--bot <bot>]
|
|
143
150
|
Build every bot's AGENTS.md from its charter and
|
|
144
151
|
the rule units it carries, or just the one you
|
|
@@ -274,6 +281,11 @@ Usage:
|
|
|
274
281
|
nudges a send there left because it could not
|
|
275
282
|
tell from inside Codex's sandbox, and types each
|
|
276
283
|
one or says why not.
|
|
284
|
+
obk session name --bots <path> --bot <bot>
|
|
285
|
+
For the kit's own hook, not for typing: after a
|
|
286
|
+
Codex session's turn, it names the session's thread
|
|
287
|
+
<bot>.<session> with Codex's /rename, once the
|
|
288
|
+
session is idle, so its tab says which it is.
|
|
277
289
|
obk session mailbox --bots <path> --bot <bot> --session <name>
|
|
278
290
|
For the kit's own launch line, not for typing: run
|
|
279
291
|
in the session's own tab, it gives the session its
|
|
@@ -317,11 +329,13 @@ const COMMANDS = {
|
|
|
317
329
|
'session record': ['bots', 'bot'],
|
|
318
330
|
'session sent': ['bots', 'bot'],
|
|
319
331
|
'session nudge': ['bots', 'bot'],
|
|
332
|
+
'session name': ['bots', 'bot'],
|
|
320
333
|
'session mailbox': ['bots', 'bot', 'session'],
|
|
321
334
|
'temp make': ['bots', 'name'],
|
|
322
335
|
'temp roles': ['bots'],
|
|
323
336
|
'temp retire': ['bots', 'name'],
|
|
324
337
|
'temp trust-hooks': ['bots', 'name'],
|
|
338
|
+
'temp answer': ['bots', 'name'],
|
|
325
339
|
};
|
|
326
340
|
|
|
327
341
|
/** What each flag is for, in the sentence a caller reads when it is missing. */
|
|
@@ -449,6 +463,7 @@ async function run(argv) {
|
|
|
449
463
|
// there included, so it goes before anything that can complain (ADR 0032).
|
|
450
464
|
if (command === SENT) return sent(path.resolve(values.bots));
|
|
451
465
|
if (command === NUDGE) return nudgeLeft();
|
|
466
|
+
if (command === NAME) return nameThread(path.resolve(values.bots), values.bot);
|
|
452
467
|
|
|
453
468
|
// One fleet, one identity, whatever spelling of its path was given (#164).
|
|
454
469
|
const bots = command === 'init' ? path.resolve(values.bots) : sameFleet(path.resolve(values.bots));
|
|
@@ -517,6 +532,7 @@ const ANSWER_IT = [
|
|
|
517
532
|
const RECORD = 'session record';
|
|
518
533
|
const SENT = 'session sent';
|
|
519
534
|
const NUDGE = 'session nudge';
|
|
535
|
+
const NAME = 'session name';
|
|
520
536
|
|
|
521
537
|
/**
|
|
522
538
|
* What the kit's send hook says after a native message: a warning the session
|
|
@@ -556,6 +572,21 @@ function nudgeLeft() {
|
|
|
556
572
|
return 0;
|
|
557
573
|
}
|
|
558
574
|
|
|
575
|
+
/**
|
|
576
|
+
* What the kit's Codex Stop hook does at a turn end: name the session's thread
|
|
577
|
+
* after its bot and session, once it is idle (#480). Silent, never a decision,
|
|
578
|
+
* and never anything but exit 0: what it could not type now waits for the next
|
|
579
|
+
* turn end.
|
|
580
|
+
*/
|
|
581
|
+
async function nameThread(bots, bot) {
|
|
582
|
+
try {
|
|
583
|
+
await nameSession(bots, bot, JSON.parse(readFileSync(0, 'utf8')), process.env[TAB_ENV]);
|
|
584
|
+
} catch {
|
|
585
|
+
// Nothing: a hook does not disturb the session it runs in.
|
|
586
|
+
}
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
|
|
559
590
|
/**
|
|
560
591
|
* What the kit's hook does with what the harness told it, and what it answers.
|
|
561
592
|
*
|
|
@@ -768,6 +799,19 @@ const commands = {
|
|
|
768
799
|
};
|
|
769
800
|
},
|
|
770
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
|
+
|
|
771
815
|
'bot create'(bots, values) {
|
|
772
816
|
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
|
|
773
817
|
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
package/src/groom.js
CHANGED
|
@@ -42,7 +42,7 @@ import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
|
42
42
|
import path from 'node:path';
|
|
43
43
|
|
|
44
44
|
import { botDir, readBot, requireBotsFolder } from './bot.js';
|
|
45
|
-
import { readBook } from './book.js';
|
|
45
|
+
import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
46
46
|
import { claudeTranscript } from './conversations.js';
|
|
47
47
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
48
48
|
import { tabs, tabToTypeInto, typeIntoTab } from './orca.js';
|
|
@@ -189,21 +189,28 @@ function refuseRun(run, ask) {
|
|
|
189
189
|
/**
|
|
190
190
|
* Type `line` into the grooming tab, through the kit's one gate for typing into
|
|
191
191
|
* a running session: nothing goes into a tab with something on screen waiting
|
|
192
|
-
* for an answer, or with anything but Claude Code in front of it.
|
|
192
|
+
* for an answer, or with anything but Claude Code in front of it. In the
|
|
193
|
+
* session's turn for a line, so it never lands in what the kit is typing (#482).
|
|
193
194
|
*/
|
|
194
195
|
function typeInto(bots, home, session, tab, line) {
|
|
195
196
|
if (!session.up) {
|
|
196
197
|
throw new Error(`${BOT_FATHER}'s ${GROOMING} session is not up, so nothing was typed. Bring it up with ${upCommand(bots)}, then run this again.`);
|
|
197
198
|
}
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
const turn = takeLineTurn(home, GROOMING, TYPING_WAIT_MS);
|
|
200
|
+
if (turn === undefined) throw new Error(`the ${GROOMING} tab: ${TYPING_HELD}. Run this again in a moment.`);
|
|
201
|
+
try {
|
|
202
|
+
const found = tabToTypeInto(home, tab, LOOK_MS);
|
|
203
|
+
if (found.blocked !== undefined) {
|
|
204
|
+
throw new Error(`the ${GROOMING} tab is waiting for an answer (${found.blocked}), so nothing was typed. Answer it in the tab, then run this again.`);
|
|
205
|
+
}
|
|
206
|
+
if (found.unsure !== undefined) throw new Error(`the ${GROOMING} tab: ${found.unsure}.`);
|
|
207
|
+
if (found.handle === undefined || found.agent !== 'claude') {
|
|
208
|
+
throw new Error(`the ${GROOMING} tab has no Claude Code in front of it, so nothing was typed. Bring it back with ${restartCommand(bots)}, then run this again.`);
|
|
209
|
+
}
|
|
210
|
+
typeIntoTab(found.handle, line);
|
|
211
|
+
} finally {
|
|
212
|
+
turn.release();
|
|
205
213
|
}
|
|
206
|
-
typeIntoTab(found.handle, line);
|
|
207
214
|
}
|
|
208
215
|
|
|
209
216
|
/**
|