@assuredloop/orca-bot-kit 0.27.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 +55 -15
- package/SETUP.md +30 -14
- package/package.json +1 -1
- package/rules/mail.md +3 -3
- package/skills/obk-bot-building/SKILL.md +29 -16
- package/skills/obk-grooming/SKILL.md +4 -3
- package/src/book.js +91 -10
- package/src/bot.js +246 -118
- package/src/clear.js +7 -3
- package/src/cli.js +116 -36
- package/src/commands.js +23 -11
- package/src/conversations.js +73 -3
- package/src/groom.js +27 -9
- package/src/health.js +89 -8
- package/src/hooks.js +44 -2
- package/src/init.js +36 -29
- package/src/launch.js +202 -18
- package/src/message.js +42 -2
- package/src/orca-reload.applescript +21 -22
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +47 -14
- package/src/permissions.js +80 -23
- package/src/pi.js +55 -0
- package/src/rules.js +18 -4
- package/src/run.js +155 -0
- package/src/skills.js +50 -34
- package/src/sources.js +73 -4
- package/src/temp.js +65 -20
- package/src/up.js +72 -15
package/src/up.js
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
// conversation and Orca's resume record with it. Run it twice and the second
|
|
5
5
|
// run does nothing.
|
|
6
6
|
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
10
11
|
|
|
11
|
-
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
12
|
+
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, rememberSession, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
12
13
|
import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
13
14
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
|
-
import { installHook } from './hooks.js';
|
|
15
|
+
import { dropRemovedHooks, installHook } from './hooks.js';
|
|
15
16
|
import { writePermissions } from './permissions.js';
|
|
16
|
-
import {
|
|
17
|
+
import { piTrouble } from './pi.js';
|
|
18
|
+
import { addressOf, HARNESSES, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
17
19
|
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
18
20
|
import { TAB_ENV } from './record.js';
|
|
19
21
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
@@ -110,6 +112,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
110
112
|
return { home, bot: readBot(home, name) };
|
|
111
113
|
});
|
|
112
114
|
for (const { bot, home } of chosen) refuseWhatCannotStart(bot, home, onlySession);
|
|
115
|
+
refuseWhenPiIsNotReady(chosen, onlySession);
|
|
113
116
|
|
|
114
117
|
// Every bot's AGENTS.md is built before Orca is asked for anything, because a
|
|
115
118
|
// session reads that file as it starts: a build landing after the tab was
|
|
@@ -145,8 +148,10 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
145
148
|
// so one written later would miss the session it was written for (ADR 0022),
|
|
146
149
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
147
150
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
148
|
-
// 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).
|
|
149
153
|
for (const { bot, home } of running) {
|
|
154
|
+
dropRemovedHooks(home);
|
|
150
155
|
for (const harness of new Set(awake(sessionsOf(bot, onlySession)).map((session) => harnessOf(session, bot.harness)))) {
|
|
151
156
|
installHook(home, harness, { bots, bot: bot.name });
|
|
152
157
|
}
|
|
@@ -170,7 +175,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
170
175
|
export function botsNamed(bots, onlyBot) {
|
|
171
176
|
const names = botNames(bots);
|
|
172
177
|
if (names.length === 0) {
|
|
173
|
-
throw new Error(`there are no bots in ${bots} yet: make one with obk bot create --bots <path> --name <name> --harness claude|codex.`);
|
|
178
|
+
throw new Error(`there are no bots in ${bots} yet: make one with obk bot create --bots <path> --name <name> --harness claude|codex|pi.`);
|
|
174
179
|
}
|
|
175
180
|
if (onlyBot !== undefined && !names.includes(onlyBot)) {
|
|
176
181
|
throw new Error(`there is no bot called ${onlyBot} in ${bots}. The bots there are: ${names.join(', ')}.`);
|
|
@@ -185,6 +190,23 @@ function refuseWhatCannotStart(bot, home, onlySession) {
|
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Refuse the run before Orca is asked for anything when it would start a Pi
|
|
195
|
+
* session and Pi cannot start one: Pi does not run, can use no model, or
|
|
196
|
+
* cannot use the one a session names (#578). The user signs in to Pi on their
|
|
197
|
+
* own. A run with no Pi session does not ask Pi anything.
|
|
198
|
+
*/
|
|
199
|
+
function refuseWhenPiIsNotReady(chosen, onlySession) {
|
|
200
|
+
const onPi = chosen.flatMap(({ bot }) => awake(sessionsOf(bot, onlySession))
|
|
201
|
+
.filter((session) => harnessOf(session, bot.harness) === 'pi')
|
|
202
|
+
.map((session) => ({ bot, session })));
|
|
203
|
+
if (onPi.length === 0) return;
|
|
204
|
+
const trouble = piTrouble(onPi.map(({ session }) => session.model).filter((model) => typeof model === 'string' && model.trim() !== ''));
|
|
205
|
+
if (trouble !== undefined) {
|
|
206
|
+
throw new Error(`${trouble} It would start ${onPi.map(({ bot, session }) => `${bot.name} ${session.name}`).join(', ')}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
/**
|
|
189
211
|
* The sessions that are not paused. A paused session is left out of a run
|
|
190
212
|
* altogether, so what it is set to asks nothing of the ones that will start;
|
|
@@ -273,7 +295,10 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
273
295
|
// Asked before the book is held, because it reads a folder of the harness's
|
|
274
296
|
// own files: nothing slow happens under the lock.
|
|
275
297
|
let which = whichConversation(book, home, bot, session, was, harness);
|
|
276
|
-
|
|
298
|
+
// A Pi session is told which conversation to be, and a new one is named here:
|
|
299
|
+
// the book is where that name is kept (ADR 0012, tech notes, section 6).
|
|
300
|
+
const fresh = harness === 'pi' ? randomUUID() : undefined;
|
|
301
|
+
const launchOf = (chosen) => launchFor(bots, bot, session, harness, home, workDir, chosen.resume, was?.address, fresh);
|
|
277
302
|
let launch = launchOf(which);
|
|
278
303
|
|
|
279
304
|
// A work dir is a plain folder, made for the session before it is told about
|
|
@@ -317,10 +342,16 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
317
342
|
// gets, and trust-hooks asks what this one got (#506).
|
|
318
343
|
let entry = { ...current.sessions[session.name], tab: made.tabId, launched, launched_with: session.extra_args ?? [], rules };
|
|
319
344
|
if (rules === undefined) delete entry.rules;
|
|
345
|
+
// Only Claude Code answers to an address. One left from a Claude run
|
|
346
|
+
// before a move to another harness would draw mail to nobody (#565).
|
|
347
|
+
if (harness !== 'claude') delete entry.address;
|
|
320
348
|
held = entry.session;
|
|
321
349
|
// Before the line is typed, so the hook finds no id here and takes the one
|
|
322
350
|
// it reports for a start rather than a clear, which would tell the duty twice.
|
|
323
|
-
if (which.noConversation !== undefined && held === which.noConversation) entry =
|
|
351
|
+
if (which.noConversation !== undefined && held === which.noConversation) entry = setAside(entry, which);
|
|
352
|
+
// The id a new Pi conversation is started under, written before the line
|
|
353
|
+
// that starts it, as a hook writes Claude Code's and Codex's.
|
|
354
|
+
if (fresh !== undefined && launch.resume === undefined) entry = rememberSession(entry, fresh);
|
|
324
355
|
current.sessions[session.name] = withUnclaimed(entry, which.unclaimed ?? []);
|
|
325
356
|
forgetClaimed(current);
|
|
326
357
|
});
|
|
@@ -331,13 +362,15 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
331
362
|
// The old tab's hook can name another conversation while the tab is being
|
|
332
363
|
// opened. A fresh start chosen before that is chosen again from the id the
|
|
333
364
|
// book holds now: resumed if the harness has it, and otherwise set aside the
|
|
334
|
-
// same way (review of PR #310).
|
|
335
|
-
|
|
336
|
-
|
|
365
|
+
// same way (review of PR #310). A Pi session has no hook, and the id this run
|
|
366
|
+
// named for it is the one the book now holds.
|
|
367
|
+
if (fresh === undefined && which.noConversation !== undefined && typeof held === 'string' && held !== which.noConversation) {
|
|
368
|
+
which = hasConversation(harness, home, held) ? { resume: held } : notHere(harness, home, held);
|
|
337
369
|
if (which.noConversation !== undefined) {
|
|
370
|
+
const chosen = which;
|
|
338
371
|
await updateBook(home, (current) => {
|
|
339
372
|
const entry = current.sessions[session.name];
|
|
340
|
-
if (entry?.session === held) current.sessions[session.name] =
|
|
373
|
+
if (entry?.session === held) current.sessions[session.name] = setAside(entry, chosen);
|
|
341
374
|
});
|
|
342
375
|
}
|
|
343
376
|
launch = launchOf(which);
|
|
@@ -402,7 +435,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
402
435
|
promptReceived,
|
|
403
436
|
promptFile: launch.promptFile,
|
|
404
437
|
resumed: launch.resume !== undefined,
|
|
405
|
-
noConversation: which.noConversation,
|
|
438
|
+
noConversation: which.ranOn === undefined ? which.noConversation : undefined,
|
|
439
|
+
harnessChanged: which.ranOn === undefined ? undefined : which.noConversation,
|
|
440
|
+
ranOn: which.ranOn,
|
|
406
441
|
unclaimed: which.unclaimed,
|
|
407
442
|
listLine: list?.typed,
|
|
408
443
|
listLineTrouble: list?.why,
|
|
@@ -532,7 +567,7 @@ function lookAndType(home, tabId, line, until) {
|
|
|
532
567
|
* The line that starts this session: resuming `resume`, or, with none, a fresh
|
|
533
568
|
* start carrying its duty. Returns `{ resume, prompt, promptFile, command, address }`.
|
|
534
569
|
*/
|
|
535
|
-
function launchFor(bots, bot, session, harness, home, workDir, resume, held) {
|
|
570
|
+
function launchFor(bots, bot, session, harness, home, workDir, resume, held, fresh) {
|
|
536
571
|
const prompt = resume === undefined ? startPrompt(session, { home, workDir }) : undefined;
|
|
537
572
|
// Anything longer than a line goes to the harness out of a file, rather than
|
|
538
573
|
// through the tab's shell a character at a time.
|
|
@@ -554,6 +589,9 @@ function launchFor(bots, bot, session, harness, home, workDir, resume, held) {
|
|
|
554
589
|
resume,
|
|
555
590
|
address,
|
|
556
591
|
kit: kitFolders(bots, bot.name),
|
|
592
|
+
// A new Pi conversation's id, and the name its tab and session list show,
|
|
593
|
+
// `<bot>.<session>` as a Codex thread is named (#480).
|
|
594
|
+
...(harness === 'pi' ? { sessionId: resume === undefined ? fresh : undefined, name: `${bot.name}.${session.name}` } : {}),
|
|
557
595
|
});
|
|
558
596
|
// The session's mailbox first, from inside its own tab (#317), and the
|
|
559
597
|
// harness whatever became of it.
|
|
@@ -831,7 +869,7 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
831
869
|
// it: a session paused before its first turn is one. There is nothing to
|
|
832
870
|
// resume, so it starts again with its duty, and the book keeps the id in the
|
|
833
871
|
// history with the reason (#295).
|
|
834
|
-
return
|
|
872
|
+
return notHere(harness, home, was.session);
|
|
835
873
|
}
|
|
836
874
|
// No tab: nothing has ever run for this session, so there is nothing to find.
|
|
837
875
|
if (typeof was?.tab !== 'string') return {};
|
|
@@ -851,6 +889,22 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
851
889
|
return { unclaimed: known.map((one) => one.id) };
|
|
852
890
|
}
|
|
853
891
|
|
|
892
|
+
/**
|
|
893
|
+
* An id the book holds that this harness has no conversation behind, so the
|
|
894
|
+
* session starts fresh: `{ noConversation: id }`, and `ranOn`, the harness that
|
|
895
|
+
* does have it, when the session has moved to another harness since it ran
|
|
896
|
+
* (#565). That conversation is not lost, only not this harness's to resume.
|
|
897
|
+
*/
|
|
898
|
+
function notHere(harness, home, id) {
|
|
899
|
+
const ranOn = HARNESSES.find((other) => other !== harness && hasConversation(other, home, id));
|
|
900
|
+
return ranOn === undefined ? { noConversation: id } : { noConversation: id, ranOn };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** The entry with the id `which` set aside moved into its history, with why. */
|
|
904
|
+
const setAside = (entry, which) => (which.ranOn === undefined
|
|
905
|
+
? forgetSession(entry, 'no conversation')
|
|
906
|
+
: forgetSession(entry, 'harness changed', { harness: which.ranOn }));
|
|
907
|
+
|
|
854
908
|
/**
|
|
855
909
|
* A bot's Orca project, made if it is not there yet, with `change` saying
|
|
856
910
|
* whether this run `made` it, `renamed` it or left it as it was (undefined).
|
|
@@ -886,7 +940,7 @@ export const promptPath = (bots, bot, session) =>
|
|
|
886
940
|
export const oldPromptPath = (bots, bot, session) =>
|
|
887
941
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
888
942
|
|
|
889
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
943
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
890
944
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
891
945
|
// A tab whose shell was not ready for the launch line, and why: nothing was
|
|
892
946
|
// typed into it (#498).
|
|
@@ -900,6 +954,9 @@ function entry(tab, { bot, name, created, running = false, blockedReason, prompt
|
|
|
900
954
|
if (listLineTrouble !== undefined) made.listLineTrouble = listLineTrouble;
|
|
901
955
|
// The id the book held that had no conversation behind it, now in the history.
|
|
902
956
|
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
957
|
+
// The id the book held that ran on another harness, the one this session
|
|
958
|
+
// has since moved from, now in the history (#565).
|
|
959
|
+
if (harnessChanged !== undefined) Object.assign(made, { harnessChanged, ranOn });
|
|
903
960
|
// Orca's own words for what is on screen waiting to be answered, when it
|
|
904
961
|
// gave any: the caller acts on it, the kit only passes it on.
|
|
905
962
|
if (blockedReason !== undefined) made.blockedReason = blockedReason;
|