@assuredloop/orca-bot-kit 0.26.0 → 0.27.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 -30
- package/package.json +1 -1
- package/rules/mail.md +5 -5
- package/skills/obk-bot-building/SKILL.md +5 -2
- package/src/book.js +55 -16
- package/src/bot.js +2 -2
- package/src/cli.js +53 -110
- package/src/commands.js +17 -9
- package/src/conversations.js +0 -37
- package/src/health.js +63 -6
- package/src/hooks.js +35 -63
- package/src/launch.js +21 -4
- package/src/message.js +42 -334
- package/src/name.js +1 -1
- package/src/orca.js +19 -35
- package/src/permissions.js +22 -6
- package/src/processes.js +392 -0
- package/src/retire.js +43 -40
- package/src/skills.js +1 -1
- package/src/temp.js +1 -1
- package/src/up.js +13 -2
- package/src/unread.js +0 -160
package/src/cli.js
CHANGED
|
@@ -19,14 +19,14 @@ import { checkHealth, orcaSettingFindings } from './health.js';
|
|
|
19
19
|
import { initBots } from './init.js';
|
|
20
20
|
import { nameSession } from './name.js';
|
|
21
21
|
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
22
|
-
import { checkMail,
|
|
22
|
+
import { checkMail, lookUp, noMailboxYet, sendMessage, sessionInTab } from './message.js';
|
|
23
23
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
24
24
|
import { allowIn, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
25
25
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
26
26
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
27
27
|
import { restartSessions } from './restart.js';
|
|
28
28
|
import { clearSession, compactSession } from './clear.js';
|
|
29
|
-
import { retireBot, retireSession
|
|
29
|
+
import { retireBot, retireSession } from './retire.js';
|
|
30
30
|
import { sentWarning } from './sent.js';
|
|
31
31
|
import { readRoster } from './roster.js';
|
|
32
32
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
@@ -109,7 +109,9 @@ Usage:
|
|
|
109
109
|
End a session: close its tab and take it off the
|
|
110
110
|
bot, keeping its conversations in the book, and
|
|
111
111
|
first do the same for the temporary sessions it
|
|
112
|
-
made, and theirs.
|
|
112
|
+
made, and theirs. It stops what the session left
|
|
113
|
+
running in its work dir, and names each process.
|
|
114
|
+
Or end a bot: close its tabs,
|
|
113
115
|
remove its Orca project and move its folder to
|
|
114
116
|
retired/. It will not retire a bot whose Orca
|
|
115
117
|
project holds a tab your book does not name, and
|
|
@@ -207,9 +209,12 @@ Usage:
|
|
|
207
209
|
the Orca mailbox. Nothing is sent.
|
|
208
210
|
obk message send --bots <path> --to <bot>[/<session>] [--from <bot>/<session>]
|
|
209
211
|
--subject <text> [--text <text> | --text-file <path>]
|
|
210
|
-
[--thread <id>]
|
|
211
|
-
Put a message in that session's Orca mailbox
|
|
212
|
-
|
|
212
|
+
[--thread <id>] [--interrupt]
|
|
213
|
+
Put a message in that session's Orca mailbox. Orca
|
|
214
|
+
tells it to look once it is idle. --interrupt is
|
|
215
|
+
for urgent mail: when the session is busy, the kit
|
|
216
|
+
presses Escape once in its tab first, to end its
|
|
217
|
+
turn, so it looks now. A message too long to travel
|
|
213
218
|
as itself is written to a file beside your bots
|
|
214
219
|
folder and named in the message. A pair the
|
|
215
220
|
harness's own messaging reaches is not carried:
|
|
@@ -305,17 +310,6 @@ Usage:
|
|
|
305
310
|
Claude session's native message, it warns the
|
|
306
311
|
session when the address was none of this bots
|
|
307
312
|
folder's own. It never stops a message.
|
|
308
|
-
obk session nudge --bots <path> --bot <bot>
|
|
309
|
-
For the kit's own hook, not for typing: after a
|
|
310
|
-
Codex session's shell command, it decides the mail
|
|
311
|
-
nudges a send there left because it could not
|
|
312
|
-
tell from inside Codex's sandbox, and types each
|
|
313
|
-
one or says why not.
|
|
314
|
-
obk session mail --bots <path> --bot <bot>
|
|
315
|
-
For the kit's own hook, not for typing: at the end
|
|
316
|
-
of a Claude session's turn, it tells the session
|
|
317
|
-
once about each fleet mail the kit sent it that is
|
|
318
|
-
still unread. It types nothing into the tab.
|
|
319
313
|
obk session name --bots <path> --bot <bot>
|
|
320
314
|
For the kit's own hook, not for typing: after a
|
|
321
315
|
Codex session's turn, it names the session's thread
|
|
@@ -409,6 +403,7 @@ async function run(argv) {
|
|
|
409
403
|
'text-file': { type: 'string' },
|
|
410
404
|
thread: { type: 'string' },
|
|
411
405
|
peek: { type: 'boolean' },
|
|
406
|
+
interrupt: { type: 'boolean' },
|
|
412
407
|
charter: { type: 'string' },
|
|
413
408
|
allow: { type: 'string', multiple: true },
|
|
414
409
|
disallow: { type: 'string', multiple: true },
|
|
@@ -476,8 +471,6 @@ async function run(argv) {
|
|
|
476
471
|
// The send hook is quiet whatever it is given, a bots folder that is not
|
|
477
472
|
// there included, so it goes before anything that can complain (ADR 0032).
|
|
478
473
|
if (command === SENT) return sent(path.resolve(values.bots));
|
|
479
|
-
if (command === NUDGE) return nudgeLeft();
|
|
480
|
-
if (command === MAIL) return mailHook(path.resolve(values.bots), values.bot);
|
|
481
474
|
if (command === NAME) return nameThread(path.resolve(values.bots), values.bot);
|
|
482
475
|
|
|
483
476
|
// One fleet, one identity, whatever spelling of its path was given (#164).
|
|
@@ -548,8 +541,6 @@ const ANSWER_IT = [
|
|
|
548
541
|
/** The commands a harness runs rather than a person: the kit's hooks. */
|
|
549
542
|
const RECORD = 'session record';
|
|
550
543
|
const SENT = 'session sent';
|
|
551
|
-
const NUDGE = 'session nudge';
|
|
552
|
-
const MAIL = 'session mail';
|
|
553
544
|
const NAME = 'session name';
|
|
554
545
|
|
|
555
546
|
/**
|
|
@@ -569,45 +560,6 @@ function sent(bots) {
|
|
|
569
560
|
return 0;
|
|
570
561
|
}
|
|
571
562
|
|
|
572
|
-
/**
|
|
573
|
-
* What the kit's Codex hook does after a shell command: decide the nudges a
|
|
574
|
-
* send in this tab left for it, outside Codex's sandbox, and say to the session
|
|
575
|
-
* what became of each, as Codex's `additionalContext` (#350, ADR 0034). Silent
|
|
576
|
-
* when nothing was left, never a decision, and never anything but exit 0.
|
|
577
|
-
*/
|
|
578
|
-
async function nudgeLeft() {
|
|
579
|
-
try {
|
|
580
|
-
const said = JSON.parse(readFileSync(0, 'utf8'));
|
|
581
|
-
if (said?.hook_event_name !== 'PostToolUse') return 0;
|
|
582
|
-
const decided = await decideLeftNudges(process.env[TAB_ENV]);
|
|
583
|
-
if (decided.length > 0) {
|
|
584
|
-
const words = decided.map((one) => `Your mail "${one.subject}" to ${one.to}: ${one.nudged ? "the kit's hook nudged its tab, from outside Codex's sandbox:" : "the kit's hook looked at its tab from outside Codex's sandbox, and"} ${nudgeLine(one, one.to).trim()}`);
|
|
585
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: words.join('\n') } })}\n`);
|
|
586
|
-
}
|
|
587
|
-
} catch {
|
|
588
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
589
|
-
}
|
|
590
|
-
return 0;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* What the kit's Claude Code Stop hook does at a turn end (#509, ADR 0035): tell
|
|
595
|
-
* the session once about fleet mail still unread, as the Stop event's
|
|
596
|
-
* additional context, which the session goes on with. Not a block: Claude Code
|
|
597
|
-
* 2.1.296 draws a block's reason in red as "Stop hook error", and its additional
|
|
598
|
-
* context as "Stop hook feedback". Nothing typed into the tab; never anything
|
|
599
|
-
* but exit 0; silent whenever it cannot be sure.
|
|
600
|
-
*/
|
|
601
|
-
function mailHook(bots, bot) {
|
|
602
|
-
try {
|
|
603
|
-
const reason = stillUnread(sameFleet(bots), bot, JSON.parse(readFileSync(0, 'utf8')), process.env[TAB_ENV], process.env[TERMINAL_ENV]);
|
|
604
|
-
if (reason !== undefined) process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: reason } })}\n`);
|
|
605
|
-
} catch {
|
|
606
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
607
|
-
}
|
|
608
|
-
return 0;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
563
|
/**
|
|
612
564
|
* What the kit's Codex Stop hook does at a turn end: name the session's thread
|
|
613
565
|
* after its bot and session, once it is idle (#480). Silent, never a decision,
|
|
@@ -737,7 +689,7 @@ const commands = {
|
|
|
737
689
|
...withLines(retired.retiredWith, retired.session),
|
|
738
690
|
...closedLines(retired.closed),
|
|
739
691
|
`retired ${retired.bot} ${retired.session}: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
740
|
-
...
|
|
692
|
+
...processLines(retired.processes),
|
|
741
693
|
...leftLines(retired.promptsLeft),
|
|
742
694
|
],
|
|
743
695
|
};
|
|
@@ -749,6 +701,7 @@ const commands = {
|
|
|
749
701
|
answer: { bots, ...retired },
|
|
750
702
|
lines: [
|
|
751
703
|
...closedLines(retired.closed),
|
|
704
|
+
...processLines(retired.processes),
|
|
752
705
|
`${'trouble'.padEnd(9)} Orca project ${retired.project}`,
|
|
753
706
|
` ${retired.trouble}`,
|
|
754
707
|
`Retire it again: ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${retired.bot}`,
|
|
@@ -767,7 +720,7 @@ const commands = {
|
|
|
767
720
|
retired.windowReloaded ? "reloaded Orca's window, so its sidebar no longer shows the project" : RELOAD_LINE,
|
|
768
721
|
]),
|
|
769
722
|
`retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
|
|
770
|
-
...(retired.
|
|
723
|
+
...processLines(retired.processes),
|
|
771
724
|
...leftLines(retired.promptsLeft),
|
|
772
725
|
],
|
|
773
726
|
};
|
|
@@ -824,7 +777,7 @@ const commands = {
|
|
|
824
777
|
...withLines(retired.retiredWith, retired.session),
|
|
825
778
|
...closedLines(retired.closed),
|
|
826
779
|
`retired ${retired.bot} ${retired.session}, a temporary session of ${retired.maker}'s: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
827
|
-
...
|
|
780
|
+
...processLines(retired.processes),
|
|
828
781
|
...leftLines(retired.promptsLeft),
|
|
829
782
|
],
|
|
830
783
|
};
|
|
@@ -909,7 +862,7 @@ const commands = {
|
|
|
909
862
|
throw new Error(`bot change does not change a bot's harness: its sessions' conversations belong to the harness they ran on. To move to ${values.harness}, give it a session on ${values.harness} with obk session add, or retire the bot with obk retire and create a new one.`);
|
|
910
863
|
}
|
|
911
864
|
// Permission changes have commands of their own, which keep the user's yes
|
|
912
|
-
// (ADR
|
|
865
|
+
// (ADR 0041): the old spelling refuses, and changes nothing.
|
|
913
866
|
const moved = [['allow', 'permission allow'], ['disallow', 'permission disallow']].filter(([flag]) => values[flag] !== undefined);
|
|
914
867
|
if (moved.length > 0) {
|
|
915
868
|
throw new Error(`bot change no longer takes ${moved.map(([flag]) => `--${flag}`).join(' or ')}: permission rules change through ${moved.map(([, now]) => `${shellWord(ownCli())} ${now} --bots ${shellWord(bots)} --bot ${values.bot} --rule <rule>`).join(' and ')}, after the user's yes. Nothing was changed.`);
|
|
@@ -1116,7 +1069,7 @@ const commands = {
|
|
|
1116
1069
|
...(values.effort === undefined ? {} : { effort: values.effort }),
|
|
1117
1070
|
...(values['extra-arg'] === undefined ? {} : { extra_args: values['extra-arg'] }),
|
|
1118
1071
|
};
|
|
1119
|
-
// A run's launch may not widen its approval (ADR
|
|
1072
|
+
// A run's launch may not widen its approval (ADR 0041).
|
|
1120
1073
|
if (values['run-on'] === 'codex') refuseApprovalArgs('codex', values['extra-arg']);
|
|
1121
1074
|
const groom = grooming(bots, { at: values.at, ask, run });
|
|
1122
1075
|
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
@@ -1189,6 +1142,7 @@ const commands = {
|
|
|
1189
1142
|
text: values.text,
|
|
1190
1143
|
textFile: values['text-file'],
|
|
1191
1144
|
thread: values.thread,
|
|
1145
|
+
interrupt: values.interrupt === true,
|
|
1192
1146
|
});
|
|
1193
1147
|
const where = `${answer.to.bot}/${answer.to.session}`;
|
|
1194
1148
|
|
|
@@ -1207,7 +1161,7 @@ const commands = {
|
|
|
1207
1161
|
...(answer.file === undefined
|
|
1208
1162
|
? []
|
|
1209
1163
|
: [` it was too long to travel as itself, so it went as a file: ${answer.file}`]),
|
|
1210
|
-
...
|
|
1164
|
+
...sentLines(answer, where),
|
|
1211
1165
|
],
|
|
1212
1166
|
};
|
|
1213
1167
|
},
|
|
@@ -1348,46 +1302,23 @@ function toLines(answer, bots, where) {
|
|
|
1348
1302
|
}
|
|
1349
1303
|
|
|
1350
1304
|
/**
|
|
1351
|
-
* What
|
|
1352
|
-
*
|
|
1353
|
-
* about the message.
|
|
1305
|
+
* What the send says after `sent`: the road it took, and what the interrupt
|
|
1306
|
+
* came to when one was asked for (#555).
|
|
1354
1307
|
*/
|
|
1355
|
-
function
|
|
1356
|
-
const
|
|
1357
|
-
if (answer.
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
const why = {
|
|
1364
|
-
'no-turn': `no turn started in its tab in ${WATCH_SECONDS} s, so the kit's line was typed`,
|
|
1365
|
-
busy: "it is busy with a turn, and Codex takes a line into the turn it is having, so the kit's line was typed",
|
|
1366
|
-
'other-turn': "a turn of other work started in its tab, not Orca's notice, so the kit's line was typed",
|
|
1367
|
-
}[answer.because];
|
|
1368
|
-
const head = why === undefined ? ' ' : ` ${why}: `;
|
|
1369
|
-
if (answer.nudged && answer.nudgeWatched === false) {
|
|
1370
|
-
return `${head}its tab was typed into, and Orca could not say whether it was taken: ${answer.nudgeUnseen} The message waits in its mailbox for its next check either way.`;
|
|
1371
|
-
}
|
|
1372
|
-
if (answer.nudged && answer.nudgeUnseen !== undefined) {
|
|
1373
|
-
return `${head}its tab was typed into, but Orca did not see the line start a turn: it may be queued behind the work in hand, or lost. The message waits in its mailbox for its next check either way.`;
|
|
1374
|
-
}
|
|
1375
|
-
if (answer.nudged) return `${head}its tab was told to look; it will read it when it is done with what it is doing.`;
|
|
1376
|
-
if (answer.blocked !== undefined) {
|
|
1377
|
-
return ` its tab has something waiting to be answered (${answer.blocked}), so nothing was typed into it. Settle that, and the mail is there.`;
|
|
1378
|
-
}
|
|
1379
|
-
if (answer.nudgeLeft) {
|
|
1380
|
-
return ` it is queued, and its tab could not be told to look from here: ${answer.nudgeTrouble}. So the nudge is left for this session's hook, which looks at the tab from outside Codex's sandbox when this command is done, and says what it did.`;
|
|
1308
|
+
function sentLines(answer, where) {
|
|
1309
|
+
const road = " it is in its Orca mailbox, and Orca's own notice tells it to look once it is idle.";
|
|
1310
|
+
if (answer.interrupted === true) {
|
|
1311
|
+
return [
|
|
1312
|
+
`${road.replace('once it is idle', 'now')}`,
|
|
1313
|
+
` it was busy, so the kit pressed Escape once in its tab to end its turn.${answer.to.harness === 'codex' ? ' Codex keeps a command it was running as a background terminal after an Escape.' : ''}`,
|
|
1314
|
+
];
|
|
1381
1315
|
}
|
|
1382
|
-
if (answer.
|
|
1383
|
-
return ` it is
|
|
1316
|
+
if (answer.interrupted === false) {
|
|
1317
|
+
return [road, answer.idle ? ` it is idle, so it was not interrupted.` : ` it was not interrupted: ${answer.interruptTrouble}.`];
|
|
1384
1318
|
}
|
|
1385
|
-
return
|
|
1319
|
+
return [road];
|
|
1386
1320
|
}
|
|
1387
1321
|
|
|
1388
|
-
/** The watch before the kit's line, as the send says it (#509, ADR 0035). */
|
|
1389
|
-
const WATCH_SECONDS = WATCH_MS / 1000;
|
|
1390
|
-
|
|
1391
1322
|
/**
|
|
1392
1323
|
* What there is to say about the work dir this command was given: one finding
|
|
1393
1324
|
* when it leads out of the bot home, through `..` or a link or as a path
|
|
@@ -1407,7 +1338,7 @@ function workDirFound(values, bot, home, session) {
|
|
|
1407
1338
|
|
|
1408
1339
|
/**
|
|
1409
1340
|
* Refuse what would change a session's approval through `session add` or
|
|
1410
|
-
* `session change` (ADR
|
|
1341
|
+
* `session change` (ADR 0041): `--approval`, and an extra argument that sets
|
|
1411
1342
|
* it on the session's harness. Only `permission approval` changes it.
|
|
1412
1343
|
*/
|
|
1413
1344
|
function refuseApproval(bots, values, command) {
|
|
@@ -1459,12 +1390,19 @@ const closedLines = (closed) => closed.map((tab) => `closed ${tab.bot} ${tab
|
|
|
1459
1390
|
const withLines = (retiredWith, session) => retiredWith.flatMap((gone) => [
|
|
1460
1391
|
...closedLines(gone.closed),
|
|
1461
1392
|
`retired ${gone.bot} ${gone.session}, a temporary session of ${gone.maker}'s, along with ${session}: off ${path.join('bots', gone.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
1462
|
-
...
|
|
1393
|
+
...processLines(gone.processes),
|
|
1463
1394
|
...leftLines(gone.promptsLeft),
|
|
1464
1395
|
]);
|
|
1465
1396
|
|
|
1466
|
-
/**
|
|
1467
|
-
|
|
1397
|
+
/** What a retire stopped of what its sessions left running, and what it named and left (#537). */
|
|
1398
|
+
function processLines(processes) {
|
|
1399
|
+
if (processes === undefined) return [];
|
|
1400
|
+
if (processes.unreadable !== undefined) return [`processes the kit cannot tell which processes were left running in the work dir, so it stopped none: ${processes.unreadable}`];
|
|
1401
|
+
return [
|
|
1402
|
+
...processes.stopped.map((one) => `stopped pid ${one.pid} of ${one.session}, with ${one.signal}: ${one.command}${one.cwd === null ? '' : ` in ${one.cwd}`}`),
|
|
1403
|
+
...processes.left.map((one) => `left pid ${one.pid} in ${one.cwd ?? 'a folder lsof did not name'}: ${one.command}. Not stopped: ${one.why}.`),
|
|
1404
|
+
];
|
|
1405
|
+
}
|
|
1468
1406
|
|
|
1469
1407
|
/** What a retire could not remove, and how to: nothing reads these files now (#393). */
|
|
1470
1408
|
const leftLines = (left = []) => left.flatMap(({ file, reason }) => [
|
|
@@ -1787,17 +1725,22 @@ function charterRulesLines(bots, bot, rules) {
|
|
|
1787
1725
|
|
|
1788
1726
|
/**
|
|
1789
1727
|
* What became of each bot's permission rules: each of the kit's default rules
|
|
1790
|
-
* the run added to the bot, word for word (ADR 0036),
|
|
1791
|
-
*
|
|
1728
|
+
* the run added to the bot, word for word (ADR 0036), each rule of a command no
|
|
1729
|
+
* longer in the set that the bot still holds, with the command that takes it
|
|
1730
|
+
* back (ADR 0041), and the file the allowed ones were written into. Nothing for
|
|
1731
|
+
* a bot with nothing added, held or written.
|
|
1792
1732
|
*/
|
|
1793
1733
|
function permissionsLines(permissions, bots) {
|
|
1794
1734
|
// A bot on both harnesses has an entry for each file, with the same
|
|
1795
|
-
// defaults: they are listed once, with the first.
|
|
1735
|
+
// defaults and held rules: they are listed once, with the first.
|
|
1796
1736
|
const listed = new Set();
|
|
1737
|
+
const disallow = (bot, rule) => `${shellWord(ownCli())} permission disallow --bots ${shellWord(bots)} --bot ${shellWord(bot)} --rule ${shellWord(rule)}`;
|
|
1797
1738
|
return permissions.flatMap((entry) => [
|
|
1798
1739
|
...(entry.trouble === undefined ? [] : [`${'refused'.padEnd(9)} ${entry.trouble}`]),
|
|
1799
|
-
...(
|
|
1800
|
-
|
|
1740
|
+
...(listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1741
|
+
...(entry.defaults ?? []).map((rule) => `${'allowed'.padEnd(9)} ${entry.bot} ${rule}`),
|
|
1742
|
+
...(entry.held ?? []).map((rule) => `${'holds'.padEnd(9)} ${entry.bot} ${rule}, which is no longer one of the kit's defaults. To take it back: ${disallow(entry.bot, rule)}`),
|
|
1743
|
+
]),
|
|
1801
1744
|
...(entry.written.length === 0
|
|
1802
1745
|
? []
|
|
1803
1746
|
: [`${'wrote'.padEnd(9)} ${path.relative(bots, entry.file)} ${entry.written.length} permission rule${entry.written.length === 1 ? '' : 's'} from its allow list`]),
|
package/src/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The kit's commands, each with the flags it cannot do without, which `obk`
|
|
2
2
|
// reads to parse a command line; and the two lists every command is on one of:
|
|
3
3
|
// those every bot is allowed by default (#527, ADR 0036), and those kept back
|
|
4
|
-
// for the user's yes (ADR
|
|
4
|
+
// for the user's yes (ADR 0041). A new command is on neither until someone
|
|
5
5
|
// puts it on one, and a test holds every command to that choice.
|
|
6
6
|
|
|
7
7
|
/** The commands, and the flags each one cannot do without. */
|
|
@@ -37,8 +37,6 @@ export const COMMANDS = {
|
|
|
37
37
|
'message check': ['bots'],
|
|
38
38
|
'session record': ['bots', 'bot'],
|
|
39
39
|
'session sent': ['bots', 'bot'],
|
|
40
|
-
'session nudge': ['bots', 'bot'],
|
|
41
|
-
'session mail': ['bots', 'bot'],
|
|
42
40
|
'session name': ['bots', 'bot'],
|
|
43
41
|
'session mailbox': ['bots', 'bot', 'session'],
|
|
44
42
|
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
@@ -56,19 +54,29 @@ export const COMMANDS = {
|
|
|
56
54
|
* runs whatever its arguments say never is.
|
|
57
55
|
*/
|
|
58
56
|
export const DEFAULT_COMMANDS = [
|
|
59
|
-
'up', '
|
|
60
|
-
'bot
|
|
57
|
+
'up', 'unpause', 'health', 'groom', 'roster', 'usage',
|
|
58
|
+
'bot change', 'rules build',
|
|
61
59
|
'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
|
|
62
60
|
'session add', 'session change', 'session clear', 'session compact',
|
|
63
61
|
'message to', 'message send', 'message check',
|
|
64
|
-
'session record', 'session sent', 'session
|
|
62
|
+
'session record', 'session sent', 'session name', 'session mailbox',
|
|
65
63
|
'session trust-hooks', 'session answer',
|
|
66
64
|
'temp make', 'temp roles', 'temp retire', 'temp trust-hooks', 'temp answer',
|
|
67
65
|
];
|
|
68
66
|
|
|
69
67
|
/**
|
|
70
68
|
* The commands kept out of the default permission set, which keep the user's
|
|
71
|
-
* yes (ADR
|
|
72
|
-
* session, or make the
|
|
69
|
+
* yes (ADR 0041): the ones that change permissions, close a long-lived
|
|
70
|
+
* session, make the bots folder, or make a bot, which the owner leaves to Bot
|
|
71
|
+
* Father (#548).
|
|
73
72
|
*/
|
|
74
|
-
export const KEPT_BACK = [
|
|
73
|
+
export const KEPT_BACK = [
|
|
74
|
+
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
75
|
+
'permission allow', 'permission disallow', 'permission approval',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The commands kit 0.26.0 gave every bot by default and that are kept back now
|
|
80
|
+
* (#548). A bot may still hold their rules, which the kit names and leaves.
|
|
81
|
+
*/
|
|
82
|
+
export const NO_LONGER_DEFAULT = ['restart', 'bot create'];
|
package/src/conversations.js
CHANGED
|
@@ -159,43 +159,6 @@ export function heldAsUserTurn(harness, file, text) {
|
|
|
159
159
|
return held;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
/**
|
|
163
|
-
* Where the harness keeps the conversation `id` of this bot home, and how long
|
|
164
|
-
* that record is now: `{ harness, file, size }`, so a later look reads only
|
|
165
|
-
* what was written after this moment. Undefined when there is no id, no
|
|
166
|
-
* record, or it cannot be read (#509).
|
|
167
|
-
*/
|
|
168
|
-
export function recordMark(harness, home, id) {
|
|
169
|
-
if (typeof id !== 'string' || id === '') return undefined;
|
|
170
|
-
try {
|
|
171
|
-
const file = harness === 'codex' ? codexRollout(id) : claudeTranscript(home, id);
|
|
172
|
-
if (file === undefined) return undefined;
|
|
173
|
-
const found = statSync(file);
|
|
174
|
-
return found.isFile() ? { harness, file, size: found.size } : undefined;
|
|
175
|
-
} catch {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Whether a turn of the user's written after `mark` holds `text` anywhere in
|
|
182
|
-
* it (#509). A record that cannot be read says no: not seeing a thing is not
|
|
183
|
-
* seeing it.
|
|
184
|
-
*/
|
|
185
|
-
export function userTurnSince(mark, text) {
|
|
186
|
-
if (mark === undefined || mark === null) return false;
|
|
187
|
-
let held = false;
|
|
188
|
-
try {
|
|
189
|
-
eachLine(mark.file, (line) => {
|
|
190
|
-
held = line !== undefined && userTexts(mark.harness, line).some((said) => said.includes(text));
|
|
191
|
-
return held;
|
|
192
|
-
}, { from: mark.size });
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
return held;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
162
|
/** The texts of one record line, when it is a turn of the user's. */
|
|
200
163
|
function userTexts(harness, line) {
|
|
201
164
|
let parsed;
|
package/src/health.js
CHANGED
|
@@ -18,22 +18,23 @@
|
|
|
18
18
|
// is, the one thing to go and look at, and a sentence naming it — so that
|
|
19
19
|
// somebody who cannot read code knows what was found and where.
|
|
20
20
|
|
|
21
|
-
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
|
|
24
24
|
import { bookFile, readBook, sessionIdsIn, tabIdsIn } from './book.js';
|
|
25
25
|
import { botDir, botNames, botsDir, readBot, tempRoles, unknownKeys } from './bot.js';
|
|
26
26
|
import { transcriptsIn } from './conversations.js';
|
|
27
27
|
import { hookTrouble } from './hooks.js';
|
|
28
|
-
import { APPROVALS, bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
28
|
+
import { APPROVALS, bypassFlags, harnessOf, HARNESSES, isAddressOf, ownCli, sessionTrouble, SHELL_ENV, shellWord, workDirOf } from './launch.js';
|
|
29
29
|
import { frontOfTab, orcaDefaultArgs, projects, runMissing, tabs, wordsOfProcess } from './orca.js';
|
|
30
30
|
import { permissionsTrouble } from './permissions.js';
|
|
31
|
+
import { processesIn } from './processes.js';
|
|
31
32
|
import { TAB_ENV } from './record.js';
|
|
32
33
|
import { agentsTrouble, rulesStamp } from './rules.js';
|
|
33
34
|
import { settingsInUse } from './settings.js';
|
|
34
35
|
import { skillsTrouble } from './skills.js';
|
|
35
36
|
import { readSources, sourcesDir } from './sources.js';
|
|
36
|
-
import { BOT_FATHER, botsNamed, promptPath } from './up.js';
|
|
37
|
+
import { BOT_FATHER, botsNamed, oldPromptPath, promptPath } from './up.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Everything wrong with the setup at `bots`, in one list: what Orca's own
|
|
@@ -141,6 +142,9 @@ function leftBeside(bots) {
|
|
|
141
142
|
try {
|
|
142
143
|
for (const session of readBot(botDir(bots, name), name).sessions) {
|
|
143
144
|
owned.add(promptPath(bots, name, session.name));
|
|
145
|
+
// Where a live session's prompt was before #534: nothing reads it once
|
|
146
|
+
// the session starts again, and its retire removes it.
|
|
147
|
+
owned.add(oldPromptPath(bots, name, session.name));
|
|
144
148
|
}
|
|
145
149
|
} catch {
|
|
146
150
|
known = false;
|
|
@@ -148,8 +152,7 @@ function leftBeside(bots) {
|
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (known) {
|
|
151
|
-
for (const
|
|
152
|
-
const at = path.join(`${bots}.prompts`, file);
|
|
155
|
+
for (const at of promptFilesIn(`${bots}.prompts`)) {
|
|
153
156
|
if (owned.has(at)) continue;
|
|
154
157
|
found.push(finding('leftover', at, `${at} is a start prompt the kit wrote beside your bots folder, and no session of any bot answers to it now. Nothing reads it.`));
|
|
155
158
|
}
|
|
@@ -196,9 +199,48 @@ function aboutBot(bots, name, setups, sessions) {
|
|
|
196
199
|
...permissionsTrouble(home, bot).map(said('config')),
|
|
197
200
|
...skillsTrouble(bots, home, bot).map(said('skill')),
|
|
198
201
|
...inOrca(bots, home, bot, setups, sessions),
|
|
202
|
+
...leftRunning(home, bot),
|
|
199
203
|
];
|
|
200
204
|
}
|
|
201
205
|
|
|
206
|
+
/**
|
|
207
|
+
* What still runs in the work dir of a retired session (#537): a retire stops
|
|
208
|
+
* it, so this is one from before that, or one a retire could not stop. The
|
|
209
|
+
* book keeps a retired session's work dir; a temporary session retired before
|
|
210
|
+
* it did worked in `work/<name>`, where `obk temp make` puts it. A work dir a
|
|
211
|
+
* session in bot.yaml still uses is that session's, and not looked at, by its
|
|
212
|
+
* real path, so a link to it is not taken for a retired one (review of PR #543).
|
|
213
|
+
*/
|
|
214
|
+
function leftRunning(home, bot) {
|
|
215
|
+
let book;
|
|
216
|
+
try {
|
|
217
|
+
book = readBook(home);
|
|
218
|
+
} catch {
|
|
219
|
+
// A book that cannot be read is said where the book is read.
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
const real = (dir) => (dir === undefined ? undefined : realpathOf(dir));
|
|
223
|
+
const live = new Set(bot.sessions.map((session) => real(workDirOf(session, home))));
|
|
224
|
+
const dirs = [];
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const entry of Array.isArray(book.retired) ? book.retired : []) {
|
|
227
|
+
const named = typeof entry?.work_dir === 'string' && entry.work_dir !== '' ? entry.work_dir : undefined;
|
|
228
|
+
const dir = workDirOf({ work_dir: named ?? (entry?.temporary === undefined ? undefined : `work/${entry.name}`) }, home);
|
|
229
|
+
// One that is not there holds nothing.
|
|
230
|
+
const at = real(dir);
|
|
231
|
+
if (at === undefined || live.has(at) || seen.has(at)) continue;
|
|
232
|
+
seen.add(at);
|
|
233
|
+
dirs.push({ session: entry.name, dir });
|
|
234
|
+
}
|
|
235
|
+
if (dirs.length === 0) return [];
|
|
236
|
+
|
|
237
|
+
const read = processesIn(dirs);
|
|
238
|
+
if (read.unreadable !== undefined) {
|
|
239
|
+
return [finding('process', bookFile(home), `The kit cannot tell whether anything still runs in the work dirs of ${bot.name}'s retired sessions: ${read.unreadable}.`, bot.name)];
|
|
240
|
+
}
|
|
241
|
+
return read.running.map((one) => finding('process', one.dir, `pid ${one.pid} still runs in the work dir of ${bot.name}/${one.session}, which was retired: ${one.command}. Nothing of the kit's stops it now. If it is not wanted, stop it yourself: kill ${one.pid}`, bot.name));
|
|
242
|
+
}
|
|
243
|
+
|
|
202
244
|
/** A session the kit would refuse to start, in the words `up` refuses it with. */
|
|
203
245
|
function sessionSettings(home, bot) {
|
|
204
246
|
const file = path.join(home, 'bot.yaml');
|
|
@@ -213,7 +255,7 @@ function sessionSettings(home, bot) {
|
|
|
213
255
|
/** What is wrong with the roles the bot's temporary sessions are made in (#465). */
|
|
214
256
|
function rolesTrouble(home, bot) {
|
|
215
257
|
const where = path.join(home, 'bot.yaml');
|
|
216
|
-
// The widest approval they may be made at, the user's yes (ADR
|
|
258
|
+
// The widest approval they may be made at, the user's yes (ADR 0041).
|
|
217
259
|
const level = bot.temp_approval;
|
|
218
260
|
const approval = level === undefined || APPROVALS.includes(level) ? [] : [{
|
|
219
261
|
where,
|
|
@@ -461,6 +503,21 @@ function recordIn(home, bot) {
|
|
|
461
503
|
}
|
|
462
504
|
|
|
463
505
|
/** What a directory holds, and nothing at all when there is no directory. */
|
|
506
|
+
/**
|
|
507
|
+
* The files in the kit's folder of start prompts: in each bot's own folder
|
|
508
|
+
* there (#534), and flat in the folder itself, where they were before.
|
|
509
|
+
*/
|
|
510
|
+
function promptFilesIn(dir) {
|
|
511
|
+
return namesIn(dir).flatMap((name) => {
|
|
512
|
+
const at = path.join(dir, name);
|
|
513
|
+
try {
|
|
514
|
+
return statSync(at).isDirectory() ? namesIn(at).map((file) => path.join(at, file)) : [at];
|
|
515
|
+
} catch {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
464
521
|
function namesIn(dir) {
|
|
465
522
|
try {
|
|
466
523
|
return readdirSync(dir);
|