@assuredloop/orca-bot-kit 0.31.0 → 0.33.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 +19 -14
- package/SETUP.md +10 -8
- package/package.json +1 -1
- package/skills/obk-grooming/SKILL.md +3 -2
- package/src/bot.js +1 -1
- package/src/clear.js +106 -37
- package/src/cli.js +19 -8
- package/src/conversations.js +1 -1
- package/src/health.js +72 -10
- package/src/init.js +2 -1
- package/src/launch.js +23 -0
- package/src/orca.js +92 -6
- package/src/pi-record.js +96 -0
- package/src/pi.js +55 -17
- package/src/rules.js +9 -1
- package/src/settings.js +109 -19
- package/src/skills.js +22 -9
- package/src/temp.js +69 -10
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/
|
|
@@ -152,14 +152,19 @@ a Codex session
|
|
|
152
152
|
([ADR 0045](docs/adr/0045-a-codex-sessions-work-dir-stays-in-its-bot-folder.md)).
|
|
153
153
|
|
|
154
154
|
A bot or a session can also run on [Pi](https://pi.dev) (`--harness pi`, tested
|
|
155
|
-
with Pi 1.
|
|
155
|
+
with Pi 1.3.0). Pi asks before no tool call, so a Pi session is always
|
|
156
156
|
`dangerously-skip`, and the kit refuses `auto` and `ask` for it. Sign in to Pi
|
|
157
157
|
yourself first: `obk up` asks Pi which models it can use, and starts nothing
|
|
158
158
|
when Pi has none, or not the one the session names. The kit gives a Pi session
|
|
159
159
|
its id, so `obk up` resumes it after its tab is closed. Pi has no context
|
|
160
|
-
setting: name a model that carries it.
|
|
161
|
-
|
|
162
|
-
`obk
|
|
160
|
+
setting: name a model that carries it. A Pi session reads its bot's
|
|
161
|
+
`AGENTS.md` and loads its bot's skills from `.agents/skills`; when
|
|
162
|
+
`obk skills build` changes them, it types `/reload` into an idle Pi tab. Pi has
|
|
163
|
+
no permission rules and no hooks, so the bot's allow list does not limit a Pi
|
|
164
|
+
session. `obk health` reads a Pi session's model and thinking level from Pi's
|
|
165
|
+
own record, and says when Pi cannot run it. `obk session clear` types Pi's
|
|
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.
|
|
163
168
|
|
|
164
169
|
Every bot gets the kit's default permission rules, written by the kit with
|
|
165
170
|
nobody asked: its own commands for this bots folder, reading a long message,
|
|
@@ -530,8 +535,8 @@ leaves it running until the session is next restarted; after that, `--on --at`
|
|
|
530
535
|
puts it back.
|
|
531
536
|
`obk groom --compact` compacts its conversation between runs.
|
|
532
537
|
|
|
533
|
-
The grooming session is always a Claude Code one, even on a Codex Bot Father
|
|
534
|
-
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.
|
|
535
540
|
To have the grooming itself done on Codex, turn it on with `--run-on codex`, and
|
|
536
541
|
give the runs their model and effort:
|
|
537
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.
|
|
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,17 @@ 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.
|
|
128
130
|
|
|
129
131
|
## 4. Make the fleet
|
|
130
132
|
|
|
131
133
|
```sh
|
|
132
|
-
obk init --bots <their path> --harness claude|codex
|
|
134
|
+
obk init --bots <their path> --harness claude|codex|pi
|
|
133
135
|
```
|
|
134
136
|
|
|
135
137
|
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,16 +16,16 @@ 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
|
-
import { FEEDBACK_PANEL, feedbackPanelIn, findProject, orca, QUESTION_ON_SCREEN, questionIn, screenRows, tabs, tabToTypeInto, TIMED_OUT } from './orca.js';
|
|
22
|
+
import { FEEDBACK_PANEL, feedbackPanelIn, findProject, orca, piAtWorkIn, piBox, QUESTION_ON_SCREEN, questionIn, screenRows, tabs, tabToTypeInto, TIMED_OUT } from './orca.js';
|
|
23
23
|
import { botsNamed, sessionsOf, typeListLine } from './up.js';
|
|
24
24
|
|
|
25
|
-
/** The harness's own command for each, as typed into its input line. Codex's clear is `/new` (tech notes,
|
|
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
|
-
clear: { claude: '/clear', codex: '/new' },
|
|
28
|
-
compact: { claude: '/compact', codex: '/compact' },
|
|
27
|
+
clear: { claude: '/clear', codex: '/new', pi: '/new' },
|
|
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). */
|
|
@@ -100,12 +100,18 @@ const CHAR_GAP_MS = 100;
|
|
|
100
100
|
const AT_WORK = [/esc to interrupt/i, /^\s*\S\s+\S+…\s+\(\d/];
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
*
|
|
103
|
+
* The selected row of Pi 1.3.0's slash menu, which it draws below its input
|
|
104
|
+
* box (pi-tui's `select-list.js`; read in code, #663).
|
|
105
|
+
*/
|
|
106
|
+
const PI_SELECTED = /^ *→ +/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear the session: `/clear` on Claude Code, `/new` on Codex and Pi. Returns
|
|
104
110
|
* `{ bot, session, harness, was, now }`, the conversation the book held before
|
|
105
111
|
* (or null) and the one it holds now.
|
|
106
112
|
*/
|
|
107
113
|
export async function clearSession(bots, { bot, session }) {
|
|
108
|
-
const it = sessionToType(bots, bot, session
|
|
114
|
+
const it = sessionToType(bots, bot, session);
|
|
109
115
|
const was = readBook(it.home).sessions[session]?.session ?? null;
|
|
110
116
|
const { command, handle, typing } = await enter(it, 'clear');
|
|
111
117
|
|
|
@@ -141,7 +147,7 @@ export async function clearSession(bots, { bot, session }) {
|
|
|
141
147
|
* command began, and not yet when COMPACT_MS passed without one.
|
|
142
148
|
*/
|
|
143
149
|
export async function compactSession(bots, { bot, session }) {
|
|
144
|
-
const it = sessionToType(bots, bot, session
|
|
150
|
+
const it = sessionToType(bots, bot, session);
|
|
145
151
|
// What the record holds when /compact is typed is not this compact's: one
|
|
146
152
|
// that ends while the kit waits for the session to be idle is the one
|
|
147
153
|
// before it (review of PR #466). So only what is written after counts.
|
|
@@ -168,17 +174,13 @@ export async function compactSession(bots, { bot, session }) {
|
|
|
168
174
|
/**
|
|
169
175
|
* The session as something to type into, once everything that refuses before
|
|
170
176
|
* Orca is asked to type has been asked: `{ name, home, session, harness, tabId }`.
|
|
171
|
-
* A `verb`, clear or compact, is refused on Pi until a later slice of #547.
|
|
172
177
|
*/
|
|
173
|
-
export function sessionToType(bots, bot, session
|
|
178
|
+
export function sessionToType(bots, bot, session) {
|
|
174
179
|
botsNamed(bots, bot);
|
|
175
180
|
const home = realpathSync(botDir(bots, bot));
|
|
176
181
|
const known = readBot(home, bot);
|
|
177
182
|
const [settings] = sessionsOf(known, session);
|
|
178
183
|
const name = `${bot} ${session}`;
|
|
179
|
-
if (verb !== undefined && harnessOf(settings, known.harness) === 'pi') {
|
|
180
|
-
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.`);
|
|
181
|
-
}
|
|
182
184
|
const unpause = `${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${bot}`;
|
|
183
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}.`);
|
|
184
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}.`);
|
|
@@ -211,6 +213,7 @@ async function enter(it, verb, before = () => {}) {
|
|
|
211
213
|
before();
|
|
212
214
|
},
|
|
213
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,
|
|
214
217
|
});
|
|
215
218
|
return { ...typed, typing };
|
|
216
219
|
} catch (error) {
|
|
@@ -225,17 +228,25 @@ async function enter(it, verb, before = () => {}) {
|
|
|
225
228
|
* screen: by default the slash menu's check, which a command with words after
|
|
226
229
|
* it does not pass, since Codex closes its menu at the space (#480). `before`
|
|
227
230
|
* runs just before the command is typed; `cannot` adds to a refusal what a
|
|
228
|
-
* wrong screen means;
|
|
229
|
-
*
|
|
230
|
-
*
|
|
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.
|
|
231
235
|
*/
|
|
232
|
-
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 } = {}) {
|
|
233
237
|
// Nothing may be in the input line before: no draft for the command to join.
|
|
234
238
|
// Orca gives a draft in the line beside the screen, and the screen's line
|
|
235
239
|
// then reads the pointer alone (#510, #516). Where it gives none, Codex's
|
|
236
|
-
// line on the screen has to be empty.
|
|
240
|
+
// line on the screen has to be empty, and so do Pi's editor rows.
|
|
237
241
|
const emptyLine = ({ rows, draft }) => {
|
|
238
242
|
if (draft !== undefined) throw new Error(`${it.name}: nothing was typed, because its input line holds a draft: it reads "${draft}".${shownEnd(rows)}`);
|
|
243
|
+
if (it.harness === 'pi') {
|
|
244
|
+
const box = piBox(rows);
|
|
245
|
+
if (box === undefined) throw new Error(`${it.name}: nothing was typed, because its screen shows no input box.${shownEnd(rows)}`);
|
|
246
|
+
const line = box.editor.map((row) => row.trim()).filter((row) => row !== '').join(' ⏎ ');
|
|
247
|
+
if (line !== '') throw new Error(`${it.name}: nothing was typed, because its input line is not empty: it reads "${line}".${shownEnd(rows)}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
239
250
|
if (it.harness !== 'codex') return;
|
|
240
251
|
const line = rows.findLast((row) => /^ *›/.test(row))?.trim();
|
|
241
252
|
if (!CODEX_EMPTY.includes(line)) {
|
|
@@ -286,22 +297,31 @@ export async function typeCommand(it, command, { before = () => {}, check = menu
|
|
|
286
297
|
takeBack();
|
|
287
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.`);
|
|
288
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
|
+
};
|
|
289
312
|
try {
|
|
290
313
|
for (const char of command) {
|
|
291
314
|
if (typed > 0) await lookAgain();
|
|
292
|
-
|
|
293
|
-
send(handle, char);
|
|
294
|
-
} catch (error) {
|
|
295
|
-
// A send Orca did not answer in time may have gone in all the same.
|
|
296
|
-
// Counted only where the line was empty before the first key, so the
|
|
297
|
-
// one backspace too many can take nothing of the user's (#480 review).
|
|
298
|
-
if (error.code === TIMED_OUT && it.harness === 'codex') typed += 1;
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
typed += 1;
|
|
315
|
+
typeKey(char);
|
|
302
316
|
}
|
|
303
317
|
await lookAgain();
|
|
304
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
|
+
}
|
|
305
325
|
if (wrong === undefined && Date.now() > deadline) wrong = { why: 'its time ran out' };
|
|
306
326
|
if (wrong !== undefined) {
|
|
307
327
|
takeBack();
|
|
@@ -373,7 +393,7 @@ function lookAt(it, { idle = true } = {}) {
|
|
|
373
393
|
}
|
|
374
394
|
if (found.agent !== it.harness) return { why: `Orca names ${found.agent} in it, and the session runs on ${it.harness}` };
|
|
375
395
|
// The gate has read the screen already: it saw no question on it.
|
|
376
|
-
const signal = signalIn(found.rows);
|
|
396
|
+
const signal = signalIn(found.rows, it.harness);
|
|
377
397
|
if (signal !== undefined) return { why: signal };
|
|
378
398
|
if (idle && !found.idle) return { why: 'it is busy with a turn: Orca\'s tui-idle did not answer ok' };
|
|
379
399
|
return { handle: found.handle, rows: found.rows, draft: found.draft };
|
|
@@ -394,7 +414,7 @@ async function typedWrong(handle, harness, command, version, check) {
|
|
|
394
414
|
const seen = screenRows(handle, READ_MS);
|
|
395
415
|
// The screen that lets the return in has to pass the look too: a turn or
|
|
396
416
|
// a question that shows on it stops it at once (review of c1e5ba4).
|
|
397
|
-
const signal = seen.rows === undefined ? undefined : signalIn(seen.rows);
|
|
417
|
+
const signal = seen.rows === undefined ? undefined : signalIn(seen.rows, harness);
|
|
398
418
|
if (signal !== undefined) return { why: signal, rows: seen.rows };
|
|
399
419
|
const wrong = seen.rows === undefined ? { why: `its screen could not be read (${seen.unreadable})` } : check(seen.rows, harness, command, version, seen.draft);
|
|
400
420
|
if (wrong === undefined || Date.now() >= until) return wrong && { ...wrong, rows: seen.rows };
|
|
@@ -409,9 +429,9 @@ async function typedWrong(handle, harness, command, version, check) {
|
|
|
409
429
|
*/
|
|
410
430
|
const PANEL_WHY = 'its panel of feedback drafts is open above its input box, and the kit does not answer it, since a draft sent from it goes to Anthropic: it clears once the user reviews, sends or dismisses the drafts in that tab';
|
|
411
431
|
|
|
412
|
-
/** Why `rows` say not to type now, as the look says it, or undefined: a turn at work, or a question. */
|
|
413
|
-
function signalIn(rows) {
|
|
414
|
-
const working = rows.find((row) => AT_WORK.some((marker) => marker.test(row)));
|
|
432
|
+
/** Why `rows` of `harness` say not to type now, as the look says it, or undefined: a turn at work, or a question. */
|
|
433
|
+
function signalIn(rows, harness) {
|
|
434
|
+
const working = rows.find((row) => AT_WORK.some((marker) => marker.test(row))) ?? piAtWorkIn(rows, harness);
|
|
415
435
|
if (working !== undefined) return `it is busy with a turn: its screen shows "${working.trim()}"`;
|
|
416
436
|
if (feedbackPanelIn(rows)) return PANEL_WHY;
|
|
417
437
|
if (questionIn(rows)) return 'a question of its harness\'s own is on its screen';
|
|
@@ -419,6 +439,7 @@ function signalIn(rows) {
|
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
function menuWrong(rows, harness, command, version, draft) {
|
|
442
|
+
if (harness === 'pi') return piMenuWrong(rows, command, draft);
|
|
422
443
|
const at = rows.findLastIndex((row) => /^ *[›❯]/.test(row));
|
|
423
444
|
if (at < 0) return { why: 'its screen shows no input line' };
|
|
424
445
|
// Claude Code 2.1.288 puts a non-breaking space after its pointer (live run 4).
|
|
@@ -478,6 +499,53 @@ function codexMenuWrong(rows, at, line, command, version) {
|
|
|
478
499
|
return undefined;
|
|
479
500
|
}
|
|
480
501
|
|
|
502
|
+
/**
|
|
503
|
+
* With the slash menu open, Return in Pi 1.3.0 runs the selected row's
|
|
504
|
+
* command (pi-tui's `editor.js`, `tui.select.confirm`; read in code, #663).
|
|
505
|
+
* So the editor's one row, or Orca's draft where it gives one, reads the
|
|
506
|
+
* command, and the selected row below the box names it. A menu still drawn
|
|
507
|
+
* for an earlier part of the command looks the same when its selected row is
|
|
508
|
+
* the command too, and this cannot tell it apart: PI_CLOSE does.
|
|
509
|
+
*/
|
|
510
|
+
function piMenuWrong(rows, command, draft) {
|
|
511
|
+
const box = piBox(rows);
|
|
512
|
+
if (box === undefined) return { why: 'its screen shows no input box' };
|
|
513
|
+
const lines = box.editor.map((row) => row.trim()).filter((row) => row !== '');
|
|
514
|
+
const line = draft ?? lines.join(' ⏎ ');
|
|
515
|
+
if (line !== command || (draft === undefined && lines.length !== 1)) return { why: `its input line reads "${line}"` };
|
|
516
|
+
const selected = box.below.find((row) => PI_SELECTED.test(row));
|
|
517
|
+
if (selected === undefined) return { why: 'no menu of commands came up below its input box', menu: true };
|
|
518
|
+
const name = selected.replace(PI_SELECTED, '').trim().split(/\s+/)[0];
|
|
519
|
+
if (`/${name}` !== command) return { why: `the selected row of its menu is "${selected.trim()}"`, menu: true };
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
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
|
+
|
|
481
549
|
/** The Codex version that wrote the rollout of `conversation`, from its session_meta line; undefined when that cannot be read. */
|
|
482
550
|
function codexVersion(conversation) {
|
|
483
551
|
const file = typeof conversation === 'string' ? codexRollout(conversation) : undefined;
|
|
@@ -517,6 +585,7 @@ async function answerWhereToRun(it, handle, command) {
|
|
|
517
585
|
/** Where the harness keeps the record of `conversation`, or undefined when it has none yet. */
|
|
518
586
|
function recordFile(harness, home, conversation) {
|
|
519
587
|
if (conversation === null) return undefined;
|
|
588
|
+
if (harness === 'pi') return piSession(home, conversation);
|
|
520
589
|
return harness === 'claude' ? claudeTranscript(home, conversation) : codexRollout(conversation);
|
|
521
590
|
}
|
|
522
591
|
|
|
@@ -549,9 +618,9 @@ function compactedSince(harness, record, since) {
|
|
|
549
618
|
} catch {
|
|
550
619
|
return false;
|
|
551
620
|
}
|
|
552
|
-
// Claude Code's marker, and
|
|
553
|
-
const compaction = harness === 'claude'
|
|
554
|
-
|
|
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'
|
|
555
624
|
: entry?.type === 'compacted';
|
|
556
625
|
found = compaction && Date.parse(entry.timestamp) >= since;
|
|
557
626
|
return found;
|
|
@@ -580,4 +649,4 @@ const SEND_MS = 5000;
|
|
|
580
649
|
/** Type `keys` into the tab as they are, with no return of Orca's. */
|
|
581
650
|
const send = (handle, keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys], { timeoutMs: SEND_MS });
|
|
582
651
|
|
|
583
|
-
const harnessName = (harness) => (
|
|
652
|
+
const harnessName = (harness) => ({ claude: 'Claude Code', codex: 'Codex', pi: 'Pi' })[harness];
|
package/src/cli.js
CHANGED
|
@@ -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.
|
|
@@ -122,7 +122,7 @@ Usage:
|
|
|
122
122
|
moves the folder only once Orca no longer lists
|
|
123
123
|
the project.
|
|
124
124
|
obk temp make --bots <path> --name <session> [--role <role>[:<option>]]
|
|
125
|
-
(--prompt <text> | --prompt-file <path>) [--harness claude|codex]
|
|
125
|
+
(--prompt <text> | --prompt-file <path>) [--harness claude|codex|pi]
|
|
126
126
|
[--model <m>] [--effort <e>] [--context <c>]
|
|
127
127
|
[--approval ${APPROVALS.join('|')}] [--extra-arg=<arg>]...
|
|
128
128
|
Run in a session's own tab: make a temporary
|
|
@@ -133,7 +133,9 @@ Usage:
|
|
|
133
133
|
you say otherwise; on another harness than yours
|
|
134
134
|
it takes only your approval. --approval is refused
|
|
135
135
|
wider than yours, unless obk permission approval
|
|
136
|
-
--temps allowed it for the bot.
|
|
136
|
+
--temps allowed it for the bot. On Pi it runs as
|
|
137
|
+
dangerously-skip, Pi's one level, so that holds
|
|
138
|
+
there too. It works in
|
|
137
139
|
work/<session>, and has the task as its start
|
|
138
140
|
prompt. The book records it as temporary, made by
|
|
139
141
|
you.
|
|
@@ -168,7 +170,8 @@ Usage:
|
|
|
168
170
|
"Teach auto mode" screen on a temporary session it
|
|
169
171
|
made with Not now, in a shape the kit knows, and
|
|
170
172
|
check the screen went. It refuses any other
|
|
171
|
-
screen, and types nothing then.
|
|
173
|
+
screen, and types nothing then. On a Pi session
|
|
174
|
+
it answers nothing, and says what the screen is.
|
|
172
175
|
obk rules build --bots <path> [--bot <bot>]
|
|
173
176
|
Build every bot's AGENTS.md from its charter and
|
|
174
177
|
the rule units it carries, or just the one you
|
|
@@ -607,8 +610,6 @@ async function record(bots, bot) {
|
|
|
607
610
|
|
|
608
611
|
const commands = {
|
|
609
612
|
async init(bots, values) {
|
|
610
|
-
// Bot Father on Pi is not in the first slice of #547.
|
|
611
|
-
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.');
|
|
612
613
|
// Asked before anything is written, so an Orca that is down leaves the disk
|
|
613
614
|
// exactly as it was and the caller can simply run the command again.
|
|
614
615
|
refuseWhenOrcaIsDown();
|
|
@@ -1840,6 +1841,11 @@ function rulesLines(rules, bots) {
|
|
|
1840
1841
|
...(entry.bytes > CODEX_CAP
|
|
1841
1842
|
? [` it is over the ${CODEX_CAP / 1024} KiB Codex reads, so a Codex session will not see all of it.`]
|
|
1842
1843
|
: []),
|
|
1844
|
+
// What applies to Pi and what does not, said wherever the rules are
|
|
1845
|
+
// reported, so the reader never takes the allow list for a limit (#659).
|
|
1846
|
+
...(entry.pi === true
|
|
1847
|
+
? [`${'pi'.padEnd(9)} ${entry.bot} its Pi sessions read AGENTS.md when they start. Pi has no permission rules and no hooks, so the kit writes neither for Pi: a Pi session runs as dangerously-skip, and the bot's allow list does not limit it.`]
|
|
1848
|
+
: []),
|
|
1843
1849
|
];
|
|
1844
1850
|
});
|
|
1845
1851
|
}
|
|
@@ -1853,6 +1859,7 @@ function skillsLines(skills) {
|
|
|
1853
1859
|
return skills.flatMap((entry) => [
|
|
1854
1860
|
entry.bot,
|
|
1855
1861
|
...(entry.trouble === undefined ? [] : [` ${'trouble'.padEnd(9)} ${entry.trouble}`]),
|
|
1862
|
+
...(entry.pi === undefined ? [] : [` ${'pi'.padEnd(9)} its Pi sessions load the skills in .agents/skills, which the kit names on each Pi launch line; a running Pi session takes a change at /reload`]),
|
|
1856
1863
|
...entry.skills.map((skill) => ` ${(skill.managed ? 'linked' : 'yours').padEnd(9)} ${skill.name.padEnd(24)} ${skill.from ?? 'no list names it; the kit leaves it alone'}`),
|
|
1857
1864
|
...(entry.removed ?? []).map((name) => ` ${'removed'.padEnd(9)} ${name.padEnd(24)} no list names it now`),
|
|
1858
1865
|
...(entry.sessions ?? []).map((one) => toldLine(entry.bot, one)),
|
|
@@ -1862,9 +1869,13 @@ function skillsLines(skills) {
|
|
|
1862
1869
|
/** What `skills build` did about one running session of a bot whose skills changed. */
|
|
1863
1870
|
function toldLine(bot, { session, harness, state, read, blocked, trouble }) {
|
|
1864
1871
|
const who = `${bot}/${session}`;
|
|
1865
|
-
const
|
|
1872
|
+
const reload = harness === 'pi' ? '/reload' : '/reload-skills';
|
|
1873
|
+
const after = harness === 'codex' ? 'Its next turn takes the change by itself.' : `Once that is settled, type ${reload} in its tab.`;
|
|
1866
1874
|
const [word, says] = {
|
|
1867
|
-
reloaded: ['reloaded',
|
|
1875
|
+
reloaded: ['reloaded', harness === 'pi'
|
|
1876
|
+
? '/reload was typed into its tab; Pi takes the change at its next turn'
|
|
1877
|
+
: '/reload-skills was typed into its tab; a busy session runs it when its turn ends'],
|
|
1878
|
+
busy: ['not told', 'it is busy, and Pi refuses /reload while it works, so nothing was typed. Once its turn ends, type /reload in its tab.'],
|
|
1868
1879
|
'next-turn': ['next turn', `Codex takes the change at the start of its next turn. If a skill is not there then, a restart makes it appear${read?.length > 0 ? `; meanwhile it can read ${read.join(', ')}` : ''}`],
|
|
1869
1880
|
'not-up': ['not told', 'not up; it reads its skills when it starts'],
|
|
1870
1881
|
blocked: ['not told', `its tab is waiting for an answer (${blocked}), so nothing was typed. ${after}`],
|
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
|
};
|