@assuredloop/orca-bot-kit 0.28.0 → 0.29.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 +9 -2
- package/SETUP.md +6 -3
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +6 -2
- package/src/cli.js +21 -2
- package/src/commands.js +4 -1
- package/src/health.js +71 -1
- package/src/hooks.js +37 -1
- package/src/orca-reload.applescript +20 -21
- package/src/orca.js +31 -7
- package/src/permissions.js +50 -7
- package/src/rules.js +13 -2
- package/src/run.js +155 -0
- package/src/up.js +4 -2
package/README.md
CHANGED
|
@@ -166,8 +166,8 @@ nobody asked: its own commands for this bots folder, reading a long message,
|
|
|
166
166
|
`git add` and `git commit`
|
|
167
167
|
([ADR 0050](docs/adr/0050-the-kit-writes-every-bots-default-permission-rules.md)).
|
|
168
168
|
`obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
|
|
169
|
-
`obk bot change`, `obk session add`, `obk session change
|
|
170
|
-
not in the set, and keep your yes
|
|
169
|
+
`obk bot change`, `obk session add`, `obk session change`, `obk init` and
|
|
170
|
+
`obk run` are not in the set, and keep your yes
|
|
171
171
|
([ADR 0051](docs/adr/0051-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
|
|
172
172
|
A rule beyond the set is written by `obk permission allow`, after your yes to
|
|
173
173
|
the exact rule. A bot that still holds a rule that the kit gave every bot
|
|
@@ -577,6 +577,13 @@ What it looks for:
|
|
|
577
577
|
`<bot>.<session>` every fleet's sessions had before 0.10.0, which a session of
|
|
578
578
|
another fleet can write to. Its next start through the kit gives it one of its
|
|
579
579
|
own, so the finding names the `obk restart … --session` that does it;
|
|
580
|
+
- **a session whose mail waits and Orca has not told it**: mail older than an
|
|
581
|
+
hour that is unread and that Orca's notice never reached, while the session's
|
|
582
|
+
harness is idle now. Orca can stop telling a session that sat idle through an
|
|
583
|
+
Orca restart or update, so the finding names the `obk restart … --session`
|
|
584
|
+
that opens a new tab, which Orca tells. It counts the letters and gives the
|
|
585
|
+
time of the oldest; it never shows their subject or text. A busy session is
|
|
586
|
+
not named: Orca tells it when its turn ends;
|
|
580
587
|
- **a process still running in a retired session's work dir**: one from before
|
|
581
588
|
retire stopped them, or one it could not stop, with its pid and command;
|
|
582
589
|
- **leftovers no book owns**: an Orca project inside your bots folder with no
|
package/SETUP.md
CHANGED
|
@@ -54,7 +54,10 @@ folder.
|
|
|
54
54
|
|
|
55
55
|
On the Orca CLI path: `/usr/local/bin/orca` is a root-only symlink on some
|
|
56
56
|
machines and fails for an ordinary user, so use the full path above. If Orca
|
|
57
|
-
lives somewhere else here, find it and set `OBK_ORCA` to it.
|
|
57
|
+
lives somewhere else here, find it and set `OBK_ORCA` to it. If that is another
|
|
58
|
+
build of Orca with its own data folder, also set `OBK_ORCA_DATA` to the full
|
|
59
|
+
path of that folder, for example `$HOME/Library/Application Support/orca-lean`.
|
|
60
|
+
The kit reads Orca's launch arguments from the `profiles` folder there.
|
|
58
61
|
|
|
59
62
|
If Node is older than 24.21.0, say so and stop. The kit's book of sessions is a
|
|
60
63
|
SQLite write transaction through Node's own `node:sqlite`, and 24.21.0 is the
|
|
@@ -141,8 +144,8 @@ Read everything it prints. Three parts of it matter to them:
|
|
|
141
144
|
beside the bots folder, and its commits. The kit writes this set for every
|
|
142
145
|
bot, with nobody asked. Say in a sentence what it allows, and that
|
|
143
146
|
`obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
|
|
144
|
-
`obk bot change`, `obk session add`, `obk session change
|
|
145
|
-
not in it and keep their yes;
|
|
147
|
+
`obk bot change`, `obk session add`, `obk session change`, `obk init` and
|
|
148
|
+
`obk run` are not in it and keep their yes;
|
|
146
149
|
- **anything it says about Orca's own default launch arguments.** Orca adds
|
|
147
150
|
these to every agent it launches, relaunches and resumes. When they carry a
|
|
148
151
|
permission bypass, every session runs in that mode whatever approval level the
|
package/package.json
CHANGED
|
@@ -214,7 +214,7 @@ does not have yet to its `bot.yaml` and its settings, or for Codex its
|
|
|
214
214
|
`.codex/rules/obk.rules`, and say which rules they wrote. The set leaves out
|
|
215
215
|
the commands that keep the user's yes: `obk permission`, `obk retire`, `obk
|
|
216
216
|
pause`, `obk restart`, `obk bot create`, `obk bot change`, `obk session add`,
|
|
217
|
-
`obk session change` and `obk
|
|
217
|
+
`obk session change`, `obk init` and `obk run`. A bot that still holds a rule that the
|
|
218
218
|
kit gave every bot before and that is no longer in the set (`obk restart`,
|
|
219
219
|
`obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
|
|
220
220
|
`obk session mail`, `obk session nudge`, or Orca's `orca orchestration check
|
|
@@ -245,7 +245,11 @@ out; the kit refuses it, and it refuses a rule of its own default set too. Keep
|
|
|
245
245
|
each rule narrow: a program and what it does. The kit refuses a rule that lets
|
|
246
246
|
the bot run any command, a program with any arguments, or a shell or
|
|
247
247
|
interpreter with any arguments; a user who wants one for a bot adds it to that
|
|
248
|
-
bot's settings themselves.
|
|
248
|
+
bot's settings themselves. For a test run with a time limit, propose `obk run`
|
|
249
|
+
with the real command after `--`, such as
|
|
250
|
+
`Bash(<kit> run --timeout 300 -- go test:*)`: the kit refuses a rule for
|
|
251
|
+
`perl -e 'alarm …'`, since perl runs whatever its arguments say. Propose no
|
|
252
|
+
rule for what the charter does not grant.
|
|
249
253
|
|
|
250
254
|
A bot that should learn across its sessions can use its harness's own memory,
|
|
251
255
|
turned on for that bot alone, whatever the user's own setting is for everything
|
package/src/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
|
34
34
|
import { addSource, fetchSources } from './sources.js';
|
|
35
35
|
import { answerSession, answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks, trustSessionHooks } from './temp.js';
|
|
36
36
|
import { readUsage } from './usage.js';
|
|
37
|
+
import { parseRun, runBounded } from './run.js';
|
|
37
38
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
38
39
|
|
|
39
40
|
const USAGE = `obk — Orca Bot Kit.
|
|
@@ -326,14 +327,28 @@ Usage:
|
|
|
326
327
|
For the kit's own launch line, not for typing: run
|
|
327
328
|
in the session's own tab, it gives the session its
|
|
328
329
|
mailbox, or binds the one it has, to that tab.
|
|
330
|
+
obk run --timeout <seconds> -- <command> [<arg> ...]
|
|
331
|
+
Run one command with a hard time limit, with no
|
|
332
|
+
shell, and exit with its exit code. When the time
|
|
333
|
+
runs out, it stops the command and everything it
|
|
334
|
+
started (SIGTERM, then SIGKILL after 3 s), says so
|
|
335
|
+
on stderr, and exits 124. A command it cannot find
|
|
336
|
+
exits 127. A rule can allow it for one command,
|
|
337
|
+
such as Bash(obk run --timeout 300 -- go test:*).
|
|
338
|
+
It needs no --bots, and takes no --json.
|
|
329
339
|
obk --version Print the kit's version.
|
|
330
340
|
obk --help Print this text.
|
|
331
341
|
|
|
332
342
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
333
343
|
that close a tab or take something away say so above. Every bot is allowed
|
|
334
|
-
the kit's commands by default, except obk permission, retire, pause and
|
|
335
|
-
which keep the user's yes.
|
|
344
|
+
the kit's commands by default, except obk permission, retire, pause, init and
|
|
345
|
+
run, which keep the user's yes.
|
|
336
346
|
Add --json to any of them for the same answer as JSON.
|
|
347
|
+
|
|
348
|
+
For another build of Orca, OBK_ORCA names its CLI, and OBK_ORCA_DATA the full
|
|
349
|
+
path of the folder it keeps its data in, such as
|
|
350
|
+
$HOME/Library/Application Support/orca-lean.
|
|
351
|
+
The kit reads Orca's launch arguments from the profiles folder there.
|
|
337
352
|
`;
|
|
338
353
|
|
|
339
354
|
|
|
@@ -381,6 +396,10 @@ function version() {
|
|
|
381
396
|
}
|
|
382
397
|
|
|
383
398
|
async function run(argv) {
|
|
399
|
+
// What follows `obk run --timeout <n> --` is the command's own, flags and
|
|
400
|
+
// all, so it is read by `run` alone, before the kit reads its flags (#532).
|
|
401
|
+
if (argv[0] === 'run') return runBounded(parseRun(argv.slice(1)));
|
|
402
|
+
|
|
384
403
|
const { values, positionals, tokens } = parseArgs({
|
|
385
404
|
args: argv,
|
|
386
405
|
allowPositionals: true,
|
package/src/commands.js
CHANGED
|
@@ -46,6 +46,7 @@ export const COMMANDS = {
|
|
|
46
46
|
'temp retire': ['bots', 'name'],
|
|
47
47
|
'temp trust-hooks': ['bots', 'name'],
|
|
48
48
|
'temp answer': ['bots', 'name'],
|
|
49
|
+
run: ['timeout'],
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -68,12 +69,14 @@ export const DEFAULT_COMMANDS = [
|
|
|
68
69
|
* The commands kept out of the default permission set, which keep the user's
|
|
69
70
|
* yes (ADR 0051): the ones that change permissions, close a long-lived
|
|
70
71
|
* session, make the bots folder, or make or change a bot or its sessions,
|
|
71
|
-
* which the owner leaves to Bot Father (#548, #615)
|
|
72
|
+
* which the owner leaves to Bot Father (#548, #615); and `run`, which runs
|
|
73
|
+
* whatever its arguments say, so a rule for it names its command (#532).
|
|
72
74
|
*/
|
|
73
75
|
export const KEPT_BACK = [
|
|
74
76
|
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
75
77
|
'bot change', 'session add', 'session change',
|
|
76
78
|
'permission allow', 'permission disallow', 'permission approval',
|
|
79
|
+
'run',
|
|
77
80
|
];
|
|
78
81
|
|
|
79
82
|
/**
|
package/src/health.js
CHANGED
|
@@ -26,7 +26,7 @@ import { botDir, botNames, botsDir, readBot, tempRoles, unknownKeys } from './bo
|
|
|
26
26
|
import { transcriptsIn } from './conversations.js';
|
|
27
27
|
import { hookTrouble } from './hooks.js';
|
|
28
28
|
import { APPROVALS, bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, promptFile, sessionTrouble, set, SHELL_ENV, shellWord, workDirOf } from './launch.js';
|
|
29
|
-
import { frontOfTab, orcaDefaultArgs, projects, runMissing, tabs, wordsOfProcess } from './orca.js';
|
|
29
|
+
import { frontOfTab, mailIn, orcaDefaultArgs, projects, runMissing, tabs, tabToTypeInto, wordsOfProcess } from './orca.js';
|
|
30
30
|
import { permissionsTrouble } from './permissions.js';
|
|
31
31
|
import { processesIn } from './processes.js';
|
|
32
32
|
import { TAB_ENV } from './record.js';
|
|
@@ -459,6 +459,12 @@ function inOrca(bots, home, bot, setups, sessions) {
|
|
|
459
459
|
found.push(finding('session', bookFile(home), `${bot.name}'s session ${name} has the mailbox ${entry.mailbox} in the book, and Orca has no Run of that id: it was made by another Orca, as on another machine, so mail sent to it is refused. The session's next start makes it a new one: ${fix}`, bot.name));
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
// Mail Orca has not told a running session of (#630). A session that is
|
|
463
|
+
// paused or whose tab is gone reads no mail by design, or is named above.
|
|
464
|
+
if (typeof entry?.mailbox === 'string' && there.has(entry.tab) && !closed.has(name)) {
|
|
465
|
+
found.push(...unannounced(bots, home, bot, name, entry));
|
|
466
|
+
}
|
|
467
|
+
|
|
462
468
|
const unclaimed = (Array.isArray(entry?.unclaimed) ? entry.unclaimed : []).filter((id) => !helpers.has(id));
|
|
463
469
|
if (unclaimed.length > 0) {
|
|
464
470
|
found.push(finding('leftover', bookFile(home), `The book notes conversations under ${bot.name}'s session ${name} that no session of this bot claims: ${unclaimed.join(', ')}. They ran in this bot's folder, and the kit will not say whose they were. To give one back, write it into ${bookFile(home)} under ${name} as session: <id> and run obk up again.`, bot.name));
|
|
@@ -481,6 +487,70 @@ function inOrca(bots, home, bot, setups, sessions) {
|
|
|
481
487
|
return found;
|
|
482
488
|
}
|
|
483
489
|
|
|
490
|
+
/** How old an unread letter Orca has not rung for is before it counts as waiting (#630). */
|
|
491
|
+
const WAITING_MS = 60 * 60 * 1000;
|
|
492
|
+
|
|
493
|
+
/** How long the look at a session's tab waits for its harness to be idle. */
|
|
494
|
+
const IDLE_LOOK_MS = 2000;
|
|
495
|
+
|
|
496
|
+
/** How many of a mailbox's newest letters health reads: one read, whatever the mailbox holds. */
|
|
497
|
+
const MAIL_READ = 100;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The session `name`'s mail that Orca has not told it of, while its harness is
|
|
501
|
+
* idle now: one finding, or none (#630). Orca rings only a tab whose harness it
|
|
502
|
+
* has seen go idle since Orca last started, so a session that sat idle through
|
|
503
|
+
* an Orca restart or update is never told (tech notes, section 1). A busy one
|
|
504
|
+
* is told when its turn ends, so it is not named. Only flags and times are
|
|
505
|
+
* read: a letter's subject and text are the session's own. A mailbox or a tab
|
|
506
|
+
* Orca will not answer about leaves it unsaid.
|
|
507
|
+
*
|
|
508
|
+
* One read of the newest letters (review of PR #632). Orca hands mail to its
|
|
509
|
+
* reader oldest first, so a full read whose oldest letter is read is taken to
|
|
510
|
+
* hold every unread one. That misses an older letter left unread below a letter
|
|
511
|
+
* `orca orchestration reply` marked read out of order; the kit never runs it.
|
|
512
|
+
* A full read whose oldest letter is unread may have more below it, and the
|
|
513
|
+
* finding says so rather than count what it did not read.
|
|
514
|
+
*/
|
|
515
|
+
function unannounced(bots, home, bot, name, entry) {
|
|
516
|
+
let mail;
|
|
517
|
+
try {
|
|
518
|
+
mail = mailIn(entry.mailbox, MAIL_READ);
|
|
519
|
+
} catch {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
const before = Date.now() - WAITING_MS;
|
|
523
|
+
const waiting = mail
|
|
524
|
+
.filter((one) => one.read === 0 && one.delivered_at == null && Date.parse(one.created_at) < before)
|
|
525
|
+
.sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
|
|
526
|
+
const last = mail.at(-1);
|
|
527
|
+
const more = mail.length >= MAIL_READ && last.read === 0;
|
|
528
|
+
// Unread and never rung for, with more of the mailbox below it.
|
|
529
|
+
const below = more && last.delivered_at == null;
|
|
530
|
+
if (waiting.length === 0 && !below) return [];
|
|
531
|
+
|
|
532
|
+
// The gate the kit's own keys go through tells an idle harness from a busy
|
|
533
|
+
// one, a question on its screen or a shell; only its answer is kept.
|
|
534
|
+
let look;
|
|
535
|
+
try {
|
|
536
|
+
look = tabToTypeInto(home, entry.tab, IDLE_LOOK_MS);
|
|
537
|
+
} catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
if (look.handle === undefined || look.idle !== true) return [];
|
|
541
|
+
|
|
542
|
+
const count = `${more ? 'at least ' : ''}${waiting.length}`;
|
|
543
|
+
const letters = waiting.length === 1 && !more ? '1 letter' : `${count} letters`;
|
|
544
|
+
const read = `Health reads the newest ${MAIL_READ} letters of a mailbox, and the oldest of those is unread too, so older letters may wait as well.`;
|
|
545
|
+
const what = waiting.length === 0
|
|
546
|
+
? `has mail in its mailbox ${entry.mailbox} that Orca has not told it of: the oldest of the newest ${MAIL_READ} letters, from ${last.created_at}, is unread, and Orca has not rung for it. Older letters may wait below it, which health does not read.`
|
|
547
|
+
: more
|
|
548
|
+
? `has ${letters} in its mailbox ${entry.mailbox} that it has not read and that Orca has not told it of, each more than an hour old; the oldest of them that health read is from ${waiting[0].created_at}. ${read}`
|
|
549
|
+
: `has ${letters} in its mailbox ${entry.mailbox} that it has not read and that Orca has not told it of, the oldest from ${waiting[0].created_at}, more than an hour ago.`;
|
|
550
|
+
const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${bot.name} --session ${name}`;
|
|
551
|
+
return [finding('mail', entry.mailbox, `${bot.name}'s session ${name} ${what} Its harness is idle now, and Orca tells an idle session of its mail within minutes, unless the session sat idle through an Orca restart or update: then Orca does not tell it until it takes a turn. ${restart} opens a new tab for it, which Orca tells. A restart drops what lives only in the old tab, such as Claude Code schedules, so the session makes them again.`, bot.name)];
|
|
552
|
+
}
|
|
553
|
+
|
|
484
554
|
/**
|
|
485
555
|
* The conversations a harness has on record in this bot's folder that the book
|
|
486
556
|
* does not name anywhere. The hook is how an id reaches the book, and when it
|
package/src/hooks.js
CHANGED
|
@@ -101,7 +101,7 @@ export function installHook(home, harness, { bots, bot }) {
|
|
|
101
101
|
const started = withKitHook(settings.hooks, file, mine);
|
|
102
102
|
// The Claude Code mail hook and the Codex nudge hook went with the kit's
|
|
103
103
|
// typed line (#555), so an entry of either is taken out.
|
|
104
|
-
const wanted = { ...settings, hooks: toolHooksOf(harness, bots, bot).reduce(withToolHook, withoutHooks(started,
|
|
104
|
+
const wanted = { ...settings, hooks: toolHooksOf(harness, bots, bot).reduce(withToolHook, withoutHooks(started, REMOVED_HOOKS)) };
|
|
105
105
|
|
|
106
106
|
// Compared as documents, not as text: how the user laid their file out is
|
|
107
107
|
// theirs, and a run that changes nothing writes nothing.
|
|
@@ -112,6 +112,41 @@ export function installHook(home, harness, { bots, bot }) {
|
|
|
112
112
|
return HOOK_FILE[harness];
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Take the kit's hooks of commands it no longer has out of every hooks file the
|
|
117
|
+
* bot at `home` has, whatever its sessions run on now and whether or not it is
|
|
118
|
+
* paused (#613). `installHook` does this only for a harness a session runs on,
|
|
119
|
+
* so a file the kit wrote for a harness the bot has since left kept them.
|
|
120
|
+
* Returns the files it wrote, relative to the bot home.
|
|
121
|
+
*
|
|
122
|
+
* It adds nothing and changes no other entry. A file it cannot read, a hooks
|
|
123
|
+
* entry that is not a mapping of events, or a link out of the bot folder is
|
|
124
|
+
* left as it is: the kit cannot tell its entries there, and health and
|
|
125
|
+
* `installHook` speak for a harness that runs.
|
|
126
|
+
*/
|
|
127
|
+
export function dropRemovedHooks(home) {
|
|
128
|
+
return Object.values(HOOK_FILE).flatMap((relative) => {
|
|
129
|
+
const file = path.join(home, relative);
|
|
130
|
+
if (!existsSync(file) || outside(home, file) !== undefined) return [];
|
|
131
|
+
let settings;
|
|
132
|
+
try {
|
|
133
|
+
settings = readSettings(file);
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const { hooks } = settings;
|
|
138
|
+
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) return [];
|
|
139
|
+
const kept = withoutHooks(hooks, REMOVED_HOOKS);
|
|
140
|
+
if (isDeepStrictEqual(hooks, kept)) return [];
|
|
141
|
+
try {
|
|
142
|
+
writeFileSync(file, `${JSON.stringify({ ...settings, hooks: kept }, null, 2)}\n`);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(`${file} cannot be written (${error.code ?? error.message}), and it holds the kit's hook of a command the kit no longer has, which the kit takes out. Let the kit write it, then run the command again.`);
|
|
145
|
+
}
|
|
146
|
+
return [relative];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
115
150
|
/**
|
|
116
151
|
* Whether the kit's hook is in place for one harness, and what to say when it
|
|
117
152
|
* is not: `{ where, says }`, or nothing when the file already holds it.
|
|
@@ -361,6 +396,7 @@ const KIT_SENT = new RegExp(String.raw`^(${WORD}) session sent --bots ${WORD} --
|
|
|
361
396
|
const KIT_NUDGE = new RegExp(String.raw`^(${WORD}) session nudge --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
362
397
|
const KIT_MAIL = new RegExp(String.raw`^(${WORD}) session mail --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
363
398
|
const KIT_NAME = new RegExp(String.raw`^(${WORD}) session name --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
399
|
+
const REMOVED_HOOKS = [KIT_MAIL, KIT_NUDGE];
|
|
364
400
|
|
|
365
401
|
/**
|
|
366
402
|
* Whether a hook entry is one of the kit's own as this kit writes it: a
|
|
@@ -16,29 +16,28 @@
|
|
|
16
16
|
on run argv
|
|
17
17
|
set appPath to item 1 of argv
|
|
18
18
|
tell application "System Events"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end if
|
|
19
|
+
-- By its path alone: another build of Orca has another name (#551).
|
|
20
|
+
repeat with candidate in (every application process whose POSIX path of application file is appPath)
|
|
21
|
+
tell candidate
|
|
22
|
+
-- Menu 1 is Apple's own.
|
|
23
|
+
repeat with b from 2 to count of menu bar items of menu bar 1
|
|
24
|
+
set m to menu 1 of menu bar item b of menu bar 1
|
|
25
|
+
set labels to name of menu items of m
|
|
26
|
+
repeat with i from 1 to count of labels
|
|
27
|
+
set label to item i of labels
|
|
28
|
+
if label is not missing value then
|
|
29
|
+
if label is "Force Reload" or label starts with ("Force Reload" & tab) or label ends with (tab & "⌘⇧R") then
|
|
30
|
+
-- Asked last, right before the click, so Orca has had no time to
|
|
31
|
+
-- lose the front since.
|
|
32
|
+
if frontmost of candidate is false then return "Orca is not the front app"
|
|
33
|
+
click menu item i of m
|
|
34
|
+
return "reloaded"
|
|
36
35
|
end if
|
|
37
|
-
end
|
|
36
|
+
end if
|
|
38
37
|
end repeat
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
end repeat
|
|
39
|
+
end tell
|
|
40
|
+
return "no Force Reload in Orca's menus"
|
|
42
41
|
end repeat
|
|
43
42
|
end tell
|
|
44
43
|
return "no Orca running from " & appPath
|
package/src/orca.js
CHANGED
|
@@ -29,10 +29,11 @@ export const orcaCli = () => process.env.OBK_ORCA || BUILT_IN;
|
|
|
29
29
|
* Run one Orca command and give back its `result`.
|
|
30
30
|
* Every call asks for `--json`; anything else would be text to guess at.
|
|
31
31
|
* With `timeoutMs`, Orca is given that long to answer, and the call is ended
|
|
32
|
-
* and refused if it has not.
|
|
32
|
+
* and refused if it has not. With `whole`, an answer of any size is taken,
|
|
33
|
+
* where Node would otherwise stop at a megabyte (review of PR #632).
|
|
33
34
|
*/
|
|
34
|
-
export function orca(args, { timeoutMs } = {}) {
|
|
35
|
-
const answer = ask(args, timeoutMs);
|
|
35
|
+
export function orca(args, { timeoutMs, whole = false } = {}) {
|
|
36
|
+
const answer = ask(args, timeoutMs, whole);
|
|
36
37
|
if (answer.ok !== true) {
|
|
37
38
|
// Orca's own words: it knows what went wrong, and the caller is an LLM
|
|
38
39
|
// that can act on them.
|
|
@@ -55,8 +56,12 @@ const refusal = (args, answer) => Object.assign(
|
|
|
55
56
|
);
|
|
56
57
|
|
|
57
58
|
/** One Orca command, and the whole envelope back, refusals included. */
|
|
58
|
-
function ask(args, timeoutMs) {
|
|
59
|
-
const asked = spawnSync(orcaCli(), [...args, '--json'], {
|
|
59
|
+
function ask(args, timeoutMs, whole = false) {
|
|
60
|
+
const asked = spawnSync(orcaCli(), [...args, '--json'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
...(timeoutMs === undefined ? {} : { timeout: timeoutMs }),
|
|
63
|
+
...(whole ? { maxBuffer: Infinity } : {}),
|
|
64
|
+
});
|
|
60
65
|
if (asked.error?.code === 'ETIMEDOUT') {
|
|
61
66
|
throw Object.assign(new Error(`Orca did not answer ${args.join(' ')} within ${timeoutMs / 1000} seconds.`), { code: TIMED_OUT });
|
|
62
67
|
}
|
|
@@ -135,8 +140,16 @@ export function findProject(home) {
|
|
|
135
140
|
return projects().find((setup) => setup.path === home);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Where Orca keeps its own settings: one file per profile, under the user's home.
|
|
145
|
+
* OBK_ORCA_DATA names the data folder of another build of Orca, as OBK_ORCA
|
|
146
|
+
* names its CLI: a build's folder is written only inside its own CLI, so the
|
|
147
|
+
* kit cannot tell it from the app (#551).
|
|
148
|
+
*/
|
|
149
|
+
export const profilesDir = () => path.join(
|
|
150
|
+
process.env.OBK_ORCA_DATA || path.join(homedir(), 'Library', 'Application Support', 'orca'),
|
|
151
|
+
'profiles',
|
|
152
|
+
);
|
|
140
153
|
|
|
141
154
|
/**
|
|
142
155
|
* Orca's own default launch arguments, per profile it keeps:
|
|
@@ -1057,6 +1070,17 @@ export function postMessage({ to, from, subject, body, type = 'status', thread }
|
|
|
1057
1070
|
export const readMailbox = (id, { peek = false, handle } = {}, options) =>
|
|
1058
1071
|
orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])], options);
|
|
1059
1072
|
|
|
1073
|
+
/**
|
|
1074
|
+
* The newest `limit` messages in a mailbox, newest first, with every column
|
|
1075
|
+
* Orca keeps for them: `read` and `delivered_at` among them, which `check`
|
|
1076
|
+
* leaves out. Orca asks no reader for this, so any tab can read any mailbox,
|
|
1077
|
+
* and nothing is marked read or delivered (tech notes, section 1; #630). The
|
|
1078
|
+
* answer carries each letter's whole text, and a local letter's text has no
|
|
1079
|
+
* cap in Orca, so the answer is taken whatever its size.
|
|
1080
|
+
*/
|
|
1081
|
+
export const mailIn = (id, limit) =>
|
|
1082
|
+
orca(['orchestration', 'inbox', '--terminal', `run:${id}`, '--limit', String(limit)], { whole: true }).messages ?? [];
|
|
1083
|
+
|
|
1060
1084
|
/** Say a batch has been read. Orca answers with the batch after it. */
|
|
1061
1085
|
export const ackMailbox = (id, delivery, handle, options) =>
|
|
1062
1086
|
orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery], options);
|
package/src/permissions.js
CHANGED
|
@@ -165,12 +165,14 @@ function commandBroadness(all) {
|
|
|
165
165
|
if (star === -1) return undefined;
|
|
166
166
|
|
|
167
167
|
const program = path.basename(words[0].replace(/:?\*.*$/, ''));
|
|
168
|
+
if (isKit(words[0]) && mayRun(words[1] ?? '')) return runBroadness(words);
|
|
168
169
|
if (runnerOf(program) !== undefined) {
|
|
169
170
|
// Only what runs, fixed by its absolute path, narrows it. A wrapper's
|
|
170
|
-
// program is then judged by itself; an interpreter's script is the end
|
|
171
|
+
// program is then judged by itself; an interpreter's script is the end,
|
|
172
|
+
// unless it is the kit, which is judged as the kit.
|
|
171
173
|
const next = (words[1] ?? '').replace(/:\*$/, '');
|
|
172
174
|
const fixed = next.startsWith('/') && !next.includes('*');
|
|
173
|
-
if (fixed && WRAPPERS.has(program)) return commandBroadness(words.slice(1));
|
|
175
|
+
if (fixed && (WRAPPERS.has(program) || isKit(next))) return commandBroadness(words.slice(1));
|
|
174
176
|
if (fixed && runnerOf(path.basename(next)) === undefined) return undefined;
|
|
175
177
|
return `${program} runs whatever its arguments say, so ${ANY}`;
|
|
176
178
|
}
|
|
@@ -183,6 +185,37 @@ function commandBroadness(all) {
|
|
|
183
185
|
return undefined;
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
/** Whether a word is the kit's CLI: `obk` by its name, or the path the kit runs as. */
|
|
189
|
+
const isKit = (word) => path.basename(word) === 'obk' || word === ownCli();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Whether a rule's word can match `run`: Claude's `*` matches any characters,
|
|
193
|
+
* spaces too, so `ru*` matches `run --timeout 1 -- sh`, while `:*` matches
|
|
194
|
+
* only at the end of a word.
|
|
195
|
+
*/
|
|
196
|
+
function mayRun(word) {
|
|
197
|
+
const star = word.indexOf('*');
|
|
198
|
+
if (star === -1) return word === 'run';
|
|
199
|
+
const before = word.slice(0, star);
|
|
200
|
+
return before.endsWith(':') ? before === 'run:' : 'run'.startsWith(before);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Why a rule for `obk run`, as shell words, is broad, or undefined (#532). It
|
|
205
|
+
* runs whatever its arguments say, so only a fixed time limit, `--`, and a
|
|
206
|
+
* command after it that is narrow as a rule of its own narrow it.
|
|
207
|
+
*/
|
|
208
|
+
function runBroadness(words) {
|
|
209
|
+
const [, , ...rest] = words;
|
|
210
|
+
let command;
|
|
211
|
+
if (rest[0] === '--timeout' && /^\d+$/.test(rest[1] ?? '') && rest[2] === '--') command = rest.slice(3);
|
|
212
|
+
if (/^--timeout=\d+$/.test(rest[0] ?? '') && rest[1] === '--') command = rest.slice(2);
|
|
213
|
+
if (words[1] !== 'run' || command === undefined || command.length === 0) {
|
|
214
|
+
return `obk run runs whatever its arguments say, so ${ANY}. A rule for it names the command after --, such as Bash(obk run --timeout 300 -- go test:*)`;
|
|
215
|
+
}
|
|
216
|
+
return commandBroadness(command);
|
|
217
|
+
}
|
|
218
|
+
|
|
186
219
|
/**
|
|
187
220
|
* Refuse the rules `permission allow` was given when any is broad, before anything is
|
|
188
221
|
* written, naming the file the user can add it to themselves.
|
|
@@ -220,6 +253,15 @@ export function allowOf(home, bot) {
|
|
|
220
253
|
export const runsOnCodex = (bot) => bot.harness === 'codex'
|
|
221
254
|
|| bot.sessions.some((session) => harnessOf(session, bot.harness) === 'codex');
|
|
222
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Whether the kit keeps the bot's Codex rules file: while the bot runs on
|
|
258
|
+
* Codex, and after it has left Codex for as long as the file it wrote is
|
|
259
|
+
* there in the bot folder, so the file never holds a rule that `allow` no
|
|
260
|
+
* longer does (#613). A link out of the bot folder is not the kit's file.
|
|
261
|
+
*/
|
|
262
|
+
const keepsCodex = (bot, home) => runsOnCodex(bot)
|
|
263
|
+
|| (existsSync(path.join(home, CODEX_FILE)) && leadsOutside(home, path.join(home, CODEX_FILE)) === undefined);
|
|
264
|
+
|
|
223
265
|
/**
|
|
224
266
|
* The default rules this bot is not allowed yet, in the default order. A bot
|
|
225
267
|
* only on Codex is not given the Read rule: its sandbox reads every file
|
|
@@ -293,7 +335,7 @@ export function refuseNoCodexForm(bot, rules) {
|
|
|
293
335
|
* not write is an entry with its `trouble` rather than a throw.
|
|
294
336
|
*/
|
|
295
337
|
export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
|
|
296
|
-
const writers = [[runsOnClaude, FILE], [
|
|
338
|
+
const writers = [[runsOnClaude, FILE], [keepsCodex, CODEX_FILE]].filter(([runs]) => runs(bot, home));
|
|
297
339
|
let defaults = [];
|
|
298
340
|
// Read before anything is written, so a file's trouble still names them.
|
|
299
341
|
let held = [];
|
|
@@ -324,8 +366,8 @@ export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
|
|
|
324
366
|
*/
|
|
325
367
|
export function allowIn(bots, home, bot) {
|
|
326
368
|
refuseUnwritable(path.join(home, 'bot.yaml'));
|
|
327
|
-
const plans = [[runsOnClaude, planClaude], [
|
|
328
|
-
.filter(([runs]) => runs(bot)).map(([, plan]) => plan(bots, home, bot));
|
|
369
|
+
const plans = [[runsOnClaude, planClaude], [keepsCodex, planCodex]]
|
|
370
|
+
.filter(([runs]) => runs(bot, home)).map(([, plan]) => plan(bots, home, bot));
|
|
329
371
|
for (const plan of plans) if (plan.writes) refuseUnwritable(plan.answer.file);
|
|
330
372
|
return () => plans.map((plan) => plan.write());
|
|
331
373
|
}
|
|
@@ -404,7 +446,8 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
404
446
|
}
|
|
405
447
|
const codex = path.join(home, CODEX_FILE);
|
|
406
448
|
let before = [];
|
|
407
|
-
|
|
449
|
+
const codexKept = keepsCodex(bot, home);
|
|
450
|
+
if (codexKept) {
|
|
408
451
|
refuseOutside(home, codex);
|
|
409
452
|
if (existsSync(codex)) {
|
|
410
453
|
before = ruleLines(readFileSync(codex, 'utf8'));
|
|
@@ -422,7 +465,7 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
422
465
|
}
|
|
423
466
|
entries.push({ bot: bot.name, file, written: [], removed });
|
|
424
467
|
}
|
|
425
|
-
if (
|
|
468
|
+
if (codexKept) {
|
|
426
469
|
const entry = planCodex(bots, home, after).write();
|
|
427
470
|
// A line goes only when no rule still allowed gives it.
|
|
428
471
|
const kept = [...codexOf(home, after).lines.keys()];
|
package/src/rules.js
CHANGED
|
@@ -21,6 +21,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import { parse } from 'yaml';
|
|
22
22
|
|
|
23
23
|
import { botDir, botNames, displayName, readBot, refuseOutside } from './bot.js';
|
|
24
|
+
import { dropRemovedHooks } from './hooks.js';
|
|
24
25
|
import { ownCli, shellWord } from './launch.js';
|
|
25
26
|
|
|
26
27
|
/** The kit's own units, inside the installed package. */
|
|
@@ -109,13 +110,23 @@ export function buildRules(bots, { bot: onlyBot } = {}) {
|
|
|
109
110
|
|
|
110
111
|
return (onlyBot === undefined ? names : [onlyBot]).map((name) => {
|
|
111
112
|
const home = botDir(bots, name);
|
|
113
|
+
let built;
|
|
112
114
|
try {
|
|
113
|
-
|
|
115
|
+
built = buildAgents(bots, home, readBot(home, name));
|
|
114
116
|
} catch (error) {
|
|
115
117
|
// A `bot.yaml` the kit cannot read at all. Same answer as a list it
|
|
116
118
|
// cannot follow: this bot is not built, and nothing else changes.
|
|
117
|
-
|
|
119
|
+
built = { bot: name, file: path.join(home, AGENTS), state: 'failed', trouble: error.message };
|
|
118
120
|
}
|
|
121
|
+
// Every bot, a paused one too, so this is the command that reaches each
|
|
122
|
+
// hooks file the kit wrote, whatever the bot's sessions run on now (#613).
|
|
123
|
+
// A file it cannot write is this bot's trouble, as above.
|
|
124
|
+
try {
|
|
125
|
+
dropRemovedHooks(home);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { ...built, trouble: [built.trouble, error.message].filter((one) => one !== undefined).join(' ') };
|
|
128
|
+
}
|
|
129
|
+
return built;
|
|
119
130
|
});
|
|
120
131
|
}
|
|
121
132
|
|
package/src/run.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// `obk run --timeout <seconds> -- <command…>`: one command run with a hard time
|
|
2
|
+
// limit (#532). macOS has no `timeout`, and the fleet's form for a bounded run,
|
|
3
|
+
// `perl -e 'alarm …; exec @ARGV' -- <command>`, cannot be allowed by a narrow
|
|
4
|
+
// rule, since perl runs whatever its arguments say. A rule for this command
|
|
5
|
+
// names the real command after `--`, and the kit judges that command as a rule
|
|
6
|
+
// of its own (permissions.js). It runs whatever its arguments say too, so it is
|
|
7
|
+
// never in the default set (ADR 0050).
|
|
8
|
+
//
|
|
9
|
+
// The command runs with no shell, in a process group of its own, so the time
|
|
10
|
+
// limit stops what it started as well. Its exit code comes back as it is; one
|
|
11
|
+
// ended by a signal the kit did not send comes back as 128 and the signal's
|
|
12
|
+
// number, as a shell gives it.
|
|
13
|
+
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { constants } from 'node:os';
|
|
16
|
+
|
|
17
|
+
/** The exit code of a run the time limit stopped, as GNU timeout gives it. */
|
|
18
|
+
const TIMED_OUT = 124;
|
|
19
|
+
|
|
20
|
+
/** The exit codes a shell gives a command it cannot find, and one it cannot run. */
|
|
21
|
+
const NOT_FOUND = 127;
|
|
22
|
+
const CANNOT_RUN = 126;
|
|
23
|
+
|
|
24
|
+
/** How long the group has after SIGTERM before SIGKILL, as `retire` gives a session's processes (#537). */
|
|
25
|
+
const GRACE_MS = 3000;
|
|
26
|
+
|
|
27
|
+
/** How often the kit looks whether anything of the group still runs. */
|
|
28
|
+
const LOOK_MS = 100;
|
|
29
|
+
|
|
30
|
+
/** The longest time limit a timer holds, in seconds: about 24 days. */
|
|
31
|
+
const LONGEST = Math.floor((2 ** 31 - 1) / 1000);
|
|
32
|
+
|
|
33
|
+
/** The signals that end the kit and that it passes on to the command's group, so it is not left running. */
|
|
34
|
+
const PASSED_ON = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
35
|
+
|
|
36
|
+
const USE = 'obk run takes --timeout <seconds>, then --, then the command: obk run --timeout 300 -- go test ./...';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The time limit and the command from `obk run`'s arguments, or a refusal.
|
|
40
|
+
* Only `--timeout` goes before `--`; everything after it is the command's.
|
|
41
|
+
*/
|
|
42
|
+
export function parseRun(args) {
|
|
43
|
+
const end = args.indexOf('--');
|
|
44
|
+
const options = end === -1 ? args : args.slice(0, end);
|
|
45
|
+
let value;
|
|
46
|
+
for (let at = 0; at < options.length; at += 1) {
|
|
47
|
+
const option = options[at];
|
|
48
|
+
if (option === '--timeout' && at + 1 < options.length && value === undefined) {
|
|
49
|
+
value = options[at + 1];
|
|
50
|
+
at += 1;
|
|
51
|
+
} else if (option.startsWith('--timeout=') && value === undefined) {
|
|
52
|
+
value = option.slice('--timeout='.length);
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`run does not take ${option}. ${USE}. Nothing was run.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (value === undefined) throw new Error(`run needs --timeout <seconds>: how long the command may run. ${USE}. Nothing was run.`);
|
|
58
|
+
if (!/^\d+$/.test(value) || Number(value) === 0 || Number(value) > LONGEST) {
|
|
59
|
+
throw new Error(`run's --timeout is a whole number of seconds from 1 to ${LONGEST}, and got: ${value === '' ? 'nothing' : value}. Nothing was run.`);
|
|
60
|
+
}
|
|
61
|
+
const command = end === -1 ? [] : args.slice(end + 1);
|
|
62
|
+
if (command.length === 0) throw new Error(`run needs the command after --. ${USE}. Nothing was run.`);
|
|
63
|
+
return { seconds: Number(value), command };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run the command with its time limit, its input and output the caller's, and
|
|
68
|
+
* give back the exit code `obk run` exits with. The run ends when nothing of
|
|
69
|
+
* the command's group runs any more: a process the command started and left
|
|
70
|
+
* running is held to the same limit, and to a signal the kit passes on.
|
|
71
|
+
*/
|
|
72
|
+
export function runBounded({ seconds, command }) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
// `detached` makes the command the leader of a group of its own, so its
|
|
75
|
+
// pid, taken here at spawn, is the group's id.
|
|
76
|
+
const child = spawn(command[0], command.slice(1), { stdio: 'inherit', detached: true });
|
|
77
|
+
// The leader's exit code, once it has exited.
|
|
78
|
+
let exitCode;
|
|
79
|
+
let stopping = false;
|
|
80
|
+
let result;
|
|
81
|
+
let killAt;
|
|
82
|
+
let deadline;
|
|
83
|
+
let look;
|
|
84
|
+
const signal = (pid, name) => {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, name);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
// Each process of the group gets a signal once. The leader gets it by its
|
|
93
|
+
// own pid as well only when it may have left the group: when nothing is
|
|
94
|
+
// left in the group, or for SIGKILL, which no handler sees. And only
|
|
95
|
+
// while it has not exited, since its pid is free again after that.
|
|
96
|
+
const group = (name) => {
|
|
97
|
+
const sent = signal(-child.pid, name);
|
|
98
|
+
if (exitCode === undefined && name !== 0 && (!sent || name === 'SIGKILL')) signal(child.pid, name);
|
|
99
|
+
return sent;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const finish = (code) => {
|
|
103
|
+
clearTimeout(deadline);
|
|
104
|
+
clearTimeout(look);
|
|
105
|
+
for (const name of PASSED_ON) process.off(name, passOn);
|
|
106
|
+
resolve(code);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Once the leader has exited or the group is being stopped: look until
|
|
110
|
+
// nothing of the group runs, and send SIGKILL when the grace is over.
|
|
111
|
+
const watch = () => {
|
|
112
|
+
if (exitCode !== undefined && !group(0)) return finish(result ?? exitCode);
|
|
113
|
+
if (killAt !== undefined && Date.now() >= killAt) {
|
|
114
|
+
group('SIGKILL');
|
|
115
|
+
killAt = undefined;
|
|
116
|
+
}
|
|
117
|
+
look = setTimeout(watch, LOOK_MS);
|
|
118
|
+
return undefined;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Stop the group: `name` first, then SIGKILL to what still runs after the
|
|
122
|
+
// grace. obk run exits with `code`, or with the leader's own when it is
|
|
123
|
+
// undefined.
|
|
124
|
+
const stop = (name, code) => {
|
|
125
|
+
if (stopping) return;
|
|
126
|
+
stopping = true;
|
|
127
|
+
result = code;
|
|
128
|
+
clearTimeout(deadline);
|
|
129
|
+
group(name);
|
|
130
|
+
killAt = Date.now() + GRACE_MS;
|
|
131
|
+
clearTimeout(look);
|
|
132
|
+
watch();
|
|
133
|
+
};
|
|
134
|
+
const passOn = (name) => stop(name, undefined);
|
|
135
|
+
|
|
136
|
+
child.on('error', (error) => {
|
|
137
|
+
const missing = error.code === 'ENOENT';
|
|
138
|
+
process.stderr.write(`obk: run could not start ${command[0]}: ${missing ? 'there is no such command' : error.code ?? error.message}.\n`);
|
|
139
|
+
finish(missing ? NOT_FOUND : CANNOT_RUN);
|
|
140
|
+
});
|
|
141
|
+
if (child.pid === undefined) return;
|
|
142
|
+
for (const name of PASSED_ON) process.on(name, passOn);
|
|
143
|
+
|
|
144
|
+
child.on('exit', (code, name) => {
|
|
145
|
+
exitCode = code ?? 128 + constants.signals[name];
|
|
146
|
+
clearTimeout(look);
|
|
147
|
+
watch();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
deadline = setTimeout(() => {
|
|
151
|
+
process.stderr.write(`obk: run stopped ${command.join(' ')}: it ran past its time limit of ${seconds} seconds, and its whole process group was stopped.\n`);
|
|
152
|
+
stop('SIGTERM', TIMED_OUT);
|
|
153
|
+
}, seconds * 1000);
|
|
154
|
+
});
|
|
155
|
+
}
|
package/src/up.js
CHANGED
|
@@ -12,7 +12,7 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
12
12
|
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, rememberSession, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
13
13
|
import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
14
14
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
15
|
-
import { installHook } from './hooks.js';
|
|
15
|
+
import { dropRemovedHooks, installHook } from './hooks.js';
|
|
16
16
|
import { writePermissions } from './permissions.js';
|
|
17
17
|
import { piTrouble } from './pi.js';
|
|
18
18
|
import { addressOf, HARNESSES, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
@@ -148,8 +148,10 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
148
148
|
// so one written later would miss the session it was written for (ADR 0022),
|
|
149
149
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
150
150
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
151
|
-
// sessions gets none.
|
|
151
|
+
// sessions gets none. The hooks of commands the kit no longer has go from
|
|
152
|
+
// every hooks file the bot has, of a harness it has left too (#613).
|
|
152
153
|
for (const { bot, home } of running) {
|
|
154
|
+
dropRemovedHooks(home);
|
|
153
155
|
for (const harness of new Set(awake(sessionsOf(bot, onlySession)).map((session) => harnessOf(session, bot.harness)))) {
|
|
154
156
|
installHook(home, harness, { bots, bot: bot.name });
|
|
155
157
|
}
|