@assuredloop/orca-bot-kit 0.6.0 → 0.8.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/README.md +30 -2
- package/SETUP.md +3 -2
- package/package.json +1 -1
- package/rules/commit.md +18 -0
- package/skills/obk-bot-building/SKILL.md +6 -2
- package/skills/obk-fleet-review/SKILL.md +2 -1
- package/skills/obk-grooming/NOTICE.md +21 -5
- package/skills/obk-grooming/SKILL.md +49 -12
- package/src/book.js +2 -4
- package/src/bot.js +1 -1
- package/src/cli.js +92 -25
- package/src/conversations.js +7 -1
- package/src/groom.js +263 -105
- package/src/health.js +29 -7
- package/src/hooks.js +3 -3
- package/src/message.js +8 -6
- package/src/orca-window.cjs +1 -1
- package/src/orca.js +79 -7
- package/src/record.js +25 -11
- package/src/skills.js +1 -1
- package/src/up.js +8 -5
package/src/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Every call the kit makes to Orca goes through here (ADR
|
|
1
|
+
// Every call the kit makes to Orca goes through here (ADR 0023). Orca's CLI
|
|
2
2
|
// changes often, so the kit reads `--json` and never the human text, and keeps
|
|
3
3
|
// the parsing in one place.
|
|
4
4
|
//
|
|
@@ -192,7 +192,7 @@ const CLIENT_KILL_MS = 3000;
|
|
|
192
192
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
193
193
|
* changes does, and Orca's CLI does not offer it, so this goes through Orca's
|
|
194
194
|
* own runtime client out of the installed app, run by Orca's binary the way
|
|
195
|
-
* its `bin/orca` runs its CLI (ADR
|
|
195
|
+
* its `bin/orca` runs its CLI (ADR 0023). None of that is Orca's published
|
|
196
196
|
* interface, so anything that goes wrong is a quiet false: it never throws and
|
|
197
197
|
* is never tried twice, and the caller prints `RELOAD_LINE` either way.
|
|
198
198
|
*/
|
|
@@ -262,7 +262,9 @@ export const retitleTab = (handle, title) =>
|
|
|
262
262
|
* undefined when that cannot be read, with `unreadable` saying why. `answered` (an `ok` from `tui-idle`) and
|
|
263
263
|
* `agent` are Orca's own hints, for a caller that has nothing better.
|
|
264
264
|
* `blockedReason` is Orca saying something on screen wants answering, which is
|
|
265
|
-
* for the caller to deal with, not the kit.
|
|
265
|
+
* for the caller to deal with, not the kit. `question` is the kit's own look at
|
|
266
|
+
* the screen, as `questionIn` answers, and `screenUnreadable` says why there
|
|
267
|
+
* was none.
|
|
266
268
|
*/
|
|
267
269
|
export function harnessInTab(handle, timeoutMs) {
|
|
268
270
|
const args = ['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)];
|
|
@@ -281,9 +283,59 @@ export function harnessInTab(handle, timeoutMs) {
|
|
|
281
283
|
blockedReason: answer.result?.wait?.blockedReason,
|
|
282
284
|
agent,
|
|
283
285
|
...frontOf(shown?.ptyId),
|
|
286
|
+
...screenOf(handle),
|
|
284
287
|
};
|
|
285
288
|
}
|
|
286
289
|
|
|
290
|
+
/** The kit's word for a tab whose screen shows a question of its harness's own (#329). */
|
|
291
|
+
export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* What the tab `handle` renders: `{ question }`, true when it shows a question
|
|
295
|
+
* of its harness's own, or `{ screenUnreadable: <why> }`. Orca refusing is an
|
|
296
|
+
* answer that cannot be read, not an error, and so is anything that is not the
|
|
297
|
+
* rendered screen: accumulated output comes back in fragments (tech notes,
|
|
298
|
+
* section 1).
|
|
299
|
+
*/
|
|
300
|
+
function screenOf(handle) {
|
|
301
|
+
let read;
|
|
302
|
+
try {
|
|
303
|
+
read = orca(['terminal', 'read', '--terminal', handle, '--screen']).terminal;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return { screenUnreadable: `Orca would not read its screen: ${error.message}` };
|
|
306
|
+
}
|
|
307
|
+
if (read?.source !== 'screen' || !Array.isArray(read.tail)) {
|
|
308
|
+
return { screenUnreadable: `Orca gave no rendered screen for it (source: ${read?.source ?? 'none'})` };
|
|
309
|
+
}
|
|
310
|
+
return { question: questionIn(read.tail) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A row the harness starts with its selection pointer: `›` on Codex, `❯` on Claude Code. */
|
|
314
|
+
const POINTER_ROW = /^ *[›❯]/;
|
|
315
|
+
|
|
316
|
+
/** That pointer on a numbered choice, and the column its number starts in. */
|
|
317
|
+
const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
321
|
+
*
|
|
322
|
+
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
323
|
+
* one (tech notes, section 1; ADR 0023). The same pointer starts the harness's
|
|
324
|
+
* input line and its echo of the user's past turns, and the input line is the
|
|
325
|
+
* lowest of them whenever it is on screen, so only the lowest pointer row is
|
|
326
|
+
* asked about: a question counts while it stands in the input line's place.
|
|
327
|
+
* Unnumbered lists do not count. Codex puts a status row right under its input
|
|
328
|
+
* line, lined up with it, and by its layout that is one.
|
|
329
|
+
*/
|
|
330
|
+
export function questionIn(rows) {
|
|
331
|
+
const at = rows.findLastIndex((row) => POINTER_ROW.test(row));
|
|
332
|
+
const pointer = at < 0 ? null : ON_A_CHOICE.exec(rows[at]);
|
|
333
|
+
if (pointer === null) return false;
|
|
334
|
+
const column = pointer[1].length;
|
|
335
|
+
const choice = (row) => typeof row === 'string' && row.slice(0, column).trim() === '' && /^\d+\. /.test(row.slice(column));
|
|
336
|
+
return choice(rows[at - 1]) || choice(rows[at + 1]);
|
|
337
|
+
}
|
|
338
|
+
|
|
287
339
|
/**
|
|
288
340
|
* Whether the kit may type a line into `tabId`, the tab the book holds for a
|
|
289
341
|
* session in the Orca project at `home`, and the handle to type into when it
|
|
@@ -323,6 +375,13 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
323
375
|
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
324
376
|
return { unsure: `the kit could not tell whether a harness is running in it (${why}), so nothing was typed` };
|
|
325
377
|
}
|
|
378
|
+
// Orca's reason does not cover every question: it called Codex's update
|
|
379
|
+
// offer idle, and a return typed into it took "Update now" (#329). So the
|
|
380
|
+
// screen is read too, and one that cannot be read is not a reason to type.
|
|
381
|
+
if (seen.question === true) return { blocked: QUESTION_ON_SCREEN };
|
|
382
|
+
if (seen.question === undefined) {
|
|
383
|
+
return { unsure: `the kit could not tell whether a question is waiting on its screen (${seen.screenUnreadable}), so nothing was typed` };
|
|
384
|
+
}
|
|
326
385
|
return { handle: live.handle, agent: seen.agent };
|
|
327
386
|
}
|
|
328
387
|
|
|
@@ -346,11 +405,11 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
346
405
|
|
|
347
406
|
/**
|
|
348
407
|
* Who holds the terminal of the pane `ptyId`: `{ front: 'shell' }`,
|
|
349
|
-
* `{ front: 'program', command }` with the name of the process
|
|
350
|
-
* group in front, or `{ unreadable: <why> }`.
|
|
408
|
+
* `{ front: 'program', command, pid }` with the name and pid of the process
|
|
409
|
+
* leading the group in front, or `{ unreadable: <why> }`.
|
|
351
410
|
*
|
|
352
411
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
353
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
412
|
+
* gives that pid's terminal's foreground process group (ADR 0023).
|
|
354
413
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
355
414
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
356
415
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
|
@@ -374,7 +433,20 @@ function frontOf(ptyId) {
|
|
|
374
433
|
const front = psLine(own.tpgid);
|
|
375
434
|
if (front === undefined) return { unreadable: `ps could not read the process in front, pid ${own.tpgid}` };
|
|
376
435
|
const shell = front.ppid === pane && path.basename(own.comm) === 'login';
|
|
377
|
-
return shell ? { front: 'shell' } : { front: 'program', command: path.basename(front.comm) };
|
|
436
|
+
return shell ? { front: 'shell' } : { front: 'program', command: path.basename(front.comm), pid: own.tpgid };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The process `pid` as `ps -E` gives it, word by word: its command, its
|
|
441
|
+
* arguments, then its environment as `NAME=value` words. Undefined when it
|
|
442
|
+
* cannot be read. macOS gives a same-user harness's environment this way (tech
|
|
443
|
+
* notes, section 1); nothing in it says where the arguments end, so a caller
|
|
444
|
+
* looks for one whole word it knows.
|
|
445
|
+
*/
|
|
446
|
+
export function wordsOfProcess(pid) {
|
|
447
|
+
const asked = spawnSync(psCli(), ['-E', '-ww', '-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
448
|
+
if (asked.error || asked.status !== 0) return undefined;
|
|
449
|
+
return asked.stdout.trim().split(/\s+/);
|
|
378
450
|
}
|
|
379
451
|
|
|
380
452
|
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, or undefined. */
|
package/src/record.js
CHANGED
|
@@ -166,22 +166,26 @@ function unclaimedFor(book, home, bot, tabId, id) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Whether the harness that ran this hook is the session's own — the one the
|
|
170
|
-
*
|
|
169
|
+
* Whether the harness that ran this hook is the session's own — the one the
|
|
170
|
+
* tab's shell started — rather than a harness started inside the session.
|
|
171
171
|
*
|
|
172
172
|
* Proved live on both harnesses (tech notes, sections 2 and 3): a hook's process
|
|
173
173
|
* is the harness's own child, or the child of the shell the harness ran the hook
|
|
174
174
|
* command through. Either way the harness itself is the process just above, and
|
|
175
|
-
* the session's harness is the child of the shell
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* the session's harness is the child of the tab's shell. A harness started
|
|
176
|
+
* inside the session sits further down a longer chain, under whatever shell the
|
|
177
|
+
* agent ran it from, so it can never answer this.
|
|
178
|
+
*
|
|
179
|
+
* The launch line names the tab's shell. A tab Orca brought back by itself has
|
|
180
|
+
* no launch line (#318), and there the tab's shell is the one `login` started
|
|
181
|
+
* for the pane, which is where Orca's own resume puts the harness too (tech
|
|
182
|
+
* notes, section 1).
|
|
178
183
|
*
|
|
179
184
|
* Anything it cannot establish is a no: a report that cannot be shown to be the
|
|
180
185
|
* session's own is not written down.
|
|
181
186
|
*/
|
|
182
187
|
export function startedTheSession(shellPid, pid = process.pid) {
|
|
183
|
-
if (!/^[0-9]+$/.test(String(shellPid
|
|
184
|
-
const shell = Number(shellPid);
|
|
188
|
+
if (shellPid !== undefined && !/^[0-9]+$/.test(String(shellPid))) return false;
|
|
185
189
|
|
|
186
190
|
const tree = processTree();
|
|
187
191
|
if (tree === undefined) return false;
|
|
@@ -190,11 +194,21 @@ export function startedTheSession(shellPid, pid = process.pid) {
|
|
|
190
194
|
const parent = mine === undefined ? undefined : tree.get(mine.ppid);
|
|
191
195
|
if (parent === undefined) return false;
|
|
192
196
|
|
|
193
|
-
if (
|
|
194
|
-
|
|
197
|
+
if (shellPid !== undefined) {
|
|
198
|
+
const shell = Number(shellPid);
|
|
199
|
+
if (parent.ppid === shell) return true;
|
|
200
|
+
if (!isShell(parent.command)) return false;
|
|
201
|
+
|
|
202
|
+
const above = tree.get(parent.ppid);
|
|
203
|
+
return above !== undefined && above.ppid === shell;
|
|
204
|
+
}
|
|
195
205
|
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
// With no shell named, which process is the harness has to be settled first:
|
|
207
|
+
// the one above the shell the hook ran through, or the hook's own parent.
|
|
208
|
+
const harness = isShell(parent.command) ? tree.get(parent.ppid) : parent;
|
|
209
|
+
const shell = harness === undefined ? undefined : tree.get(harness.ppid);
|
|
210
|
+
const pane = shell === undefined ? undefined : tree.get(shell.ppid);
|
|
211
|
+
return pane !== undefined && path.basename(pane.command) === 'login';
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
/** Every process on the machine, by pid: who its parent is and what it runs. */
|
package/src/skills.js
CHANGED
|
@@ -292,7 +292,7 @@ export function unlinkSkills(home) {
|
|
|
292
292
|
* user made, or one of the kit's that they have since repointed, is theirs.
|
|
293
293
|
*
|
|
294
294
|
* It is the kit's own file and lives beside the hook files it already keeps in
|
|
295
|
-
* the bot folder (ADR
|
|
295
|
+
* the bot folder (ADR 0022). A record that cannot be read is treated as an
|
|
296
296
|
* empty one: the worst that follows is that the kit leaves alone something it
|
|
297
297
|
* would otherwise have tidied up, which is the safe way round.
|
|
298
298
|
*/
|
package/src/up.js
CHANGED
|
@@ -13,7 +13,7 @@ import { botDir, botNames, displayName, readBot } from './bot.js';
|
|
|
13
13
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
14
|
import { installHook } from './hooks.js';
|
|
15
15
|
import { addressOf, harnessOf, isAddressOf, isShortPrompt, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
16
|
-
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, retitleTab, tabs, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
16
|
+
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, tabs, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
17
17
|
import { TAB_ENV } from './record.js';
|
|
18
18
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
19
19
|
import { linkSkills } from './skills.js';
|
|
@@ -94,7 +94,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
94
94
|
*
|
|
95
95
|
* Separate from the tabs because the order matters twice over. A session reads
|
|
96
96
|
* its rules, its skills and its hooks as it comes up, so all three have to be
|
|
97
|
-
* there before the tab is (PRD 6.6, ADR
|
|
97
|
+
* there before the tab is (PRD 6.6, ADR 0022) — and a restart closes a tab in
|
|
98
98
|
* between, so everything that can refuse must have refused before that. It
|
|
99
99
|
* refuses by throwing, exactly as `up` always has; a bot it leaves out of
|
|
100
100
|
* `running` is one whose sessions must not be started.
|
|
@@ -140,7 +140,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
140
140
|
|
|
141
141
|
// And the kit's hook goes into every bot folder before Orca is asked for
|
|
142
142
|
// anything, for the same reason: a harness reads its hooks when it comes up,
|
|
143
|
-
// so one written later would miss the session it was written for (ADR
|
|
143
|
+
// so one written later would miss the session it was written for (ADR 0022),
|
|
144
144
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
145
145
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
146
146
|
// sessions gets none.
|
|
@@ -440,7 +440,7 @@ async function heldInRecord(home, name, harness, prompt, { launched, running })
|
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* One look at a tab the kit just typed a launch line into: whether a harness
|
|
443
|
-
* is running there, and what it is waiting on if Orca says.
|
|
443
|
+
* is running there, and what it is waiting on if Orca or its screen says.
|
|
444
444
|
*
|
|
445
445
|
* The harness is up when something other than the shell holds the tab's
|
|
446
446
|
* terminal, busy or not, or when Orca sees a question on its screen. Orca's
|
|
@@ -452,7 +452,10 @@ function lookFor(handle, timeoutMs) {
|
|
|
452
452
|
const seen = harnessInTab(handle, timeoutMs);
|
|
453
453
|
const running = seen.blockedReason !== undefined
|
|
454
454
|
|| (seen.front === undefined ? seen.answered || seen.agent !== undefined : seen.front === 'program');
|
|
455
|
-
|
|
455
|
+
// A question the kit sees on the screen of a harness that is running, where
|
|
456
|
+
// Orca named none (#329).
|
|
457
|
+
const question = running && seen.question === true ? QUESTION_ON_SCREEN : undefined;
|
|
458
|
+
return { running, blockedReason: seen.blockedReason ?? question };
|
|
456
459
|
}
|
|
457
460
|
|
|
458
461
|
/**
|