@assuredloop/orca-bot-kit 0.32.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 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 Codex. The kit is an npm package with
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
- two harnesses is in [`docs/tech-notes.md`](docs/tech-notes.md).
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 either harness, and builds each bot's `AGENTS.md`
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 Codex
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 is
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. `obk session compact` on Pi types nothing, and says so.
167
- `obk init --harness pi` and `obk session compact` on Pi come later.
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 (add
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 Codex. You are making the first bot, **Bot Father**, and a folder
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, for a bot on Pi
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 neither harness is installed, say so and stop. If only one is, that is fine,
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 `codex`. There is no default
125
- and the kit will not guess. It is Bot Father's own harness only; each bot they
126
- make later chooses its own. If only one harness is installed, say so and confirm
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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, then
246
- `obk up`. Its launch line carries them, so they are what every run uses.
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, 'clear');
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, 'compact');
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, verb) {
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; past `deadline`, a time, what was typed is taken back
235
- * and nothing is entered. Returns the command, the handle it went into, and
236
- * when it was typed.
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
- try {
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: the kit's gap between keys and its reads make that
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 Codex's (tech notes, sections 2 and 3).
589
- const compaction = harness === 'claude'
590
- ? entry?.type === 'system' && entry.subtype === 'compact_boundary'
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
@@ -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();
@@ -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 codex --version; and the operating system.
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: []