@assuredloop/orca-bot-kit 0.9.0 → 0.11.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 +8 -0
- package/SETUP.md +22 -16
- package/package.json +1 -1
- package/rules/limits.md +4 -0
- package/rules/mail.md +1 -2
- package/skills/obk-bot-building/SKILL.md +54 -13
- package/src/book.js +60 -4
- package/src/bot.js +40 -1
- package/src/cli.js +161 -34
- package/src/health.js +2 -0
- package/src/hooks.js +2 -2
- package/src/message.js +83 -11
- package/src/orca-reload.applescript +45 -0
- package/src/orca-runtime.cjs +28 -0
- package/src/orca.js +123 -26
- package/src/permissions.js +485 -0
- package/src/retire.js +12 -25
- package/src/rules.js +16 -1
- package/src/up.js +59 -18
- package/src/orca-window.cjs +0 -21
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 0025). 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
|
//
|
|
@@ -177,7 +177,7 @@ export function asFolderProject(setupId, title) {
|
|
|
177
177
|
*/
|
|
178
178
|
export const deleteProject = (setupId) => orca(['project', 'setup-delete', '--setup', setupId]);
|
|
179
179
|
|
|
180
|
-
/** Said after a run that made
|
|
180
|
+
/** Said after a run that made or renamed a project, whatever `tellWindow` answered, and after a removal `reloadWindow` could not follow. */
|
|
181
181
|
export const RELOAD_LINE = "If Orca's sidebar does not show it, reload the window with Cmd+Shift+R.";
|
|
182
182
|
|
|
183
183
|
/** How long Orca's client waits for its runtime, and how long the kit waits for the client. */
|
|
@@ -190,13 +190,22 @@ const CLIENT_KILL_MS = 3000;
|
|
|
190
190
|
*
|
|
191
191
|
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
192
192
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
193
|
-
* changes does, and Orca's CLI does not offer it
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* interface, so anything that goes wrong is a quiet false: it never throws and
|
|
197
|
-
* is never tried twice, and the caller prints `RELOAD_LINE` either way.
|
|
193
|
+
* changes does, and Orca's CLI does not offer it (ADR 0025). The caller prints
|
|
194
|
+
* `RELOAD_LINE` either way. It does not take a removed project out of the
|
|
195
|
+
* sidebar: `reloadWindow` does.
|
|
198
196
|
*/
|
|
199
|
-
export
|
|
197
|
+
export const tellWindow = (projectId) => askRuntime('project.update', { projectId, updates: {} }) !== undefined;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* One call to Orca's runtime for a method its CLI does not offer, and the
|
|
201
|
+
* runtime's answer, or undefined when there is none to be had.
|
|
202
|
+
*
|
|
203
|
+
* It goes through Orca's own runtime client out of the installed app, run by
|
|
204
|
+
* Orca's binary the way its `bin/orca` runs its CLI (ADR 0025). None of that is
|
|
205
|
+
* Orca's published interface, so anything that goes wrong is a quiet
|
|
206
|
+
* undefined: it never throws and is never tried twice.
|
|
207
|
+
*/
|
|
208
|
+
function askRuntime(method, params) {
|
|
200
209
|
try {
|
|
201
210
|
// The CLI sits at <Orca.app>/Contents/Resources/bin/orca, often reached
|
|
202
211
|
// through a link.
|
|
@@ -209,16 +218,55 @@ export function tellWindow(projectId) {
|
|
|
209
218
|
|
|
210
219
|
const asked = spawnSync(
|
|
211
220
|
path.join(contents, 'MacOS', 'Orca'),
|
|
212
|
-
[
|
|
213
|
-
{ env, stdio: 'ignore', timeout: CLIENT_KILL_MS },
|
|
221
|
+
[RUNTIME_SCRIPT, client, method, JSON.stringify(params), String(CLIENT_WAIT_MS)],
|
|
222
|
+
{ env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: CLIENT_KILL_MS },
|
|
214
223
|
);
|
|
215
|
-
|
|
224
|
+
if (asked.error !== undefined || asked.status !== 0) return undefined;
|
|
225
|
+
return parse(asked.stdout) ?? undefined;
|
|
226
|
+
} catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const RUNTIME_SCRIPT = fileURLToPath(new URL('./orca-runtime.cjs', import.meta.url));
|
|
232
|
+
|
|
233
|
+
/** The `osascript` this run asks. OBK_OSASCRIPT overrides it, as OBK_PS does `ps`. */
|
|
234
|
+
const osascriptCli = () => process.env.OBK_OSASCRIPT || '/usr/bin/osascript';
|
|
235
|
+
|
|
236
|
+
/** How long macOS is given to click the menu item. */
|
|
237
|
+
const RELOAD_KILL_MS = 5000;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Have Orca's window force-reload itself, after a project was removed: true
|
|
241
|
+
* only when it did (#343, ADR 0025).
|
|
242
|
+
*
|
|
243
|
+
* The window keeps a removed project in its sidebar until it is rebuilt, and
|
|
244
|
+
* `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
|
|
245
|
+
* release fixes it, this can go behind a version check). Orca's own menu item
|
|
246
|
+
* Force Reload rebuilds it and keeps every terminal. So on macOS the kit has
|
|
247
|
+
* System Events click that item, in the Orca app its CLI belongs to and no
|
|
248
|
+
* other, and only while that Orca is the front app: a click on a background
|
|
249
|
+
* Orca is taken and does nothing, and the kit does not take the user's screen. None of that is Orca's published interface, and macOS may refuse it,
|
|
250
|
+
* so anything that goes wrong is a quiet false, as with `tellWindow`: it never
|
|
251
|
+
* throws and is never tried twice, and the caller prints `RELOAD_LINE`.
|
|
252
|
+
*/
|
|
253
|
+
export function reloadWindow() {
|
|
254
|
+
if (process.platform !== 'darwin') return false;
|
|
255
|
+
try {
|
|
256
|
+
// <Orca.app>/Contents/Resources/bin/orca, often reached through a link.
|
|
257
|
+
const app = path.resolve(realpathSync(orcaCli()), '..', '..', '..', '..');
|
|
258
|
+
const asked = spawnSync(osascriptCli(), [RELOAD_SCRIPT, app], {
|
|
259
|
+
encoding: 'utf8',
|
|
260
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
261
|
+
timeout: RELOAD_KILL_MS,
|
|
262
|
+
});
|
|
263
|
+
return asked.error === undefined && asked.status === 0 && asked.stdout.trim() === 'reloaded';
|
|
216
264
|
} catch {
|
|
217
265
|
return false;
|
|
218
266
|
}
|
|
219
267
|
}
|
|
220
268
|
|
|
221
|
-
const
|
|
269
|
+
const RELOAD_SCRIPT = fileURLToPath(new URL('./orca-reload.applescript', import.meta.url));
|
|
222
270
|
|
|
223
271
|
/**
|
|
224
272
|
* The live tabs of the Orca project at `home`, each under its own tab id.
|
|
@@ -229,9 +277,9 @@ const WINDOW_SCRIPT = fileURLToPath(new URL('./orca-window.cjs', import.meta.url
|
|
|
229
277
|
* the real id, so an orphaned entry is given that one: the tab id is the key
|
|
230
278
|
* (PRD 6.2), and this is where Orca is asked for it (tech notes, section 1).
|
|
231
279
|
*/
|
|
232
|
-
export const tabs = (home) => orca(['terminal', 'list', '--worktree', `path:${home}`]).terminals
|
|
280
|
+
export const tabs = (home, options) => orca(['terminal', 'list', '--worktree', `path:${home}`], options).terminals
|
|
233
281
|
.map((tab) => (tab.orphaned === true
|
|
234
|
-
? { ...tab, tabId: orca(['terminal', 'show', '--terminal', tab.handle]).terminal.tabId }
|
|
282
|
+
? { ...tab, tabId: orca(['terminal', 'show', '--terminal', tab.handle], options).terminal.tabId }
|
|
235
283
|
: tab));
|
|
236
284
|
|
|
237
285
|
/**
|
|
@@ -282,11 +330,14 @@ export function harnessInTab(handle, timeoutMs) {
|
|
|
282
330
|
answered: answer.ok === true,
|
|
283
331
|
blockedReason: answer.result?.wait?.blockedReason,
|
|
284
332
|
agent,
|
|
285
|
-
...frontOf(shown?.ptyId),
|
|
333
|
+
...frontOf(handle, shown?.ptyId),
|
|
286
334
|
...screenOf(handle),
|
|
287
335
|
};
|
|
288
336
|
}
|
|
289
337
|
|
|
338
|
+
/** Orca's reason for a folder-trust screen, which it can go on giving once that is answered (#342). */
|
|
339
|
+
const TRUST_REASON = 'agent-trust-workspace';
|
|
340
|
+
|
|
290
341
|
/** The kit's word for a tab whose screen shows a question of its harness's own (#329). */
|
|
291
342
|
export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
292
343
|
|
|
@@ -320,7 +371,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
320
371
|
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
321
372
|
*
|
|
322
373
|
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
323
|
-
* one (tech notes, section 1; ADR
|
|
374
|
+
* one (tech notes, section 1; ADR 0025). The same pointer starts the harness's
|
|
324
375
|
* input line and its echo of the user's past turns, and the input line is the
|
|
325
376
|
* lowest of them whenever it is on screen, so only the lowest pointer row is
|
|
326
377
|
* asked about: a question counts while it stands in the input line's place.
|
|
@@ -359,6 +410,14 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
359
410
|
// A line typed into a screen waiting for an answer is that answer: once, it
|
|
360
411
|
// confirmed Claude Code's folder-trust default, `No, exit` (tech notes,
|
|
361
412
|
// section 1).
|
|
413
|
+
// Except where Orca still reports Codex's trust screen after it was
|
|
414
|
+
// answered, over an idle screen (#342). Nothing is typed then either, but
|
|
415
|
+
// there is nothing to answer, so the kit says what it saw. Claude Code's
|
|
416
|
+
// trust list is not numbered and the screen read cannot see it, so this is
|
|
417
|
+
// for Codex alone.
|
|
418
|
+
if (seen.blockedReason === TRUST_REASON && seen.agent === 'codex' && seen.question === false) {
|
|
419
|
+
return { unsure: `Orca still reports ${TRUST_REASON} in it, and its screen shows no question: Orca can go on reporting the trust screen after it is answered, and refuse a typed line while it does, so nothing was typed` };
|
|
420
|
+
}
|
|
362
421
|
if (seen.blockedReason !== undefined) return { blocked: seen.blockedReason };
|
|
363
422
|
// The shell in front is a tab with no harness, whatever a stale
|
|
364
423
|
// `agentIdentity` says.
|
|
@@ -397,25 +456,63 @@ export function frontOfTab(handle) {
|
|
|
397
456
|
} catch (error) {
|
|
398
457
|
return { unreadable: `Orca would not show the tab: ${error.message}` };
|
|
399
458
|
}
|
|
400
|
-
return frontOf(shown?.ptyId);
|
|
459
|
+
return frontOf(handle, shown?.ptyId);
|
|
401
460
|
}
|
|
402
461
|
|
|
403
462
|
/** The `ps` this run reads. OBK_PS overrides it, as OBK_ORCA does Orca. */
|
|
404
463
|
const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
405
464
|
|
|
406
465
|
/**
|
|
407
|
-
* Who holds the terminal of the
|
|
408
|
-
* `{ front: 'program', command, pid }` with the name and
|
|
409
|
-
* leading the group in front, or
|
|
466
|
+
* Who holds the terminal of the tab `handle`, whose pane is `ptyId`:
|
|
467
|
+
* `{ front: 'shell' }`, `{ front: 'program', command, pid }` with the name and,
|
|
468
|
+
* where `ps` gave it, the pid of the process leading the group in front, or
|
|
469
|
+
* `{ unreadable: <why> }`.
|
|
470
|
+
*
|
|
471
|
+
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
472
|
+
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
473
|
+
* (#298, ADR 0025).
|
|
474
|
+
*/
|
|
475
|
+
function frontOf(handle, ptyId) {
|
|
476
|
+
const read = frontByPs(ptyId);
|
|
477
|
+
if (read.unreadable === undefined) return read;
|
|
478
|
+
const asked = frontByOrca(handle);
|
|
479
|
+
return asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Who holds the tab's terminal as Orca's runtime says it, through
|
|
484
|
+
* `terminal.inspectProcess`: Orca's own daemon reads the process table from
|
|
485
|
+
* outside any sandbox of the kit's, and names what leads the terminal's
|
|
486
|
+
* foreground group. Only a `live` answer counts. A process named there is the
|
|
487
|
+
* program in front; none named and no child in front is the shell. Anything
|
|
488
|
+
* else is "cannot tell": for a moment after a harness quits, Orca can name
|
|
489
|
+
* nothing and still see a child in front (read in the Orca 1.4.212 bundle).
|
|
490
|
+
*/
|
|
491
|
+
function frontByOrca(handle) {
|
|
492
|
+
const seen = askRuntime('terminal.inspectProcess', { terminal: handle })?.result?.process;
|
|
493
|
+
const evidence = seen?.foregroundProcessEvidence;
|
|
494
|
+
if (evidence?.verdict !== 'live') {
|
|
495
|
+
const why = typeof evidence?.reason === 'string' ? `: ${evidence.reason}` : '';
|
|
496
|
+
return { unreadable: `Orca's runtime could not say who is in front of it${why}` };
|
|
497
|
+
}
|
|
498
|
+
const name = (word) => typeof word === 'string' && word !== '';
|
|
499
|
+
if (name(evidence.processName)) return { front: 'program', command: evidence.processName };
|
|
500
|
+
if (evidence.processName === null && name(seen.foregroundProcess)) return { front: 'program', command: seen.foregroundProcess };
|
|
501
|
+
if (evidence.processName === null && seen.foregroundProcess === null && seen.hasChildProcesses === false) return { front: 'shell' };
|
|
502
|
+
return { unreadable: "Orca's runtime gave no answer the kit can read about who is in front of it" };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Who holds the terminal of the pane `ptyId`, as `ps` says it.
|
|
410
507
|
*
|
|
411
508
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
412
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
509
|
+
* gives that pid's terminal's foreground process group (ADR 0025).
|
|
413
510
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
414
511
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
415
512
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
|
416
513
|
* here ends in "cannot tell" rather than in an error or a guess.
|
|
417
514
|
*/
|
|
418
|
-
function
|
|
515
|
+
function frontByPs(ptyId) {
|
|
419
516
|
let pane;
|
|
420
517
|
try {
|
|
421
518
|
pane = orca(['diagnostics', 'memory']).worktrees
|
|
@@ -548,12 +645,12 @@ export function postMessage({ to, from, subject, body, type = 'status', thread }
|
|
|
548
645
|
* the terminal `handle` when one is given, which is how a read works from a tab
|
|
549
646
|
* bound to another Run; otherwise as this process's own terminal.
|
|
550
647
|
*/
|
|
551
|
-
export const readMailbox = (id, { peek = false, handle } = {}) =>
|
|
552
|
-
orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])]);
|
|
648
|
+
export const readMailbox = (id, { peek = false, handle } = {}, options) =>
|
|
649
|
+
orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])], options);
|
|
553
650
|
|
|
554
651
|
/** Say a batch has been read. Orca answers with the batch after it. */
|
|
555
|
-
export const ackMailbox = (id, delivery, handle) =>
|
|
556
|
-
orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery]);
|
|
652
|
+
export const ackMailbox = (id, delivery, handle, options) =>
|
|
653
|
+
orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery], options);
|
|
557
654
|
|
|
558
655
|
/** The reader of a `check`: the terminal named, or this process's own. */
|
|
559
656
|
const as = (handle) => (handle === undefined ? [] : ['--terminal', handle]);
|