@assuredloop/orca-bot-kit 0.30.0 → 0.32.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 +18 -86
- package/SETUP.md +3 -3
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +5 -8
- package/src/bot.js +0 -20
- package/src/clear.js +50 -14
- package/src/cli.js +22 -23
- package/src/commands.js +5 -8
- package/src/health.js +75 -30
- package/src/launch.js +23 -0
- package/src/orca.js +104 -18
- package/src/permissions.js +35 -518
- package/src/pi-record.js +96 -0
- package/src/pi.js +55 -17
- package/src/processes.js +8 -9
- 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/src/up.js +2 -2
- package/src/run.js +0 -155
package/README.md
CHANGED
|
@@ -152,30 +152,37 @@ 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. `obk session compact` on Pi types nothing, and says so.
|
|
167
|
+
`obk init --harness pi` and `obk session compact` on Pi come later.
|
|
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,
|
|
166
171
|
`git add` and `git commit`
|
|
167
172
|
([ADR 0050](docs/adr/0050-the-kit-writes-every-bots-default-permission-rules.md)).
|
|
168
173
|
`obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
|
|
169
|
-
`obk bot change`, `obk session add`, `obk session change
|
|
170
|
-
|
|
174
|
+
`obk bot change`, `obk session add`, `obk session change` and `obk init` are
|
|
175
|
+
not in the set, and keep your yes
|
|
171
176
|
([ADR 0051](docs/adr/0051-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
|
|
172
177
|
A rule beyond the set is written by `obk permission allow`, after your yes to
|
|
173
178
|
the exact rule. A bot that still holds a rule that the kit gave every bot
|
|
174
179
|
before and that is no longer in the set keeps it: the rule of `obk restart`,
|
|
175
180
|
`obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
|
|
176
181
|
`obk session mail` or `obk session nudge`, or of Orca's
|
|
177
|
-
`orca orchestration check --run`.
|
|
178
|
-
the
|
|
182
|
+
`orca orchestration check --run`. So does a bot that holds a rule you allowed
|
|
183
|
+
for the kit's `run` command, which the kit no longer has. `obk rules build` and
|
|
184
|
+
`obk up` name each such rule, with the `obk permission disallow` command that
|
|
185
|
+
takes it back.
|
|
179
186
|
|
|
180
187
|
Neither command touches Orca. `obk up` is what makes it all real there: an Orca
|
|
181
188
|
project per bot, a tab per session, each started at the bot home with its own
|
|
@@ -680,82 +687,7 @@ fleet at all. Turn it off for a session with your own
|
|
|
680
687
|
the kit will report that session as out of reach of fleet mail rather than
|
|
681
688
|
failing quietly.
|
|
682
689
|
|
|
683
|
-
##
|
|
690
|
+
## Contributing
|
|
684
691
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
npm run test:system # what the system tests would drive, and nothing else
|
|
688
|
-
npm run test:system -- --yes # drive them, on this machine, for real
|
|
689
|
-
npm run test:system -- --yes test/system/groom.test.js # only the ones you name
|
|
690
|
-
npm run mutate # the mutation audit, on what your branch changed
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
The tests come in two layers, and every test file belongs to one of them.
|
|
694
|
-
|
|
695
|
-
`npm test` is `test/*.test.js` and `test/dev/*.test.js`: unit tests and
|
|
696
|
-
end-to-end runs of the CLI in a throwaway folder, with a fake `orca` on PATH. It
|
|
697
|
-
needs nothing but Node, so [GitHub Actions](.github/workflows/ci.yml) runs it on
|
|
698
|
-
every pull request, split into shards that run side by side, on Node 25.8.0, the
|
|
699
|
-
current line. A release runs every shard on 25.8.0 and again on 24.21.0, the
|
|
700
|
-
floor `engines.node` promises users, with only the kit's own dependencies
|
|
701
|
-
installed, before anything is published. The floor leaves out `test/dev/`, the
|
|
702
|
-
tests that need a development tool.
|
|
703
|
-
|
|
704
|
-
`npm run test:system` is `test/system/*.test.js`: the real `obk` against the
|
|
705
|
-
real Orca and the real harnesses on your own machine. No CI runner can do that,
|
|
706
|
-
so these are run by hand before a change that touches Orca or a harness is
|
|
707
|
-
merged. A system test touches only what it creates and removes it afterwards,
|
|
708
|
-
except the mailbox Runs Orca gives no way to delete, which the run names; it
|
|
709
|
-
never closes a tab it did not open. Before any test loads, the command copies
|
|
710
|
-
your `~/.claude.json` and `~/.codex/config.toml` into a new
|
|
711
|
-
`obk-config-backup-*` folder in your temp folder, readable by you alone, and
|
|
712
|
-
says where; if a copy fails, no test runs. After the run it says whether each
|
|
713
|
-
file still matches its copy, and never copies one back: that is yours to
|
|
714
|
-
decide. The trust its harnesses write into your `~/.codex/config.toml` and
|
|
715
|
-
`~/.claude.json` for the run's own throwaway folders is taken out again after
|
|
716
|
-
the run, and named; nothing else in them is changed.
|
|
717
|
-
An Orca project the run leaves in one of its own throwaway folders is named,
|
|
718
|
-
with its setup id, and fails the run; the command removes none itself.
|
|
719
|
-
|
|
720
|
-
They run on the machine the command is typed on, so the command will not start
|
|
721
|
-
them by itself. On its own it says what it is about to drive — whose machine,
|
|
722
|
-
which Orca, which files, and what they will do to it — and then stops without
|
|
723
|
-
driving any of it, answering non-zero so that a run which did not run them
|
|
724
|
-
cannot be read as one that passed. `-- --yes` is how you say you meant it. When
|
|
725
|
-
Orca is not answering, the command says which Orca it asked and skips, rather
|
|
726
|
-
than report a kit that is not broken. Name files after it to run only those;
|
|
727
|
-
each must be one of the `*.test.js` files under `test/system/`, links followed,
|
|
728
|
-
and a name that is not is refused before anything runs. This command is the
|
|
729
|
-
only way in: a system test loaded any other way, with `node --test` or an
|
|
730
|
-
editor's runner, skips and says how to run it.
|
|
731
|
-
|
|
732
|
-
`npm run mutate` runs [StrykerJS](https://stryker-mutator.io) over the
|
|
733
|
-
JavaScript this branch changed against `main` — committed, still in the working
|
|
734
|
-
tree, or not tracked yet — and reports the mutants the tests do not kill. It is
|
|
735
|
-
an audit, not a step of every pull request
|
|
736
|
-
([PRD 7.3](docs/prd/prd.md#prd-21-73-decided-rules-inside-the-skills)): the everyday
|
|
737
|
-
check is the implementer's own hand check, and the tool runs over the whole
|
|
738
|
-
suite at a milestone or when the owner asks, or once, narrowed to the changed
|
|
739
|
-
logic, for a change at the core of the kit. A surviving mutant is a change to
|
|
740
|
-
the code that no test objects to. It is worth a test only where it shows a gap
|
|
741
|
-
in behaviour the requirement cares about, and that test is written by the
|
|
742
|
-
separate test author. Name targets yourself to check a whole area instead:
|
|
743
|
-
|
|
744
|
-
```sh
|
|
745
|
-
npm run mutate -- 'src/**/*.js'
|
|
746
|
-
```
|
|
747
|
-
|
|
748
|
-
Every mutant means running the tests again, so the check does not run them the
|
|
749
|
-
way `npm test` does. It runs [`scripts/mutation-suite.js`](scripts/mutation-suite.js)
|
|
750
|
-
instead: the same test files, one at a time, stopping at the first file that
|
|
751
|
-
fails, trying the file that killed the last mutant first and then the quickest.
|
|
752
|
-
On a ten-core machine, `src/up.js` as it was when this was measured (128 lines,
|
|
753
|
-
76 mutants) took about twelve minutes that way. Running the whole suite for every mutant, as it used to, took
|
|
754
|
-
forty, and counted a mutant as killed whenever a run of it ran out of time. A
|
|
755
|
-
change across several files takes proportionally longer, so check one or two at
|
|
756
|
-
a time.
|
|
757
|
-
|
|
758
|
-
The check still takes minutes where the suite takes seconds, which is why it is
|
|
759
|
-
a command of its own. StrykerJS is a development dependency of this repo, pinned
|
|
760
|
-
to an exact version and needing Node 22 or newer; the kit itself neither ships
|
|
761
|
-
it nor needs it.
|
|
692
|
+
How to run the kit's own tests, and how they are laid out, is in
|
|
693
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). The package does not ship it.
|
package/SETUP.md
CHANGED
|
@@ -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, for a bot on Pi
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The Orca line must come back with `ok: true` and `result.runtime.reachable:
|
|
@@ -144,8 +144,8 @@ Read everything it prints. Three parts of it matter to them:
|
|
|
144
144
|
beside the bots folder, and its commits. The kit writes this set for every
|
|
145
145
|
bot, with nobody asked. Say in a sentence what it allows, and that
|
|
146
146
|
`obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
|
|
147
|
-
`obk bot change`, `obk session add`, `obk session change
|
|
148
|
-
|
|
147
|
+
`obk bot change`, `obk session add`, `obk session change` and `obk init` are
|
|
148
|
+
not in it and keep their yes;
|
|
149
149
|
- **anything it says about Orca's own default launch arguments.** Orca adds
|
|
150
150
|
these to every agent it launches, relaunches and resumes. When they carry a
|
|
151
151
|
permission bypass, every session runs in that mode whatever approval level the
|
package/package.json
CHANGED
|
@@ -214,12 +214,13 @@ does not have yet to its `bot.yaml` and its settings, or for Codex its
|
|
|
214
214
|
`.codex/rules/obk.rules`, and say which rules they wrote. The set leaves out
|
|
215
215
|
the commands that keep the user's yes: `obk permission`, `obk retire`, `obk
|
|
216
216
|
pause`, `obk restart`, `obk bot create`, `obk bot change`, `obk session add`,
|
|
217
|
-
`obk session change
|
|
217
|
+
`obk session change` and `obk init`. A bot that still holds a rule that the
|
|
218
218
|
kit gave every bot before and that is no longer in the set (`obk restart`,
|
|
219
219
|
`obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
|
|
220
220
|
`obk session mail`, `obk session nudge`, or Orca's `orca orchestration check
|
|
221
|
-
--run`) keeps it, and
|
|
222
|
-
|
|
221
|
+
--run`) keeps it, and so does a bot that holds a rule the user allowed for the
|
|
222
|
+
kit's `run` command, which the kit no longer has. `obk rules build` and `obk up`
|
|
223
|
+
name each such rule with the `obk permission disallow` command that takes it back; run that
|
|
223
224
|
command only after the user's yes. Never write a
|
|
224
225
|
permission rule into a settings or rules file by hand.
|
|
225
226
|
|
|
@@ -245,11 +246,7 @@ out; the kit refuses it, and it refuses a rule of its own default set too. Keep
|
|
|
245
246
|
each rule narrow: a program and what it does. The kit refuses a rule that lets
|
|
246
247
|
the bot run any command, a program with any arguments, or a shell or
|
|
247
248
|
interpreter with any arguments; a user who wants one for a bot adds it to that
|
|
248
|
-
bot's settings themselves.
|
|
249
|
-
with the real command after `--`, such as
|
|
250
|
-
`Bash(<kit> run --timeout 300 -- go test:*)`: the kit refuses a rule for
|
|
251
|
-
`perl -e 'alarm …'`, since perl runs whatever its arguments say. Propose no
|
|
252
|
-
rule for what the charter does not grant.
|
|
249
|
+
bot's settings themselves. Propose no rule for what the charter does not grant.
|
|
253
250
|
|
|
254
251
|
A bot that should learn across its sessions can use its harness's own memory,
|
|
255
252
|
turned on for that bot alone, whatever the user's own setting is for everything
|
package/src/bot.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// writes nothing, rather than leave a bot half made or a bot.yaml half edited.
|
|
9
9
|
|
|
10
10
|
import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import os from 'node:os';
|
|
12
11
|
import path from 'node:path';
|
|
13
12
|
import { isDeepStrictEqual } from 'node:util';
|
|
14
13
|
import { isSeq, parse, parseDocument, stringify } from 'yaml';
|
|
@@ -133,25 +132,6 @@ function whereItLeads(target) {
|
|
|
133
132
|
export const displayName = (name) =>
|
|
134
133
|
name.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
135
134
|
|
|
136
|
-
/**
|
|
137
|
-
* What Orca is asked to call a bot's project. A fleet whose bots folder is under
|
|
138
|
-
* the system's temp folder, as a system test's or a live check's is, gets a mark
|
|
139
|
-
* and the name of the first folder under the temp folder, so its Bot Father is
|
|
140
|
-
* never taken for the owner's in Orca's sidebar (#401). Every other fleet's
|
|
141
|
-
* project is called what it always was. Tab titles keep `displayName` either way.
|
|
142
|
-
*/
|
|
143
|
-
export function projectName(bots, name) {
|
|
144
|
-
let under;
|
|
145
|
-
try {
|
|
146
|
-
under = path.relative(realpathSync(os.tmpdir()), realpathSync(bots));
|
|
147
|
-
} catch {
|
|
148
|
-
return displayName(name);
|
|
149
|
-
}
|
|
150
|
-
// The temp folder itself, or anywhere outside it, is not a throwaway fleet's.
|
|
151
|
-
if (under === '' || under === '..' || under.startsWith(`..${path.sep}`) || path.isAbsolute(under)) return displayName(name);
|
|
152
|
-
return `${displayName(name)} · temp fleet ${under.split(path.sep)[0]}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
135
|
/**
|
|
156
136
|
* A bot's own file: which harness its sessions run on unless they say
|
|
157
137
|
* otherwise, and the sessions it keeps.
|
package/src/clear.js
CHANGED
|
@@ -19,12 +19,12 @@ import { botDir, readBot } from './bot.js';
|
|
|
19
19
|
import { claudeTranscript, codexRollout } 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' },
|
|
27
|
+
clear: { claude: '/clear', codex: '/new', pi: '/new' },
|
|
28
28
|
compact: { claude: '/compact', codex: '/compact' },
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -100,7 +100,13 @@ 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
|
*/
|
|
@@ -168,7 +174,7 @@ 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
|
|
177
|
+
* A compact is refused on Pi: a later slice of #547 may add it.
|
|
172
178
|
*/
|
|
173
179
|
export function sessionToType(bots, bot, session, verb) {
|
|
174
180
|
botsNamed(bots, bot);
|
|
@@ -176,7 +182,7 @@ export function sessionToType(bots, bot, session, verb) {
|
|
|
176
182
|
const known = readBot(home, bot);
|
|
177
183
|
const [settings] = sessionsOf(known, session);
|
|
178
184
|
const name = `${bot} ${session}`;
|
|
179
|
-
if (verb
|
|
185
|
+
if (verb === 'compact' && harnessOf(settings, known.harness) === 'pi') {
|
|
180
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.`);
|
|
181
187
|
}
|
|
182
188
|
const unpause = `${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${bot}`;
|
|
@@ -233,9 +239,16 @@ export async function typeCommand(it, command, { before = () => {}, check = menu
|
|
|
233
239
|
// Nothing may be in the input line before: no draft for the command to join.
|
|
234
240
|
// Orca gives a draft in the line beside the screen, and the screen's line
|
|
235
241
|
// then reads the pointer alone (#510, #516). Where it gives none, Codex's
|
|
236
|
-
// line on the screen has to be empty.
|
|
242
|
+
// line on the screen has to be empty, and so do Pi's editor rows.
|
|
237
243
|
const emptyLine = ({ rows, draft }) => {
|
|
238
244
|
if (draft !== undefined) throw new Error(`${it.name}: nothing was typed, because its input line holds a draft: it reads "${draft}".${shownEnd(rows)}`);
|
|
245
|
+
if (it.harness === 'pi') {
|
|
246
|
+
const box = piBox(rows);
|
|
247
|
+
if (box === undefined) throw new Error(`${it.name}: nothing was typed, because its screen shows no input box.${shownEnd(rows)}`);
|
|
248
|
+
const line = box.editor.map((row) => row.trim()).filter((row) => row !== '').join(' ⏎ ');
|
|
249
|
+
if (line !== '') throw new Error(`${it.name}: nothing was typed, because its input line is not empty: it reads "${line}".${shownEnd(rows)}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
239
252
|
if (it.harness !== 'codex') return;
|
|
240
253
|
const line = rows.findLast((row) => /^ *›/.test(row))?.trim();
|
|
241
254
|
if (!CODEX_EMPTY.includes(line)) {
|
|
@@ -295,7 +308,7 @@ export async function typeCommand(it, command, { before = () => {}, check = menu
|
|
|
295
308
|
// A send Orca did not answer in time may have gone in all the same.
|
|
296
309
|
// Counted only where the line was empty before the first key, so the
|
|
297
310
|
// one backspace too many can take nothing of the user's (#480 review).
|
|
298
|
-
if (error.code === TIMED_OUT && it.harness === 'codex') typed += 1;
|
|
311
|
+
if (error.code === TIMED_OUT && (it.harness === 'codex' || it.harness === 'pi')) typed += 1;
|
|
299
312
|
throw error;
|
|
300
313
|
}
|
|
301
314
|
typed += 1;
|
|
@@ -373,7 +386,7 @@ function lookAt(it, { idle = true } = {}) {
|
|
|
373
386
|
}
|
|
374
387
|
if (found.agent !== it.harness) return { why: `Orca names ${found.agent} in it, and the session runs on ${it.harness}` };
|
|
375
388
|
// The gate has read the screen already: it saw no question on it.
|
|
376
|
-
const signal = signalIn(found.rows);
|
|
389
|
+
const signal = signalIn(found.rows, it.harness);
|
|
377
390
|
if (signal !== undefined) return { why: signal };
|
|
378
391
|
if (idle && !found.idle) return { why: 'it is busy with a turn: Orca\'s tui-idle did not answer ok' };
|
|
379
392
|
return { handle: found.handle, rows: found.rows, draft: found.draft };
|
|
@@ -394,7 +407,7 @@ async function typedWrong(handle, harness, command, version, check) {
|
|
|
394
407
|
const seen = screenRows(handle, READ_MS);
|
|
395
408
|
// The screen that lets the return in has to pass the look too: a turn or
|
|
396
409
|
// a question that shows on it stops it at once (review of c1e5ba4).
|
|
397
|
-
const signal = seen.rows === undefined ? undefined : signalIn(seen.rows);
|
|
410
|
+
const signal = seen.rows === undefined ? undefined : signalIn(seen.rows, harness);
|
|
398
411
|
if (signal !== undefined) return { why: signal, rows: seen.rows };
|
|
399
412
|
const wrong = seen.rows === undefined ? { why: `its screen could not be read (${seen.unreadable})` } : check(seen.rows, harness, command, version, seen.draft);
|
|
400
413
|
if (wrong === undefined || Date.now() >= until) return wrong && { ...wrong, rows: seen.rows };
|
|
@@ -409,9 +422,9 @@ async function typedWrong(handle, harness, command, version, check) {
|
|
|
409
422
|
*/
|
|
410
423
|
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
424
|
|
|
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)));
|
|
425
|
+
/** Why `rows` of `harness` say not to type now, as the look says it, or undefined: a turn at work, or a question. */
|
|
426
|
+
function signalIn(rows, harness) {
|
|
427
|
+
const working = rows.find((row) => AT_WORK.some((marker) => marker.test(row))) ?? piAtWorkIn(rows, harness);
|
|
415
428
|
if (working !== undefined) return `it is busy with a turn: its screen shows "${working.trim()}"`;
|
|
416
429
|
if (feedbackPanelIn(rows)) return PANEL_WHY;
|
|
417
430
|
if (questionIn(rows)) return 'a question of its harness\'s own is on its screen';
|
|
@@ -419,6 +432,7 @@ function signalIn(rows) {
|
|
|
419
432
|
}
|
|
420
433
|
|
|
421
434
|
function menuWrong(rows, harness, command, version, draft) {
|
|
435
|
+
if (harness === 'pi') return piMenuWrong(rows, command, draft);
|
|
422
436
|
const at = rows.findLastIndex((row) => /^ *[›❯]/.test(row));
|
|
423
437
|
if (at < 0) return { why: 'its screen shows no input line' };
|
|
424
438
|
// Claude Code 2.1.288 puts a non-breaking space after its pointer (live run 4).
|
|
@@ -478,6 +492,28 @@ function codexMenuWrong(rows, at, line, command, version) {
|
|
|
478
492
|
return undefined;
|
|
479
493
|
}
|
|
480
494
|
|
|
495
|
+
/**
|
|
496
|
+
* With the slash menu open, Return in Pi 1.3.0 runs the selected row's
|
|
497
|
+
* command (pi-tui's `editor.js`, `tui.select.confirm`; read in code, #663).
|
|
498
|
+
* So the editor's one row, or Orca's draft where it gives one, reads the
|
|
499
|
+
* command, and the selected row below the box names it. A menu still drawn
|
|
500
|
+
* 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.
|
|
503
|
+
*/
|
|
504
|
+
function piMenuWrong(rows, command, draft) {
|
|
505
|
+
const box = piBox(rows);
|
|
506
|
+
if (box === undefined) return { why: 'its screen shows no input box' };
|
|
507
|
+
const lines = box.editor.map((row) => row.trim()).filter((row) => row !== '');
|
|
508
|
+
const line = draft ?? lines.join(' ⏎ ');
|
|
509
|
+
if (line !== command || (draft === undefined && lines.length !== 1)) return { why: `its input line reads "${line}"` };
|
|
510
|
+
const selected = box.below.find((row) => PI_SELECTED.test(row));
|
|
511
|
+
if (selected === undefined) return { why: 'no menu of commands came up below its input box', menu: true };
|
|
512
|
+
const name = selected.replace(PI_SELECTED, '').trim().split(/\s+/)[0];
|
|
513
|
+
if (`/${name}` !== command) return { why: `the selected row of its menu is "${selected.trim()}"`, menu: true };
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
481
517
|
/** The Codex version that wrote the rollout of `conversation`, from its session_meta line; undefined when that cannot be read. */
|
|
482
518
|
function codexVersion(conversation) {
|
|
483
519
|
const file = typeof conversation === 'string' ? codexRollout(conversation) : undefined;
|
|
@@ -580,4 +616,4 @@ const SEND_MS = 5000;
|
|
|
580
616
|
/** Type `keys` into the tab as they are, with no return of Orca's. */
|
|
581
617
|
const send = (handle, keys) => orca(['terminal', 'send', '--terminal', handle, '--text', keys], { timeoutMs: SEND_MS });
|
|
582
618
|
|
|
583
|
-
const harnessName = (harness) => (
|
|
619
|
+
const harnessName = (harness) => ({ claude: 'Claude Code', codex: 'Codex', pi: 'Pi' })[harness];
|
package/src/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import { nameSession } from './name.js';
|
|
|
21
21
|
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
22
22
|
import { checkMail, lookUp, noMailboxYet, sendMessage, sessionInTab } from './message.js';
|
|
23
23
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
24
|
-
import { allowIn, beyondDefaults, botDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
24
|
+
import { allowIn, beyondDefaults, botDefaults, isRunRule, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
25
25
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
26
26
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
27
27
|
import { restartSessions } from './restart.js';
|
|
@@ -34,7 +34,6 @@ import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
|
34
34
|
import { addSource, fetchSources } from './sources.js';
|
|
35
35
|
import { answerSession, answerTemp, listRoles, makeTemp, optionsLine, retireTemp, trustHooks, trustSessionHooks } from './temp.js';
|
|
36
36
|
import { readUsage } from './usage.js';
|
|
37
|
-
import { parseRun, runBounded } from './run.js';
|
|
38
37
|
import { BOT_FATHER, bringUp, ownMailbox } from './up.js';
|
|
39
38
|
|
|
40
39
|
const USAGE = `obk — Orca Bot Kit.
|
|
@@ -123,7 +122,7 @@ Usage:
|
|
|
123
122
|
moves the folder only once Orca no longer lists
|
|
124
123
|
the project.
|
|
125
124
|
obk temp make --bots <path> --name <session> [--role <role>[:<option>]]
|
|
126
|
-
(--prompt <text> | --prompt-file <path>) [--harness claude|codex]
|
|
125
|
+
(--prompt <text> | --prompt-file <path>) [--harness claude|codex|pi]
|
|
127
126
|
[--model <m>] [--effort <e>] [--context <c>]
|
|
128
127
|
[--approval ${APPROVALS.join('|')}] [--extra-arg=<arg>]...
|
|
129
128
|
Run in a session's own tab: make a temporary
|
|
@@ -134,7 +133,9 @@ Usage:
|
|
|
134
133
|
you say otherwise; on another harness than yours
|
|
135
134
|
it takes only your approval. --approval is refused
|
|
136
135
|
wider than yours, unless obk permission approval
|
|
137
|
-
--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
|
|
138
139
|
work/<session>, and has the task as its start
|
|
139
140
|
prompt. The book records it as temporary, made by
|
|
140
141
|
you.
|
|
@@ -169,7 +170,8 @@ Usage:
|
|
|
169
170
|
"Teach auto mode" screen on a temporary session it
|
|
170
171
|
made with Not now, in a shape the kit knows, and
|
|
171
172
|
check the screen went. It refuses any other
|
|
172
|
-
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.
|
|
173
175
|
obk rules build --bots <path> [--bot <bot>]
|
|
174
176
|
Build every bot's AGENTS.md from its charter and
|
|
175
177
|
the rule units it carries, or just the one you
|
|
@@ -327,22 +329,13 @@ Usage:
|
|
|
327
329
|
For the kit's own launch line, not for typing: run
|
|
328
330
|
in the session's own tab, it gives the session its
|
|
329
331
|
mailbox, or binds the one it has, to that tab.
|
|
330
|
-
obk run --timeout <seconds> -- <command> [<arg> ...]
|
|
331
|
-
Run one command with a hard time limit, with no
|
|
332
|
-
shell, and exit with its exit code. When the time
|
|
333
|
-
runs out, it stops the command and everything it
|
|
334
|
-
started (SIGTERM, then SIGKILL after 3 s), says so
|
|
335
|
-
on stderr, and exits 124. A command it cannot find
|
|
336
|
-
exits 127. A rule can allow it for one command,
|
|
337
|
-
such as Bash(obk run --timeout 300 -- go test:*).
|
|
338
|
-
It needs no --bots, and takes no --json.
|
|
339
332
|
obk --version Print the kit's version.
|
|
340
333
|
obk --help Print this text.
|
|
341
334
|
|
|
342
335
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
343
336
|
that close a tab or take something away say so above. Every bot is allowed
|
|
344
|
-
the kit's commands by default, except obk permission, retire, pause
|
|
345
|
-
|
|
337
|
+
the kit's commands by default, except obk permission, retire, pause and init,
|
|
338
|
+
which keep the user's yes.
|
|
346
339
|
Add --json to any of them for the same answer as JSON.
|
|
347
340
|
|
|
348
341
|
For another build of Orca, OBK_ORCA names its CLI, and OBK_ORCA_DATA the full
|
|
@@ -396,10 +389,6 @@ function version() {
|
|
|
396
389
|
}
|
|
397
390
|
|
|
398
391
|
async function run(argv) {
|
|
399
|
-
// What follows `obk run --timeout <n> --` is the command's own, flags and
|
|
400
|
-
// all, so it is read by `run` alone, before the kit reads its flags (#532).
|
|
401
|
-
if (argv[0] === 'run') return runBounded(parseRun(argv.slice(1)));
|
|
402
|
-
|
|
403
392
|
const { values, positionals, tokens } = parseArgs({
|
|
404
393
|
args: argv,
|
|
405
394
|
allowPositionals: true,
|
|
@@ -1819,7 +1808,7 @@ function permissionsLines(permissions, bots) {
|
|
|
1819
1808
|
...(entry.trouble === undefined ? [] : [`${'refused'.padEnd(9)} ${entry.trouble}`]),
|
|
1820
1809
|
...(listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1821
1810
|
...(entry.defaults ?? []).map((rule) => `${'allowed'.padEnd(9)} ${entry.bot} ${rule}`),
|
|
1822
|
-
...(entry.held ?? []).map((rule) => `${'holds'.padEnd(9)} ${entry.bot} ${rule}, which is no longer one of the kit's defaults. To take it back: ${disallow(entry.bot, rule)}`),
|
|
1811
|
+
...(entry.held ?? []).map((rule) => `${'holds'.padEnd(9)} ${entry.bot} ${rule}, ${isRunRule(rule) ? 'which is for obk run, a command the kit no longer has' : "which is no longer one of the kit's defaults"}. To take it back: ${disallow(entry.bot, rule)}`),
|
|
1823
1812
|
]),
|
|
1824
1813
|
...(entry.written.length === 0
|
|
1825
1814
|
? []
|
|
@@ -1854,6 +1843,11 @@ function rulesLines(rules, bots) {
|
|
|
1854
1843
|
...(entry.bytes > CODEX_CAP
|
|
1855
1844
|
? [` it is over the ${CODEX_CAP / 1024} KiB Codex reads, so a Codex session will not see all of it.`]
|
|
1856
1845
|
: []),
|
|
1846
|
+
// What applies to Pi and what does not, said wherever the rules are
|
|
1847
|
+
// reported, so the reader never takes the allow list for a limit (#659).
|
|
1848
|
+
...(entry.pi === true
|
|
1849
|
+
? [`${'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.`]
|
|
1850
|
+
: []),
|
|
1857
1851
|
];
|
|
1858
1852
|
});
|
|
1859
1853
|
}
|
|
@@ -1867,6 +1861,7 @@ function skillsLines(skills) {
|
|
|
1867
1861
|
return skills.flatMap((entry) => [
|
|
1868
1862
|
entry.bot,
|
|
1869
1863
|
...(entry.trouble === undefined ? [] : [` ${'trouble'.padEnd(9)} ${entry.trouble}`]),
|
|
1864
|
+
...(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`]),
|
|
1870
1865
|
...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'}`),
|
|
1871
1866
|
...(entry.removed ?? []).map((name) => ` ${'removed'.padEnd(9)} ${name.padEnd(24)} no list names it now`),
|
|
1872
1867
|
...(entry.sessions ?? []).map((one) => toldLine(entry.bot, one)),
|
|
@@ -1876,9 +1871,13 @@ function skillsLines(skills) {
|
|
|
1876
1871
|
/** What `skills build` did about one running session of a bot whose skills changed. */
|
|
1877
1872
|
function toldLine(bot, { session, harness, state, read, blocked, trouble }) {
|
|
1878
1873
|
const who = `${bot}/${session}`;
|
|
1879
|
-
const
|
|
1874
|
+
const reload = harness === 'pi' ? '/reload' : '/reload-skills';
|
|
1875
|
+
const after = harness === 'codex' ? 'Its next turn takes the change by itself.' : `Once that is settled, type ${reload} in its tab.`;
|
|
1880
1876
|
const [word, says] = {
|
|
1881
|
-
reloaded: ['reloaded',
|
|
1877
|
+
reloaded: ['reloaded', harness === 'pi'
|
|
1878
|
+
? '/reload was typed into its tab; Pi takes the change at its next turn'
|
|
1879
|
+
: '/reload-skills was typed into its tab; a busy session runs it when its turn ends'],
|
|
1880
|
+
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.'],
|
|
1882
1881
|
'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(', ')}` : ''}`],
|
|
1883
1882
|
'not-up': ['not told', 'not up; it reads its skills when it starts'],
|
|
1884
1883
|
blocked: ['not told', `its tab is waiting for an answer (${blocked}), so nothing was typed. ${after}`],
|
package/src/commands.js
CHANGED
|
@@ -46,7 +46,6 @@ export const COMMANDS = {
|
|
|
46
46
|
'temp retire': ['bots', 'name'],
|
|
47
47
|
'temp trust-hooks': ['bots', 'name'],
|
|
48
48
|
'temp answer': ['bots', 'name'],
|
|
49
|
-
run: ['timeout'],
|
|
50
49
|
};
|
|
51
50
|
|
|
52
51
|
/**
|
|
@@ -69,14 +68,12 @@ export const DEFAULT_COMMANDS = [
|
|
|
69
68
|
* The commands kept out of the default permission set, which keep the user's
|
|
70
69
|
* yes (ADR 0051): the ones that change permissions, close a long-lived
|
|
71
70
|
* session, make the bots folder, or make or change a bot or its sessions,
|
|
72
|
-
* which the owner leaves to Bot Father (#548, #615)
|
|
73
|
-
* whatever its arguments say, so a rule for it names its command (#532).
|
|
71
|
+
* which the owner leaves to Bot Father (#548, #615).
|
|
74
72
|
*/
|
|
75
73
|
export const KEPT_BACK = [
|
|
76
74
|
'init', 'retire', 'pause', 'restart', 'bot create',
|
|
77
75
|
'bot change', 'session add', 'session change',
|
|
78
76
|
'permission allow', 'permission disallow', 'permission approval',
|
|
79
|
-
'run',
|
|
80
77
|
];
|
|
81
78
|
|
|
82
79
|
/**
|
|
@@ -87,8 +84,8 @@ export const KEPT_BACK = [
|
|
|
87
84
|
export const NO_LONGER_DEFAULT = ['restart', 'bot create', 'bot change', 'session add', 'session change'];
|
|
88
85
|
|
|
89
86
|
/**
|
|
90
|
-
* The commands kit 0.26.0 gave every bot by
|
|
91
|
-
*
|
|
92
|
-
* (#564).
|
|
87
|
+
* The commands the kit no longer has: the two kit 0.26.0 gave every bot by
|
|
88
|
+
* default (#555), and `run` (#532), which #652 took out of the kit. A bot may
|
|
89
|
+
* still hold their rules, which the kit names and leaves (#564).
|
|
93
90
|
*/
|
|
94
|
-
export const REMOVED = ['session mail', 'session nudge'];
|
|
91
|
+
export const REMOVED = ['session mail', 'session nudge', 'run'];
|