@assuredloop/orca-bot-kit 0.32.0 → 0.34.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 +12 -12
- package/SETUP.md +12 -8
- package/package.json +1 -1
- package/skills/obk-grooming/SKILL.md +3 -2
- package/src/bot.js +1 -1
- package/src/clear.js +61 -28
- package/src/cli.js +8 -5
- package/src/conversations.js +1 -1
- package/src/init.js +2 -1
- package/src/message.js +39 -8
- package/src/orca.js +31 -0
- package/src/up.js +15 -3
package/README.md
CHANGED
|
@@ -5,14 +5,14 @@ a manager, developers, reviewers, a personal helper. Each bot keeps its
|
|
|
5
5
|
identity, its rules, its skills and its sessions across restarts.
|
|
6
6
|
|
|
7
7
|
[Orca](https://github.com/stablyai/orca) is the host: a bot is an Orca project
|
|
8
|
-
and a session is a tab, on Claude Code or
|
|
8
|
+
and a session is a tab, on Claude Code, Codex or Pi. The kit is an npm package with
|
|
9
9
|
an `obk` command and a set of skills. You are meant to manage the fleet by
|
|
10
10
|
talking to Bot Father, the first bot, whose skills call the CLI for you — not by
|
|
11
11
|
editing files.
|
|
12
12
|
|
|
13
13
|
The design is in [`docs/prd/prd.md`](docs/prd/prd.md); the decisions with lasting
|
|
14
14
|
consequences are in [`docs/adr/`](docs/adr/); what we know about Orca and the
|
|
15
|
-
|
|
15
|
+
harnesses is in [`docs/tech-notes.md`](docs/tech-notes.md).
|
|
16
16
|
|
|
17
17
|
## Start here
|
|
18
18
|
|
|
@@ -51,13 +51,13 @@ details and, when you say so, files the issue or gives you the text to paste.
|
|
|
51
51
|
## Status
|
|
52
52
|
|
|
53
53
|
Early. The CLI creates your bots folder, brings Bot Father up in Orca, creates
|
|
54
|
-
bots and their sessions on
|
|
55
|
-
from its charter and the rules it carries.
|
|
54
|
+
bots and their sessions on any of the three harnesses, and builds each bot's
|
|
55
|
+
`AGENTS.md` from its charter and the rules it carries.
|
|
56
56
|
|
|
57
57
|
## Install
|
|
58
58
|
|
|
59
|
-
Node.js >= 24.21.0, git, and Orca with at least one of Claude Code or
|
|
60
|
-
installed and configured. That line because the kit keeps writers out of each
|
|
59
|
+
Node.js >= 24.21.0, git, and Orca with at least one of Claude Code, Codex or
|
|
60
|
+
Pi installed and configured. That line because the kit keeps writers out of each
|
|
61
61
|
other's way in a bot's book with a SQLite write transaction, through Node's own
|
|
62
62
|
`node:sqlite`. That module is older than the 24 line; 24.21.0 is the first
|
|
63
63
|
release measured here that loads it without an experimental warning on stderr,
|
|
@@ -86,8 +86,8 @@ to npm with a provenance statement. Nothing else publishes.
|
|
|
86
86
|
obk init --bots /absolute/path/to/my-bots --harness claude
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
`--harness claude|codex` says which harness Bot Father itself runs on; there
|
|
90
|
-
no default. This makes the folder a git repository and seeds it:
|
|
89
|
+
`--harness claude|codex|pi` says which harness Bot Father itself runs on; there
|
|
90
|
+
is no default. This makes the folder a git repository and seeds it:
|
|
91
91
|
|
|
92
92
|
```
|
|
93
93
|
my-bots/
|
|
@@ -163,8 +163,8 @@ setting: name a model that carries it. A Pi session reads its bot's
|
|
|
163
163
|
no permission rules and no hooks, so the bot's allow list does not limit a Pi
|
|
164
164
|
session. `obk health` reads a Pi session's model and thinking level from Pi's
|
|
165
165
|
own record, and says when Pi cannot run it. `obk session clear` types Pi's
|
|
166
|
-
`/new` into a Pi tab
|
|
167
|
-
`obk init --harness pi`
|
|
166
|
+
`/new` into a Pi tab, and `obk session compact` types Pi's `/compact`.
|
|
167
|
+
`obk init --harness pi` puts Bot Father itself on Pi.
|
|
168
168
|
|
|
169
169
|
Every bot gets the kit's default permission rules, written by the kit with
|
|
170
170
|
nobody asked: its own commands for this bots folder, reading a long message,
|
|
@@ -535,8 +535,8 @@ leaves it running until the session is next restarted; after that, `--on --at`
|
|
|
535
535
|
puts it back.
|
|
536
536
|
`obk groom --compact` compacts its conversation between runs.
|
|
537
537
|
|
|
538
|
-
The grooming session is always a Claude Code one, even on a Codex Bot Father
|
|
539
|
-
it with `--harness claude` there): it is the clock, and where the reports arrive.
|
|
538
|
+
The grooming session is always a Claude Code one, even on a Codex or Pi Bot Father
|
|
539
|
+
(add it with `--harness claude` there): it is the clock, and where the reports arrive.
|
|
540
540
|
To have the grooming itself done on Codex, turn it on with `--run-on codex`, and
|
|
541
541
|
give the runs their model and effort:
|
|
542
542
|
|
package/SETUP.md
CHANGED
|
@@ -20,7 +20,7 @@ Everything else you can find out by looking.
|
|
|
20
20
|
## What you are making
|
|
21
21
|
|
|
22
22
|
Orca is the host. A bot is an Orca project and a session is a tab, running
|
|
23
|
-
Claude Code or
|
|
23
|
+
Claude Code, Codex or Pi. You are making the first bot, **Bot Father**, and a folder
|
|
24
24
|
that holds the whole fleet's configuration as a git repository of theirs.
|
|
25
25
|
|
|
26
26
|
When you are done they will have an Orca project called Bot Father with two
|
|
@@ -38,7 +38,7 @@ git --version
|
|
|
38
38
|
/Applications/Orca.app/Contents/Resources/bin/orca status --json
|
|
39
39
|
claude --version # at least one of these
|
|
40
40
|
codex --version # 0.156.1 or newer
|
|
41
|
-
pi --version # 1.3.0
|
|
41
|
+
pi --version # 1.3.0
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The Orca line must come back with `ok: true` and `result.runtime.reachable:
|
|
@@ -63,7 +63,7 @@ If Node is older than 24.21.0, say so and stop. The kit's book of sessions is a
|
|
|
63
63
|
SQLite write transaction through Node's own `node:sqlite`, and 24.21.0 is the
|
|
64
64
|
first release measured here that loads it without an experimental warning.
|
|
65
65
|
|
|
66
|
-
If
|
|
66
|
+
If no harness is installed, say so and stop. If only one is, that is fine,
|
|
67
67
|
and it is the one Bot Father will run on.
|
|
68
68
|
|
|
69
69
|
If Codex is older than 0.156.1, say so and ask them to update it before a bot
|
|
@@ -121,15 +121,19 @@ book of sessions. It holds none of the kit's code. The bots commit their own
|
|
|
121
121
|
changes to it; the kit's commands never commit, and pushing it somewhere is
|
|
122
122
|
theirs to decide.
|
|
123
123
|
|
|
124
|
-
**Which harness Bot Father runs on**, `claude` or `
|
|
125
|
-
and the kit will not guess. It is Bot Father's own harness only; each
|
|
126
|
-
make later chooses its own. If only one harness is installed, say so
|
|
127
|
-
it rather than asking as though the choice were open.
|
|
124
|
+
**Which harness Bot Father runs on**, `claude`, `codex` or `pi`. There is no
|
|
125
|
+
default and the kit will not guess. It is Bot Father's own harness only; each
|
|
126
|
+
bot they make later chooses its own. If only one harness is installed, say so
|
|
127
|
+
and confirm it rather than asking as though the choice were open. If they choose
|
|
128
|
+
`pi`, they sign in to Pi themselves first: the kit never handles Pi's
|
|
129
|
+
credentials, and `obk init` starts nothing while Pi can use no model. When you
|
|
130
|
+
ask or confirm, tell them: "On Pi, Bot Father acts without asking first, because
|
|
131
|
+
Pi has no approval step."
|
|
128
132
|
|
|
129
133
|
## 4. Make the fleet
|
|
130
134
|
|
|
131
135
|
```sh
|
|
132
|
-
obk init --bots <their path> --harness claude|codex
|
|
136
|
+
obk init --bots <their path> --harness claude|codex|pi
|
|
133
137
|
```
|
|
134
138
|
|
|
135
139
|
This makes the folder a git repository, seeds it, writes Bot Father, and opens
|
package/package.json
CHANGED
|
@@ -242,8 +242,9 @@ places and sending one to the other loses it.
|
|
|
242
242
|
The pass runs in a session of Bot Father's own, `grooming`, on Claude Code's
|
|
243
243
|
own scheduler. Add it like any session, with the model and effort it is to run
|
|
244
244
|
at: `obk session add --bots <folder> --bot bot-father --name grooming --model
|
|
245
|
-
<m> --effort <e>`, with `--harness claude` where Bot Father runs on Codex
|
|
246
|
-
`obk up`. Its launch line carries them, so they are what every run
|
|
245
|
+
<m> --effort <e>`, with `--harness claude` where Bot Father runs on Codex or
|
|
246
|
+
Pi, then `obk up`. Its launch line carries them, so they are what every run
|
|
247
|
+
uses.
|
|
247
248
|
|
|
248
249
|
The runs can be done on Codex instead. The session stays this Claude Code one,
|
|
249
250
|
the clock and the reader, and `obk groom --on --at <HH:MM> --run-on codex
|
package/src/bot.js
CHANGED
|
@@ -791,7 +791,7 @@ function asMapping(bot, file) {
|
|
|
791
791
|
export function requireBotsFolder(bots) {
|
|
792
792
|
const dir = botsDir(bots);
|
|
793
793
|
if (statSync(dir, { throwIfNoEntry: false })?.isDirectory() !== true) {
|
|
794
|
-
throw new Error(`${bots} is not a bots folder: run obk init --bots <path> --harness claude|codex first.`);
|
|
794
|
+
throw new Error(`${bots} is not a bots folder: run obk init --bots <path> --harness claude|codex|pi first.`);
|
|
795
795
|
}
|
|
796
796
|
return dir;
|
|
797
797
|
}
|
package/src/clear.js
CHANGED
|
@@ -16,7 +16,7 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
16
16
|
|
|
17
17
|
import { readBook, takeTypingTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
18
18
|
import { botDir, readBot } from './bot.js';
|
|
19
|
-
import { claudeTranscript, codexRollout } from './conversations.js';
|
|
19
|
+
import { claudeTranscript, codexRollout, piSession } from './conversations.js';
|
|
20
20
|
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
21
21
|
import { eachLine, firstLine } from './lines.js';
|
|
22
22
|
import { FEEDBACK_PANEL, feedbackPanelIn, findProject, orca, piAtWorkIn, piBox, QUESTION_ON_SCREEN, questionIn, screenRows, tabs, tabToTypeInto, TIMED_OUT } from './orca.js';
|
|
@@ -25,7 +25,7 @@ import { botsNamed, sessionsOf, typeListLine } from './up.js';
|
|
|
25
25
|
/** The harness's own command for each, as typed into its input line. Codex's and Pi's clear is `/new` (tech notes, sections 3 and 6). */
|
|
26
26
|
const COMMANDS = {
|
|
27
27
|
clear: { claude: '/clear', codex: '/new', pi: '/new' },
|
|
28
|
-
compact: { claude: '/compact', codex: '/compact' },
|
|
28
|
+
compact: { claude: '/compact', codex: '/compact', pi: '/compact' },
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/** The pointer each harness starts its input line with (tech notes, section 1). */
|
|
@@ -111,7 +111,7 @@ const PI_SELECTED = /^ *→ +/;
|
|
|
111
111
|
* (or null) and the one it holds now.
|
|
112
112
|
*/
|
|
113
113
|
export async function clearSession(bots, { bot, session }) {
|
|
114
|
-
const it = sessionToType(bots, bot, session
|
|
114
|
+
const it = sessionToType(bots, bot, session);
|
|
115
115
|
const was = readBook(it.home).sessions[session]?.session ?? null;
|
|
116
116
|
const { command, handle, typing } = await enter(it, 'clear');
|
|
117
117
|
|
|
@@ -147,7 +147,7 @@ export async function clearSession(bots, { bot, session }) {
|
|
|
147
147
|
* command began, and not yet when COMPACT_MS passed without one.
|
|
148
148
|
*/
|
|
149
149
|
export async function compactSession(bots, { bot, session }) {
|
|
150
|
-
const it = sessionToType(bots, bot, session
|
|
150
|
+
const it = sessionToType(bots, bot, session);
|
|
151
151
|
// What the record holds when /compact is typed is not this compact's: one
|
|
152
152
|
// that ends while the kit waits for the session to be idle is the one
|
|
153
153
|
// before it (review of PR #466). So only what is written after counts.
|
|
@@ -174,17 +174,13 @@ export async function compactSession(bots, { bot, session }) {
|
|
|
174
174
|
/**
|
|
175
175
|
* The session as something to type into, once everything that refuses before
|
|
176
176
|
* Orca is asked to type has been asked: `{ name, home, session, harness, tabId }`.
|
|
177
|
-
* A compact is refused on Pi: a later slice of #547 may add it.
|
|
178
177
|
*/
|
|
179
|
-
export function sessionToType(bots, bot, session
|
|
178
|
+
export function sessionToType(bots, bot, session) {
|
|
180
179
|
botsNamed(bots, bot);
|
|
181
180
|
const home = realpathSync(botDir(bots, bot));
|
|
182
181
|
const known = readBot(home, bot);
|
|
183
182
|
const [settings] = sessionsOf(known, session);
|
|
184
183
|
const name = `${bot} ${session}`;
|
|
185
|
-
if (verb === 'compact' && harnessOf(settings, known.harness) === 'pi') {
|
|
186
|
-
throw new Error(`${name} runs on Pi, and obk session ${verb} does not work on a Pi session yet: a later slice of #547 adds it. Nothing was typed.`);
|
|
187
|
-
}
|
|
188
184
|
const unpause = `${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${bot}`;
|
|
189
185
|
if (known.paused === true) throw new Error(`${bot} is paused, so ${name} is not running and nothing was typed. Bring it back with ${unpause}.`);
|
|
190
186
|
if (settings.paused === true) throw new Error(`${name} is paused, so it is not running and nothing was typed. Bring it back with ${unpause} --session ${session}.`);
|
|
@@ -217,6 +213,7 @@ async function enter(it, verb, before = () => {}) {
|
|
|
217
213
|
before();
|
|
218
214
|
},
|
|
219
215
|
cannot: (wrong) => (verb === 'compact' && wrong.menu && it.harness === 'codex' ? ` ${harnessName(it.harness)} here cannot compact: its menu does not offer ${COMMANDS[verb][it.harness]}.` : ''),
|
|
216
|
+
close: it.harness === 'pi' ? PI_CLOSE : undefined,
|
|
220
217
|
});
|
|
221
218
|
return { ...typed, typing };
|
|
222
219
|
} catch (error) {
|
|
@@ -231,11 +228,12 @@ async function enter(it, verb, before = () => {}) {
|
|
|
231
228
|
* screen: by default the slash menu's check, which a command with words after
|
|
232
229
|
* it does not pass, since Codex closes its menu at the space (#480). `before`
|
|
233
230
|
* runs just before the command is typed; `cannot` adds to a refusal what a
|
|
234
|
-
* wrong screen means;
|
|
235
|
-
*
|
|
236
|
-
*
|
|
231
|
+
* wrong screen means; `close`, `{ key, check }`, is one more key typed once
|
|
232
|
+
* `check` passes, and its own check of the screen before the return; past
|
|
233
|
+
* `deadline`, a time, what was typed is taken back and nothing is entered.
|
|
234
|
+
* Returns the command, the handle it went into, and when it was typed.
|
|
237
235
|
*/
|
|
238
|
-
export async function typeCommand(it, command, { before = () => {}, check = menuWrong, cannot = () => '', deadline = Infinity } = {}) {
|
|
236
|
+
export async function typeCommand(it, command, { before = () => {}, check = menuWrong, cannot = () => '', close, deadline = Infinity } = {}) {
|
|
239
237
|
// Nothing may be in the input line before: no draft for the command to join.
|
|
240
238
|
// Orca gives a draft in the line beside the screen, and the screen's line
|
|
241
239
|
// then reads the pointer alone (#510, #516). Where it gives none, Codex's
|
|
@@ -299,22 +297,31 @@ export async function typeCommand(it, command, { before = () => {}, check = menu
|
|
|
299
297
|
takeBack();
|
|
300
298
|
throw new Error(`${it.name}: ${command} was being typed, and after ${sofar} ${look.why}, so what was typed was taken back and nothing was entered.`);
|
|
301
299
|
};
|
|
300
|
+
const typeKey = (char) => {
|
|
301
|
+
try {
|
|
302
|
+
send(handle, char);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// A send Orca did not answer in time may have gone in all the same.
|
|
305
|
+
// Counted only where the line was empty before the first key, so the
|
|
306
|
+
// one backspace too many can take nothing of the user's (#480 review).
|
|
307
|
+
if (error.code === TIMED_OUT && (it.harness === 'codex' || it.harness === 'pi')) typed += 1;
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
typed += 1;
|
|
311
|
+
};
|
|
302
312
|
try {
|
|
303
313
|
for (const char of command) {
|
|
304
314
|
if (typed > 0) await lookAgain();
|
|
305
|
-
|
|
306
|
-
send(handle, char);
|
|
307
|
-
} catch (error) {
|
|
308
|
-
// A send Orca did not answer in time may have gone in all the same.
|
|
309
|
-
// Counted only where the line was empty before the first key, so the
|
|
310
|
-
// one backspace too many can take nothing of the user's (#480 review).
|
|
311
|
-
if (error.code === TIMED_OUT && (it.harness === 'codex' || it.harness === 'pi')) typed += 1;
|
|
312
|
-
throw error;
|
|
313
|
-
}
|
|
314
|
-
typed += 1;
|
|
315
|
+
typeKey(char);
|
|
315
316
|
}
|
|
316
317
|
await lookAgain();
|
|
317
318
|
let wrong = await typedWrong(handle, it.harness, command, version, check);
|
|
319
|
+
if (wrong === undefined && close !== undefined) {
|
|
320
|
+
await lookAgain();
|
|
321
|
+
typeKey(close.key);
|
|
322
|
+
await lookAgain();
|
|
323
|
+
wrong = await typedWrong(handle, it.harness, command, version, close.check);
|
|
324
|
+
}
|
|
318
325
|
if (wrong === undefined && Date.now() > deadline) wrong = { why: 'its time ran out' };
|
|
319
326
|
if (wrong !== undefined) {
|
|
320
327
|
takeBack();
|
|
@@ -498,8 +505,7 @@ function codexMenuWrong(rows, at, line, command, version) {
|
|
|
498
505
|
* So the editor's one row, or Orca's draft where it gives one, reads the
|
|
499
506
|
* command, and the selected row below the box names it. A menu still drawn
|
|
500
507
|
* for an earlier part of the command looks the same when its selected row is
|
|
501
|
-
* the command too
|
|
502
|
-
* unlikely, and this cannot tell it apart.
|
|
508
|
+
* the command too, and this cannot tell it apart: PI_CLOSE does.
|
|
503
509
|
*/
|
|
504
510
|
function piMenuWrong(rows, command, draft) {
|
|
505
511
|
const box = piBox(rows);
|
|
@@ -514,6 +520,32 @@ function piMenuWrong(rows, command, draft) {
|
|
|
514
520
|
return undefined;
|
|
515
521
|
}
|
|
516
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Pi 1.3.0 applies a menu list still drawn from an earlier key against that
|
|
525
|
+
* key's prefix, so `/compact` under a list for `/compa` submits `/ccompact`,
|
|
526
|
+
* and `/new` under one for `/n` submits `/nnew`; the screen cannot tell that
|
|
527
|
+
* list from a fresh one. A space makes Pi list `/compact ` or `/new `, for
|
|
528
|
+
* which it has nothing, and so close the menu; with no menu, Return submits
|
|
529
|
+
* the line trimmed, the command (tech notes, section 6; #671, #673).
|
|
530
|
+
*/
|
|
531
|
+
const PI_CLOSE = { key: ' ', check: (rows, harness, command, version, draft) => piClosedWrong(rows, command, draft) };
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Why the screen does not show Pi's editor reading `command`, a space after
|
|
535
|
+
* it, with no menu below the box, or undefined when it does. A list still
|
|
536
|
+
* drawn has its selected row.
|
|
537
|
+
*/
|
|
538
|
+
function piClosedWrong(rows, command, draft) {
|
|
539
|
+
const box = piBox(rows);
|
|
540
|
+
if (box === undefined) return { why: 'its screen shows no input box' };
|
|
541
|
+
const lines = box.editor.map((row) => row.trim()).filter((row) => row !== '');
|
|
542
|
+
const line = draft?.trim() ?? lines.join(' ⏎ ');
|
|
543
|
+
if (line !== command || (draft === undefined && lines.length !== 1)) return { why: `its input line reads "${line}" after the space` };
|
|
544
|
+
const selected = box.below.find((row) => PI_SELECTED.test(row));
|
|
545
|
+
if (selected !== undefined) return { why: `a menu is still below its input box after the space, its selected row "${selected.trim()}"` };
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
517
549
|
/** The Codex version that wrote the rollout of `conversation`, from its session_meta line; undefined when that cannot be read. */
|
|
518
550
|
function codexVersion(conversation) {
|
|
519
551
|
const file = typeof conversation === 'string' ? codexRollout(conversation) : undefined;
|
|
@@ -553,6 +585,7 @@ async function answerWhereToRun(it, handle, command) {
|
|
|
553
585
|
/** Where the harness keeps the record of `conversation`, or undefined when it has none yet. */
|
|
554
586
|
function recordFile(harness, home, conversation) {
|
|
555
587
|
if (conversation === null) return undefined;
|
|
588
|
+
if (harness === 'pi') return piSession(home, conversation);
|
|
556
589
|
return harness === 'claude' ? claudeTranscript(home, conversation) : codexRollout(conversation);
|
|
557
590
|
}
|
|
558
591
|
|
|
@@ -585,9 +618,9 @@ function compactedSince(harness, record, since) {
|
|
|
585
618
|
} catch {
|
|
586
619
|
return false;
|
|
587
620
|
}
|
|
588
|
-
// Claude Code's marker, and
|
|
589
|
-
const compaction = harness === 'claude'
|
|
590
|
-
|
|
621
|
+
// Claude Code's marker, Codex's, and Pi's (tech notes, sections 2, 3 and 6).
|
|
622
|
+
const compaction = harness === 'claude' ? entry?.type === 'system' && entry.subtype === 'compact_boundary'
|
|
623
|
+
: harness === 'pi' ? entry?.type === 'compaction'
|
|
591
624
|
: entry?.type === 'compacted';
|
|
592
625
|
found = compaction && Date.parse(entry.timestamp) >= since;
|
|
593
626
|
return found;
|
package/src/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { parseArgs } from 'node:util';
|
|
14
14
|
|
|
15
|
-
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, refuseCodexOutside, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
|
|
15
|
+
import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, displayName, leadsOutside, readBot, refuseCodexOutside, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
|
|
16
16
|
import { COMMANDS } from './commands.js';
|
|
17
17
|
import { addCommand, groomCommand, grooming, upCommand } from './groom.js';
|
|
18
18
|
import { checkHealth, orcaSettingFindings } from './health.js';
|
|
@@ -39,7 +39,7 @@ import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
|
39
39
|
const USAGE = `obk — Orca Bot Kit.
|
|
40
40
|
|
|
41
41
|
Usage:
|
|
42
|
-
obk init --bots <path> --harness claude|codex
|
|
42
|
+
obk init --bots <path> --harness claude|codex|pi
|
|
43
43
|
Create your bots folder: a git repo holding your
|
|
44
44
|
bots' configuration, with Bot Father in it, and
|
|
45
45
|
open Bot Father in Orca.
|
|
@@ -610,8 +610,6 @@ async function record(bots, bot) {
|
|
|
610
610
|
|
|
611
611
|
const commands = {
|
|
612
612
|
async init(bots, values) {
|
|
613
|
-
// Bot Father on Pi is not in the first slice of #547.
|
|
614
|
-
if (values.harness === 'pi') throw new Error('init does not take --harness pi yet: Bot Father runs on claude or codex, and its other bots may run on Pi. Nothing was written.');
|
|
615
613
|
// Asked before anything is written, so an Orca that is down leaves the disk
|
|
616
614
|
// exactly as it was and the caller can simply run the command again.
|
|
617
615
|
refuseWhenOrcaIsDown();
|
|
@@ -1934,12 +1932,17 @@ function tabLines({ bots, created, completed, rules, skills, permissions = [], t
|
|
|
1934
1932
|
const back = one.session === undefined ? '' : ` --session ${one.session}`;
|
|
1935
1933
|
lines.push(`${'paused'.padEnd(9)} ${what} left closed. Bring it back: ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${one.bot}${back}`);
|
|
1936
1934
|
}
|
|
1935
|
+
// A window that has not loaded a bot's project gives its tabs no notice of
|
|
1936
|
+
// mail (#676).
|
|
1937
|
+
for (const one of projects.filter((project) => project.windowTrouble !== undefined)) {
|
|
1938
|
+
lines.push(`${'window'.padEnd(9)} ${displayName(one.bot)} its tabs may get no fleet mail until Orca's window loads its project: ${one.windowTrouble}. Try again: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${one.bot}`);
|
|
1939
|
+
}
|
|
1937
1940
|
|
|
1938
1941
|
// Last before the summary, because what a check found is about the setup the
|
|
1939
1942
|
// run has just left behind rather than about any one thing it did.
|
|
1940
1943
|
lines.push(...foundLines(found));
|
|
1941
1944
|
// Hedged, because nothing here can see whether the window was told (#224).
|
|
1942
|
-
if (projects.
|
|
1945
|
+
if (projects.some((project) => project.change !== undefined)) lines.push(RELOAD_LINE);
|
|
1943
1946
|
lines.push(summary);
|
|
1944
1947
|
return lines;
|
|
1945
1948
|
}
|
package/src/conversations.js
CHANGED
|
@@ -49,7 +49,7 @@ const PI_FILE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_(.+)\.jsonl$/;
|
|
|
49
49
|
const piIdOf = (name) => PI_FILE.exec(name)?.[1];
|
|
50
50
|
|
|
51
51
|
/** Where Pi keeps the conversation `id` of this bot home, or undefined when it has none yet. */
|
|
52
|
-
const piSession = (home, id) => {
|
|
52
|
+
export const piSession = (home, id) => {
|
|
53
53
|
const name = files(piDir(home)).find((one) => piIdOf(one) === id);
|
|
54
54
|
return name === undefined ? undefined : path.join(piDir(home), name);
|
|
55
55
|
};
|
package/src/init.js
CHANGED
|
@@ -97,7 +97,8 @@ charter: |
|
|
|
97
97
|
names in paths, tokens, what a charter says. Put in the command that was run
|
|
98
98
|
and its whole output, what you expected instead, and what these print:
|
|
99
99
|
obk --version, obk health --bots <folder> --json, node --version, the Orca
|
|
100
|
-
version, and claude --version or
|
|
100
|
+
version, and claude --version, codex --version or pi --version; and the
|
|
101
|
+
operating system.
|
|
101
102
|
One problem per issue. If you cannot file it yourself, give your owner the
|
|
102
103
|
text to paste.
|
|
103
104
|
rules: []
|
package/src/message.js
CHANGED
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
|
|
19
19
|
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import { setTimeout as pause } from 'node:timers/promises';
|
|
21
22
|
|
|
22
23
|
import { MAILBOX_WAIT_MS, readBook, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
23
24
|
import { botDir, botNames, readBot } from './bot.js';
|
|
24
25
|
import { harnessOf, isAddressOf, kitFolders, ownCli, reachesMail, SHELL_ENV, shellWord } from './launch.js';
|
|
25
|
-
import { ackMailbox, coordinatorOf, postMessage, pressEscape, readMailbox, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
|
|
26
|
+
import { ackMailbox, coordinatorOf, listedOrphaned, loadInWindow, postMessage, pressEscape, readMailbox, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* How much of a message travels as itself. Above this it is written to a file
|
|
@@ -161,7 +162,7 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
|
|
|
161
162
|
|
|
162
163
|
const written = bodyOf(bots, { from, to, subject, text, textFile });
|
|
163
164
|
// Looked at before the post, which is the send's last call to Orca (PRD 6.9).
|
|
164
|
-
const shown = loaded(to);
|
|
165
|
+
const shown = await loaded(to);
|
|
165
166
|
// The Escape goes first, so the turn has ended when the letter is in.
|
|
166
167
|
const interrupted = interrupt ? interruptBusy(to) : {};
|
|
167
168
|
const message = postMessage({
|
|
@@ -189,20 +190,50 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
|
|
|
189
190
|
* and lists one it has not as orphaned, as after a window restart (#579; tech
|
|
190
191
|
* notes, section 1). So an orphaned tab is said, with its handle, and a
|
|
191
192
|
* listing that cannot be read is said too, rather than taken for a loaded tab.
|
|
192
|
-
*
|
|
193
|
+
* An orphaned tab of a project the window has never fetched is loaded by
|
|
194
|
+
* `loadInWindow`, which changes nothing on screen, so the kit asks for that
|
|
195
|
+
* and looks again until LOOK_MS after the ask (#676). A tab it does not load,
|
|
196
|
+
* as after a window restart (#579), is said as before. This is all before the
|
|
197
|
+
* send: a tab orphaned after it is not seen here.
|
|
193
198
|
*/
|
|
194
|
-
function loaded(to) {
|
|
199
|
+
async function loaded(to) {
|
|
195
200
|
if (to.tab === undefined) return {};
|
|
196
|
-
let
|
|
201
|
+
let tab;
|
|
197
202
|
try {
|
|
198
|
-
|
|
203
|
+
tab = tabs(to.home, { timeoutMs: LOOK_MS }).find((one) => one.tabId === to.tab);
|
|
199
204
|
} catch (error) {
|
|
200
205
|
return { loadTrouble: error.message };
|
|
201
206
|
}
|
|
202
|
-
|
|
203
|
-
return
|
|
207
|
+
if (tab?.orphaned !== true) return {};
|
|
208
|
+
return (await loadedNow(to.home, tab.handle)) ? {} : { orphaned: true, handle: tab.handle };
|
|
204
209
|
}
|
|
205
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Whether the window loads the orphaned tab `handle` at `home` once asked to.
|
|
213
|
+
* The ask and every look after it keep to one deadline, LOOK_MS from the ask,
|
|
214
|
+
* and each Orca call gets only the time that is left, so a slow Orca cannot
|
|
215
|
+
* hold the send up for longer (#676). A look that fails or runs out counts as
|
|
216
|
+
* a tab still orphaned.
|
|
217
|
+
*/
|
|
218
|
+
async function loadedNow(home, handle) {
|
|
219
|
+
const until = Date.now() + LOOK_MS;
|
|
220
|
+
const left = () => until - Date.now();
|
|
221
|
+
loadInWindow(home, { timeoutMs: LOOK_MS });
|
|
222
|
+
try {
|
|
223
|
+
for (let ms = left(); ms > 0; ms = left()) {
|
|
224
|
+
if (!listedOrphaned(home, handle, { timeoutMs: ms })) return true;
|
|
225
|
+
const wait = Math.min(RELOOK_MS, left());
|
|
226
|
+
if (wait > 0) await pause(wait);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// A look that fails or runs out leaves the tab said as orphaned.
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** How long the kit waits between two looks at a tab it asked the window to load: it was loaded within 0.4 s, live (#676). */
|
|
235
|
+
const RELOOK_MS = 200;
|
|
236
|
+
|
|
206
237
|
/**
|
|
207
238
|
* What is waiting for a session, and the reading of it.
|
|
208
239
|
*
|
package/src/orca.js
CHANGED
|
@@ -347,6 +347,29 @@ const CLIENT_KILL_MS = 3000;
|
|
|
347
347
|
*/
|
|
348
348
|
export const tellWindow = (projectId) => askRuntime('project.update', { projectId, updates: {} }) !== undefined;
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Have Orca's window load the tabs of the Orca project at `home`: undefined
|
|
352
|
+
* when Orca took the call, or what went wrong.
|
|
353
|
+
*
|
|
354
|
+
* The window loads a project's tabs only once it has fetched the project's
|
|
355
|
+
* worktrees, and nothing fetches them for a project the kit made, so its tabs
|
|
356
|
+
* stay orphaned and get no notice of mail (#676, tech notes). `worktree set`
|
|
357
|
+
* with no field writes an empty change and has Orca send the window the event
|
|
358
|
+
* that fetches them: nothing the user sees changes. What went wrong is said
|
|
359
|
+
* without Orca's own words, which name worktrees (PRD KIT-22: the kit says
|
|
360
|
+
* "Orca project"). `options` are `orca`'s.
|
|
361
|
+
*/
|
|
362
|
+
export function loadInWindow(home, options) {
|
|
363
|
+
try {
|
|
364
|
+
orca(['worktree', 'set', '--worktree', `path:${home}`], options);
|
|
365
|
+
return undefined;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return typeof error.code === 'string' && !/worktree/i.test(error.code)
|
|
368
|
+
? `Orca refused the call (${error.code})`
|
|
369
|
+
: 'Orca gave no answer the kit could read';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
350
373
|
/**
|
|
351
374
|
* One call to Orca's runtime for a method its CLI does not offer, and the
|
|
352
375
|
* runtime's answer, or undefined when there is none to be had.
|
|
@@ -433,6 +456,14 @@ export const tabs = (home, options) => orca(['terminal', 'list', '--worktree', `
|
|
|
433
456
|
? { ...tab, tabId: orca(['terminal', 'show', '--terminal', tab.handle], options).terminal.tabId }
|
|
434
457
|
: tab));
|
|
435
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Whether Orca lists the terminal `handle` at `home` as orphaned, from one
|
|
461
|
+
* listing and no `terminal show`, so a caller with a deadline can give the one
|
|
462
|
+
* call all the time it has left (#676).
|
|
463
|
+
*/
|
|
464
|
+
export const listedOrphaned = (home, handle, options) => orca(['terminal', 'list', '--worktree', `path:${home}`], options).terminals
|
|
465
|
+
.some((tab) => tab.handle === handle && tab.orphaned === true);
|
|
466
|
+
|
|
436
467
|
/**
|
|
437
468
|
* Open a tab in the Orca project at `home`, titled `title`.
|
|
438
469
|
*
|
package/src/up.js
CHANGED
|
@@ -16,7 +16,7 @@ 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';
|
|
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';
|
|
19
|
+
import { asFolderProject, coordinatorOf, findProject, harnessInTab, loadInWindow, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
20
20
|
import { TAB_ENV } from './record.js';
|
|
21
21
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
22
22
|
import { linkSkills } from './skills.js';
|
|
@@ -40,7 +40,9 @@ const SECOND_LOOK_MS = 2000;
|
|
|
40
40
|
* Bring bots up in Orca. Returns `{ tabs, rules }`: one tab entry per tab it
|
|
41
41
|
* looked at — the sessions the book knows, and, for Bot Father, whatever else
|
|
42
42
|
* is open in its project — and one rules entry and one skills entry per bot.
|
|
43
|
-
* `projects` has one entry per Orca project this run made or renamed
|
|
43
|
+
* `projects` has one entry per Orca project this run made or renamed, and per
|
|
44
|
+
* project Orca's window may not have loaded, with what went wrong as
|
|
45
|
+
* `windowTrouble` (#676).
|
|
44
46
|
*
|
|
45
47
|
* With no name it is every bot in the folder, in name order. `bot` brings up
|
|
46
48
|
* one bot and `session` one of its sessions, for a caller that wants one thing
|
|
@@ -238,6 +240,10 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
238
240
|
// Orca's window does not see a project made or renamed on the command line
|
|
239
241
|
// until it is told (#224).
|
|
240
242
|
if (change !== undefined) tellWindow(orca.project);
|
|
243
|
+
// Its tabs get no notice of mail until the window loads them, and the window
|
|
244
|
+
// does not load a project it has never fetched (#676). Every run asks, so a
|
|
245
|
+
// project made before this is loaded too; the call changes nothing on screen.
|
|
246
|
+
const windowTrouble = loadInWindow(home);
|
|
241
247
|
|
|
242
248
|
const live = new Map(tabs(home).map((tab) => [tab.tabId, tab]));
|
|
243
249
|
const report = [];
|
|
@@ -256,7 +262,13 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
256
262
|
: spare.map((tab) => entry(tab, { bot: name, name: null, created: false }))));
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
|
|
265
|
+
const project = {
|
|
266
|
+
bot: name,
|
|
267
|
+
project: orca.project,
|
|
268
|
+
...(change === undefined ? {} : { change }),
|
|
269
|
+
...(windowTrouble === undefined ? {} : { windowTrouble }),
|
|
270
|
+
};
|
|
271
|
+
return { tabs: report, project: change === undefined && windowTrouble === undefined ? undefined : project };
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
async function bringUpSession(bots, home, live, session, bot, title) {
|