@assuredloop/orca-bot-kit 0.29.0 → 0.31.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 +9 -82
- package/SETUP.md +4 -4
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +8 -9
- package/src/bot.js +0 -20
- package/src/cli.js +4 -18
- package/src/commands.js +5 -8
- package/src/orca.js +12 -12
- package/src/permissions.js +21 -38
- package/src/processes.js +8 -9
- package/src/temp.js +35 -35
- package/src/up.js +2 -2
- package/src/run.js +0 -155
package/README.md
CHANGED
|
@@ -166,16 +166,18 @@ nobody asked: its own commands for this bots folder, reading a long message,
|
|
|
166
166
|
`git add` and `git commit`
|
|
167
167
|
([ADR 0050](docs/adr/0050-the-kit-writes-every-bots-default-permission-rules.md)).
|
|
168
168
|
`obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
|
|
169
|
-
`obk bot change`, `obk session add`, `obk session change
|
|
170
|
-
|
|
169
|
+
`obk bot change`, `obk session add`, `obk session change` and `obk init` are
|
|
170
|
+
not in the set, and keep your yes
|
|
171
171
|
([ADR 0051](docs/adr/0051-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
|
|
172
172
|
A rule beyond the set is written by `obk permission allow`, after your yes to
|
|
173
173
|
the exact rule. A bot that still holds a rule that the kit gave every bot
|
|
174
174
|
before and that is no longer in the set keeps it: the rule of `obk restart`,
|
|
175
175
|
`obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
|
|
176
176
|
`obk session mail` or `obk session nudge`, or of Orca's
|
|
177
|
-
`orca orchestration check --run`.
|
|
178
|
-
the
|
|
177
|
+
`orca orchestration check --run`. So does a bot that holds a rule you allowed
|
|
178
|
+
for the kit's `run` command, which the kit no longer has. `obk rules build` and
|
|
179
|
+
`obk up` name each such rule, with the `obk permission disallow` command that
|
|
180
|
+
takes it back.
|
|
179
181
|
|
|
180
182
|
Neither command touches Orca. `obk up` is what makes it all real there: an Orca
|
|
181
183
|
project per bot, a tab per session, each started at the bot home with its own
|
|
@@ -680,82 +682,7 @@ fleet at all. Turn it off for a session with your own
|
|
|
680
682
|
the kit will report that session as out of reach of fleet mail rather than
|
|
681
683
|
failing quietly.
|
|
682
684
|
|
|
683
|
-
##
|
|
685
|
+
## Contributing
|
|
684
686
|
|
|
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.
|
|
687
|
+
How to run the kit's own tests, and how they are laid out, is in
|
|
688
|
+
[CONTRIBUTING.md](CONTRIBUTING.md). The package does not ship it.
|
package/SETUP.md
CHANGED
|
@@ -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
|
|
@@ -218,7 +218,7 @@ answers that screen, by the command and its arguments below the table.
|
|
|
218
218
|
| Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home); on 0.162.0 it is called **1. Use current Git worktree** (Keep using the current working directory). Never **Create new Git worktree** | Codex 0.160.0; Codex 0.162.0 (#516) |
|
|
219
219
|
| Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; seen on 0.157.1 (offering 0.158.0) with the selection on **Update now**, not pressed; 0.158.0 offered 0.160.0, taken on 2026-10-02 by Bot Father's restarts |
|
|
220
220
|
| `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
|
|
221
|
-
| Claude Code's `Teach auto mode about your environment?` | `obk session answer`, or `obk temp answer` for a temporary session | the kit sends Esc, which cancels it (**Not now**). On 2.1.283 and 2.1.302 it is a form, not a list: `How you use Claude here`, `Also scan shell history`, `Also scan your other repos`, `Continue`, and `Enter to continue · Esc to cancel`. On 2.1.283
|
|
221
|
+
| Claude Code's `Teach auto mode about your environment?` | `obk session answer`, or `obk temp answer` for a temporary session | the kit sends Esc, which cancels it (**Not now**). On 2.1.283 and 2.1.302 it is a form, not a list: `How you use Claude here`, `Also scan shell history`, `Also scan your other repos`, `Continue`, and `Enter to continue · Esc to cancel`. On 2.1.283 the foot starts `←/→ to change usage`, and on 2.1.302 it starts `←/→ to change`. The values after the three settings can differ: `Also scan shell history` showed `true` on 2.1.283, and both `false` and `true` on 2.1.302. The kit sends Esc whatever the values are. **Never send a return**: Enter is **Continue**, which starts a scan of the project, recent Claude sessions and, by default, the machine's shell history | Not yet proven: Esc cancels in Claude Code 2.1.283's code, not yet seen live (#370); the form was seen live twice on 2.1.283, in #261's live test and #239's live run 1 (#416); 2.1.278 showed a numbered list, answered with 2. Not now; the 2.1.302 form was seen live twice, in #604 and in #646, where it showed `Also scan shell history` on `true` and one Esc sent by hand removed it |
|
|
222
222
|
| Claude Code 2.1.289's `Teach auto mode about your environment?`, a numbered list: `1. Yes`, `2. Not now`, `3. Don't show again`, and `Enter to confirm · Esc to cancel`, above the input box | `obk session answer`, or `obk temp answer` for a temporary session | the kit goes down, then returns, to **2. Not now**: its selection starts on **1. Yes**. The kit reads the screen after the down arrow, and sends the return only with the selection on **2. Not now**. Never **1. Yes**, which starts the teach scan, and never **3. Don't show again** | Not yet proven: the list was seen live on 2.1.289 with its selection on 1. Yes, in #489's live run 1, and was not answered there |
|
|
223
223
|
|
|
224
224
|
Each key counts on where the selection starts, as it did on the version named.
|
|
@@ -248,7 +248,7 @@ rule to type into any tab. On Codex's hooks review, the maker runs
|
|
|
248
248
|
`Teach auto mode` screen, it runs `obk temp answer --bots <their path> --name <session>`.
|
|
249
249
|
That command reads the screen and answers only when it holds one of the
|
|
250
250
|
shapes in the table exactly as the kit knows it: Esc for the 2.1.283 and
|
|
251
|
-
2.1.302 forms, and
|
|
251
|
+
2.1.302 forms, whatever values their settings show, and
|
|
252
252
|
**2. Not now** for the 2.1.289 list. On the list it sends the arrows, reads the
|
|
253
253
|
screen again, and sends the return only with the selection on **2. Not now**.
|
|
254
254
|
It refuses any other screen, types nothing then, and prints what it saw.
|
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
|
|
@@ -328,7 +325,9 @@ numbered list, answered with `2. Not now`, and on 2.1.289 it is one again:
|
|
|
328
325
|
selection on 1. Yes (seen live, not answered, in #489). The kit goes down to
|
|
329
326
|
2. Not now, reads the screen, and only then returns. On 2.1.302 it is the
|
|
330
327
|
form again, with `Also scan shell history` on `false` and a foot that starts
|
|
331
|
-
`←/→ to change` (seen live in #604)
|
|
328
|
+
`←/→ to change` (seen live in #604), and later with it on `true` (#646). The
|
|
329
|
+
values of its three settings can differ, and the kit sends it Esc whatever
|
|
330
|
+
they are. Codex's hooks
|
|
332
331
|
question matters most. The kit's hook is how the book learns which
|
|
333
332
|
conversation the session is running, and until it is answered the
|
|
334
333
|
conversation has not started. Codex's trust applies to the repository root,
|
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/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.
|
|
@@ -327,22 +326,13 @@ Usage:
|
|
|
327
326
|
For the kit's own launch line, not for typing: run
|
|
328
327
|
in the session's own tab, it gives the session its
|
|
329
328
|
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
329
|
obk --version Print the kit's version.
|
|
340
330
|
obk --help Print this text.
|
|
341
331
|
|
|
342
332
|
Every command is safe to run again. Most only add what is missing; the ones
|
|
343
333
|
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
|
-
|
|
334
|
+
the kit's commands by default, except obk permission, retire, pause and init,
|
|
335
|
+
which keep the user's yes.
|
|
346
336
|
Add --json to any of them for the same answer as JSON.
|
|
347
337
|
|
|
348
338
|
For another build of Orca, OBK_ORCA names its CLI, and OBK_ORCA_DATA the full
|
|
@@ -396,10 +386,6 @@ function version() {
|
|
|
396
386
|
}
|
|
397
387
|
|
|
398
388
|
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
389
|
const { values, positionals, tokens } = parseArgs({
|
|
404
390
|
args: argv,
|
|
405
391
|
allowPositionals: true,
|
|
@@ -1819,7 +1805,7 @@ function permissionsLines(permissions, bots) {
|
|
|
1819
1805
|
...(entry.trouble === undefined ? [] : [`${'refused'.padEnd(9)} ${entry.trouble}`]),
|
|
1820
1806
|
...(listed.has(entry.bot) || !listed.add(entry.bot) ? [] : [
|
|
1821
1807
|
...(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)}`),
|
|
1808
|
+
...(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
1809
|
]),
|
|
1824
1810
|
...(entry.written.length === 0
|
|
1825
1811
|
? []
|
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'];
|
package/src/orca.js
CHANGED
|
@@ -381,8 +381,8 @@ function askRuntime(method, params, killMs = CLIENT_KILL_MS) {
|
|
|
381
381
|
|
|
382
382
|
const RUNTIME_SCRIPT = fileURLToPath(new URL('./orca-runtime.cjs', import.meta.url));
|
|
383
383
|
|
|
384
|
-
/** The `osascript`
|
|
385
|
-
const
|
|
384
|
+
/** The `osascript` the kit asks, by its fixed path: no setting changes it (#652). */
|
|
385
|
+
const OSASCRIPT = '/usr/bin/osascript';
|
|
386
386
|
|
|
387
387
|
/** How long macOS is given to click the menu item. */
|
|
388
388
|
const RELOAD_KILL_MS = 5000;
|
|
@@ -406,7 +406,7 @@ export function reloadWindow() {
|
|
|
406
406
|
try {
|
|
407
407
|
// <Orca.app>/Contents/Resources/bin/orca, often reached through a link.
|
|
408
408
|
const app = path.resolve(realpathSync(orcaCli()), '..', '..', '..', '..');
|
|
409
|
-
const asked = spawnSync(
|
|
409
|
+
const asked = spawnSync(OSASCRIPT, [RELOAD_SCRIPT, app], {
|
|
410
410
|
encoding: 'utf8',
|
|
411
411
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
412
412
|
timeout: RELOAD_KILL_MS,
|
|
@@ -741,8 +741,8 @@ export function frontOfTab(handle) {
|
|
|
741
741
|
return frontOf(handle, shown?.ptyId);
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
-
/** The `ps`
|
|
745
|
-
export const
|
|
744
|
+
/** The `ps` the kit reads, by its fixed path: no setting changes it (#652). */
|
|
745
|
+
export const PS = '/bin/ps';
|
|
746
746
|
|
|
747
747
|
/**
|
|
748
748
|
* Who holds the terminal of the tab `handle`, whose pane is `ptyId`:
|
|
@@ -838,9 +838,9 @@ function panePid(ptyId, readMs) {
|
|
|
838
838
|
return { pane };
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
-
/** The `lsof` and `stty`
|
|
842
|
-
export const
|
|
843
|
-
const
|
|
841
|
+
/** The `lsof` and `stty` the kit asks, by their fixed paths, as `ps`. */
|
|
842
|
+
export const LSOF = '/usr/sbin/lsof';
|
|
843
|
+
const STTY = '/bin/stty';
|
|
844
844
|
|
|
845
845
|
/**
|
|
846
846
|
* One look at the shell of a tab the kit has just opened, before anything is
|
|
@@ -893,7 +893,7 @@ export function shellInTab(handle, until) {
|
|
|
893
893
|
* or as the pane itself where the pane is the shell.
|
|
894
894
|
*/
|
|
895
895
|
function ttyMode(pid, left) {
|
|
896
|
-
const listed = spawnSync(
|
|
896
|
+
const listed = spawnSync(LSOF, ['-a', '-R', '-d', '0', '-u', String(process.getuid()), '-FpRn'], { encoding: 'utf8', timeout: left() });
|
|
897
897
|
if (listed.error?.code === 'ETIMEDOUT') return { unreadable: 'lsof did not answer in time', late: true };
|
|
898
898
|
if (listed.error || listed.status !== 0) return { unreadable: `lsof could not list the processes' ttys` };
|
|
899
899
|
// One record per process: `p<pid>`, `R<parent pid>`, `f0`, `n<name>`.
|
|
@@ -906,7 +906,7 @@ function ttyMode(pid, left) {
|
|
|
906
906
|
}
|
|
907
907
|
if (tty === undefined) return { unreadable: `lsof named no tty for the shell of its pane, pid ${pid}` };
|
|
908
908
|
|
|
909
|
-
const read = spawnSync(
|
|
909
|
+
const read = spawnSync(STTY, ['-a', '-f', tty], { encoding: 'utf8', timeout: left() });
|
|
910
910
|
if (read.error?.code === 'ETIMEDOUT') return { unreadable: `stty did not answer in time for ${tty}`, late: true };
|
|
911
911
|
if (read.error || read.status !== 0) return { unreadable: `stty could not read ${tty}` };
|
|
912
912
|
const words = read.stdout.split(/\s+/);
|
|
@@ -926,14 +926,14 @@ function ttyMode(pid, left) {
|
|
|
926
926
|
* looks for one whole word it knows.
|
|
927
927
|
*/
|
|
928
928
|
export function wordsOfProcess(pid) {
|
|
929
|
-
const asked = spawnSync(
|
|
929
|
+
const asked = spawnSync(PS, ['-E', '-ww', '-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
930
930
|
if (asked.error || asked.status !== 0) return undefined;
|
|
931
931
|
return asked.stdout.trim().split(/\s+/);
|
|
932
932
|
}
|
|
933
933
|
|
|
934
934
|
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, `{ late: true }` past `readMs`, or undefined. */
|
|
935
935
|
function psLine(pid, readMs) {
|
|
936
|
-
const asked = spawnSync(
|
|
936
|
+
const asked = spawnSync(PS, ['-o', 'pid=,ppid=,tpgid=,comm=', '-p', String(pid)], { encoding: 'utf8', ...(readMs === undefined ? {} : { timeout: readMs }) });
|
|
937
937
|
if (asked.error?.code === 'ETIMEDOUT') return { late: true };
|
|
938
938
|
if (asked.error || asked.status !== 0) return undefined;
|
|
939
939
|
const line = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(.+?)\s*$/.exec(asked.stdout);
|
package/src/permissions.js
CHANGED
|
@@ -59,18 +59,34 @@ export function defaultRules(bots, cli = ownCli()) {
|
|
|
59
59
|
*/
|
|
60
60
|
const ORCA_CHECK = 'Bash(orca orchestration check --run:*)';
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether a rule is one for `obk run` (#532), which #652 took out of the kit:
|
|
64
|
+
* the kit, then `run`. The kit is its CLI as `permission allow` recorded it,
|
|
65
|
+
* the bare `obk` that `obk run`'s own help spelled, or any path to an `obk`,
|
|
66
|
+
* quoted or not. A rule for it named the command after `--`, so it is known by
|
|
67
|
+
* its first two shell words.
|
|
68
|
+
*/
|
|
69
|
+
export function isRunRule(rule, cli = ownCli()) {
|
|
70
|
+
const text = /^Bash\((.*)\)$/s.exec(rule)?.[1];
|
|
71
|
+
const [kit, word] = (text === undefined ? undefined : shellWords(text).words) ?? [];
|
|
72
|
+
if (kit === undefined || word === undefined) return false;
|
|
73
|
+
return (kit === cli || path.basename(kit) === 'obk') && (word === 'run' || word.startsWith('run:'));
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* The rules that were defaults once and are not now, that the bot's `allow`
|
|
64
78
|
* holds: those of the commands kept back (#548, #615) or gone from the kit (#555),
|
|
65
|
-
* spelled as `defaultRules` spelled them, and Orca's check (#564)
|
|
66
|
-
*
|
|
79
|
+
* spelled as `defaultRules` spelled them, and Orca's check (#564); and any rule
|
|
80
|
+
* for `obk run`, which the user allowed and the kit no longer has (#652). The
|
|
81
|
+
* kit cannot tell its own old entry from a user's yes to the same rule, so it
|
|
67
82
|
* names them and leaves them (ADR 0051, ADR 0050).
|
|
68
83
|
*/
|
|
69
84
|
export function heldRules(bots, home, bot, cli = ownCli()) {
|
|
70
85
|
const allowed = allowOf(home, bot);
|
|
71
|
-
|
|
86
|
+
const spelled = [...NO_LONGER_DEFAULT, ...REMOVED].map((command) => `Bash(${shellWord(cli)} ${command} --bots ${shellWord(bots)}:*)`)
|
|
72
87
|
.concat(ORCA_CHECK)
|
|
73
88
|
.filter((rule) => allowed.includes(rule));
|
|
89
|
+
return [...new Set([...spelled, ...allowed.filter((rule) => isRunRule(rule, cli))])];
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
/** Command wrappers: programs that run the command their arguments name. */
|
|
@@ -165,14 +181,12 @@ function commandBroadness(all) {
|
|
|
165
181
|
if (star === -1) return undefined;
|
|
166
182
|
|
|
167
183
|
const program = path.basename(words[0].replace(/:?\*.*$/, ''));
|
|
168
|
-
if (isKit(words[0]) && mayRun(words[1] ?? '')) return runBroadness(words);
|
|
169
184
|
if (runnerOf(program) !== undefined) {
|
|
170
185
|
// Only what runs, fixed by its absolute path, narrows it. A wrapper's
|
|
171
|
-
// program is then judged by itself; an interpreter's script is the end
|
|
172
|
-
// unless it is the kit, which is judged as the kit.
|
|
186
|
+
// program is then judged by itself; an interpreter's script is the end.
|
|
173
187
|
const next = (words[1] ?? '').replace(/:\*$/, '');
|
|
174
188
|
const fixed = next.startsWith('/') && !next.includes('*');
|
|
175
|
-
if (fixed &&
|
|
189
|
+
if (fixed && WRAPPERS.has(program)) return commandBroadness(words.slice(1));
|
|
176
190
|
if (fixed && runnerOf(path.basename(next)) === undefined) return undefined;
|
|
177
191
|
return `${program} runs whatever its arguments say, so ${ANY}`;
|
|
178
192
|
}
|
|
@@ -185,37 +199,6 @@ function commandBroadness(all) {
|
|
|
185
199
|
return undefined;
|
|
186
200
|
}
|
|
187
201
|
|
|
188
|
-
/** Whether a word is the kit's CLI: `obk` by its name, or the path the kit runs as. */
|
|
189
|
-
const isKit = (word) => path.basename(word) === 'obk' || word === ownCli();
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Whether a rule's word can match `run`: Claude's `*` matches any characters,
|
|
193
|
-
* spaces too, so `ru*` matches `run --timeout 1 -- sh`, while `:*` matches
|
|
194
|
-
* only at the end of a word.
|
|
195
|
-
*/
|
|
196
|
-
function mayRun(word) {
|
|
197
|
-
const star = word.indexOf('*');
|
|
198
|
-
if (star === -1) return word === 'run';
|
|
199
|
-
const before = word.slice(0, star);
|
|
200
|
-
return before.endsWith(':') ? before === 'run:' : 'run'.startsWith(before);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Why a rule for `obk run`, as shell words, is broad, or undefined (#532). It
|
|
205
|
-
* runs whatever its arguments say, so only a fixed time limit, `--`, and a
|
|
206
|
-
* command after it that is narrow as a rule of its own narrow it.
|
|
207
|
-
*/
|
|
208
|
-
function runBroadness(words) {
|
|
209
|
-
const [, , ...rest] = words;
|
|
210
|
-
let command;
|
|
211
|
-
if (rest[0] === '--timeout' && /^\d+$/.test(rest[1] ?? '') && rest[2] === '--') command = rest.slice(3);
|
|
212
|
-
if (/^--timeout=\d+$/.test(rest[0] ?? '') && rest[1] === '--') command = rest.slice(2);
|
|
213
|
-
if (words[1] !== 'run' || command === undefined || command.length === 0) {
|
|
214
|
-
return `obk run runs whatever its arguments say, so ${ANY}. A rule for it names the command after --, such as Bash(obk run --timeout 300 -- go test:*)`;
|
|
215
|
-
}
|
|
216
|
-
return commandBroadness(command);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
202
|
/**
|
|
220
203
|
* Refuse the rules `permission allow` was given when any is broad, before anything is
|
|
221
204
|
* written, naming the file the user can add it to themselves.
|
package/src/processes.js
CHANGED
|
@@ -19,19 +19,18 @@
|
|
|
19
19
|
// signals: SIGTERM first, to the whole group where every process in it is the
|
|
20
20
|
// session's own, then up to 3 seconds for them to end, then SIGKILL to each
|
|
21
21
|
// that still runs. Inside Codex's sandbox `ps` does not start (#298), and then
|
|
22
|
-
// nothing is stopped and the answer says it cannot tell.
|
|
23
|
-
//
|
|
24
|
-
// fakes and sends no real signal.
|
|
22
|
+
// nothing is stopped and the answer says it cannot tell. The kit runs the three
|
|
23
|
+
// by their fixed paths, and no setting changes them (#652).
|
|
25
24
|
|
|
26
25
|
import { spawnSync } from 'node:child_process';
|
|
27
26
|
import { realpathSync } from 'node:fs';
|
|
28
27
|
import path from 'node:path';
|
|
29
28
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
30
29
|
|
|
31
|
-
import {
|
|
30
|
+
import { LSOF, PS } from './orca.js';
|
|
32
31
|
|
|
33
|
-
/** The `kill`
|
|
34
|
-
const
|
|
32
|
+
/** The `kill` the kit sends signals with, by its fixed path. */
|
|
33
|
+
const KILL = '/bin/kill';
|
|
35
34
|
|
|
36
35
|
/** How long one `ps`, `lsof` or `kill` may take. */
|
|
37
36
|
const READ_MS = 10_000;
|
|
@@ -273,7 +272,7 @@ function readProcesses() {
|
|
|
273
272
|
* READ_MS.
|
|
274
273
|
*/
|
|
275
274
|
function workingFolders(ms = READ_MS) {
|
|
276
|
-
const asked = spawnSync(
|
|
275
|
+
const asked = spawnSync(LSOF, ['-a', '-d', 'cwd', '-u', String(process.getuid()), '-Fpn'], { encoding: 'utf8', timeout: bound(ms), maxBuffer: 64 * 1024 * 1024 });
|
|
277
276
|
if (asked.error?.code === 'ETIMEDOUT') return { unreadable: 'lsof did not answer in time' };
|
|
278
277
|
if (asked.error || asked.status !== 0) return { unreadable: `lsof could not list the processes' working folders${because(asked)}` };
|
|
279
278
|
const cwds = new Map();
|
|
@@ -292,7 +291,7 @@ function workingFolders(ms = READ_MS) {
|
|
|
292
291
|
* own command line (review of PR #543). Or `{ unreadable: <why> }`.
|
|
293
292
|
*/
|
|
294
293
|
function processTable(ms = READ_MS) {
|
|
295
|
-
const asked = spawnSync(
|
|
294
|
+
const asked = spawnSync(PS, ['-A', '-ww', '-o', 'pid=,ppid=,pgid=,uid=,stat=,lstart=,command='], { encoding: 'utf8', timeout: bound(ms), maxBuffer: 64 * 1024 * 1024 });
|
|
296
295
|
if (asked.error?.code === 'ETIMEDOUT') return { unreadable: 'ps did not answer in time' };
|
|
297
296
|
if (asked.error || asked.status !== 0) return { unreadable: `ps could not read the process table${because(asked)}` };
|
|
298
297
|
const table = new Map();
|
|
@@ -383,7 +382,7 @@ const inside = (cwd, dir) => cwd === dir || cwd.startsWith(`${dir}${path.sep}`);
|
|
|
383
382
|
*/
|
|
384
383
|
function signal(name, target, ms = READ_MS) {
|
|
385
384
|
if (!Number.isInteger(target) || Math.abs(target) <= 1) return { refused: `the kit does not signal ${target}` };
|
|
386
|
-
const sent = spawnSync(
|
|
385
|
+
const sent = spawnSync(KILL, ['-s', name, '--', String(target)], { encoding: 'utf8', timeout: bound(ms) });
|
|
387
386
|
// Cut off at its time: whether the signal went is not known.
|
|
388
387
|
if (sent.error?.code === 'ETIMEDOUT') return { late: true };
|
|
389
388
|
if (!sent.error && sent.status === 0) return {};
|
package/src/temp.js
CHANGED
|
@@ -530,10 +530,30 @@ const TEACH_TITLE = 'Teach auto mode about your environment?';
|
|
|
530
530
|
/** The foot that ends a Claude Code form, below which its input box sits. */
|
|
531
531
|
const TEACH_FOOT = /Enter to .*· Esc to cancel/;
|
|
532
532
|
|
|
533
|
+
/**
|
|
534
|
+
* A setting row of the Teach form: its name, then spaces and a value. The value
|
|
535
|
+
* is whatever the setting shows, `Mixed`, `true` or `false` as read, and Esc
|
|
536
|
+
* cancels the form whatever it is (#646).
|
|
537
|
+
*/
|
|
538
|
+
const setting = (name) => ({ name, fits: (row) => row.startsWith(name) && /^ +\S/.test(row.slice(name.length)) });
|
|
539
|
+
|
|
540
|
+
/** The Teach form's rows, from its title to its foot, with `foot`. */
|
|
541
|
+
const teachForm = (foot) => [
|
|
542
|
+
TEACH_TITLE,
|
|
543
|
+
'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
|
|
544
|
+
'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
|
|
545
|
+
setting('How you use Claude here'),
|
|
546
|
+
setting('Also scan shell history'),
|
|
547
|
+
setting('Also scan your other repos'),
|
|
548
|
+
'Continue',
|
|
549
|
+
foot,
|
|
550
|
+
];
|
|
551
|
+
|
|
533
552
|
/**
|
|
534
553
|
* The known shapes of the Teach screen, from its title to its foot, each as
|
|
535
554
|
* captured live, with what answers it. Rows are compared trimmed, the pointer
|
|
536
|
-
* `❯` taken out, so the pointer may sit on any one of them.
|
|
555
|
+
* `❯` taken out, so the pointer may sit on any one of them. A setting row
|
|
556
|
+
* matches by its name, whatever its value.
|
|
537
557
|
*
|
|
538
558
|
* Claude Code 2.1.283's form (#261's live test, #416) and 2.1.302's (#604) are
|
|
539
559
|
* answered with Esc, which cancels them (Not now), never a return, which is
|
|
@@ -542,34 +562,8 @@ const TEACH_FOOT = /Enter to .*· Esc to cancel/;
|
|
|
542
562
|
* architect's ruling on #489).
|
|
543
563
|
*/
|
|
544
564
|
const TEACH_SHAPES = [
|
|
545
|
-
{
|
|
546
|
-
|
|
547
|
-
rows: [
|
|
548
|
-
TEACH_TITLE,
|
|
549
|
-
'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
|
|
550
|
-
'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
|
|
551
|
-
'How you use Claude here Mixed',
|
|
552
|
-
'Also scan shell history true',
|
|
553
|
-
'Also scan your other repos false',
|
|
554
|
-
'Continue',
|
|
555
|
-
'←/→ to change usage · Enter to continue · Esc to cancel',
|
|
556
|
-
],
|
|
557
|
-
escape: true,
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
name: 'the 2.1.302 form',
|
|
561
|
-
rows: [
|
|
562
|
-
TEACH_TITLE,
|
|
563
|
-
'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
|
|
564
|
-
'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
|
|
565
|
-
'How you use Claude here Mixed',
|
|
566
|
-
'Also scan shell history false',
|
|
567
|
-
'Also scan your other repos false',
|
|
568
|
-
'Continue',
|
|
569
|
-
'←/→ to change · Enter to continue · Esc to cancel',
|
|
570
|
-
],
|
|
571
|
-
escape: true,
|
|
572
|
-
},
|
|
565
|
+
{ name: 'the 2.1.283 form', rows: teachForm('←/→ to change usage · Enter to continue · Esc to cancel'), escape: true },
|
|
566
|
+
{ name: 'the 2.1.302 form', rows: teachForm('←/→ to change · Enter to continue · Esc to cancel'), escape: true },
|
|
573
567
|
{
|
|
574
568
|
name: 'the 2.1.289 list',
|
|
575
569
|
rows: [
|
|
@@ -589,6 +583,12 @@ const NOT_NOW_CHOICE = '2. Not now';
|
|
|
589
583
|
/** A row as a shape is compared: trimmed, with its pointer, wherever it sits, taken out. */
|
|
590
584
|
const unpointed = (row) => row.replace('❯', ' ').trim();
|
|
591
585
|
|
|
586
|
+
/** Whether a shape's row `want` is the screen's `row`, as unpointed gives it. */
|
|
587
|
+
const fitsRow = (want, row) => (typeof want === 'string' ? row === want : want.fits(row));
|
|
588
|
+
|
|
589
|
+
/** A shape's row as a refusal names it. */
|
|
590
|
+
const shapeRow = (want) => (typeof want === 'string' ? want : `${want.name} and a value`);
|
|
591
|
+
|
|
592
592
|
/** The full-width rule Claude Code draws right above the screen's title: `─` in 2.1.289, `▔` in 2.1.283. */
|
|
593
593
|
const ABOVE_TITLE = /^(?:─+|▔+)$/;
|
|
594
594
|
|
|
@@ -622,13 +622,13 @@ function teachScreen(rows) {
|
|
|
622
622
|
const misfits = TEACH_SHAPES.map((shape) => {
|
|
623
623
|
const pointers = block.filter((row) => row.includes('❯')).length;
|
|
624
624
|
if (pointers !== 1) return `${pointers} rows with the pointer ❯, where ${shape.name} has one`;
|
|
625
|
-
const odd = block.find((row) => !shape.rows.
|
|
625
|
+
const odd = block.find((row) => !shape.rows.some((want) => fitsRow(want, unpointed(row))));
|
|
626
626
|
if (odd !== undefined) return `a row ${shape.name} does not have: ${odd.trim()}`;
|
|
627
|
-
const missing = shape.rows.find((
|
|
628
|
-
if (missing !== undefined) return `no row ${missing}, which ${shape.name} has`;
|
|
627
|
+
const missing = shape.rows.find((want) => !block.some((row) => fitsRow(want, unpointed(row))));
|
|
628
|
+
if (missing !== undefined) return `no row ${shapeRow(missing)}, which ${shape.name} has`;
|
|
629
629
|
if (block.length !== shape.rows.length) return `${block.length} rows, where ${shape.name} has ${shape.rows.length}`;
|
|
630
|
-
const moved = block.findIndex((row, at) =>
|
|
631
|
-
if (moved >= 0) return `the row ${block[moved].trim()} where ${shape.name} has ${shape.rows[moved]}`;
|
|
630
|
+
const moved = block.findIndex((row, at) => !fitsRow(shape.rows[at], unpointed(row)));
|
|
631
|
+
if (moved >= 0) return `the row ${block[moved].trim()} where ${shape.name} has ${shapeRow(shape.rows[moved])}`;
|
|
632
632
|
return undefined;
|
|
633
633
|
});
|
|
634
634
|
const fits = misfits.findIndex((misfit) => misfit === undefined);
|
|
@@ -642,7 +642,7 @@ const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row)
|
|
|
642
642
|
/**
|
|
643
643
|
* Answer the first-run screen of a temporary session the caller made (#489):
|
|
644
644
|
* Claude Code's Teach auto mode screen, in one of the shapes the kit knows,
|
|
645
|
-
* matched exactly. The 2.1.283 and 2.1.302 forms get Esc. The 2.1.289 list gets arrows to
|
|
645
|
+
* matched exactly. The 2.1.283 and 2.1.302 forms get Esc, whatever their settings show. The 2.1.289 list gets arrows to
|
|
646
646
|
* "2. Not now" where needed, a second look that the pointer is there and the
|
|
647
647
|
* list is otherwise as it was, and only then a return. Anything else is refused with
|
|
648
648
|
* nothing typed. Like `trustHooks`, it is one command a permission rule of its
|
package/src/up.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
11
11
|
|
|
12
12
|
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, rememberSession, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
13
|
-
import { botDir, botNames, displayName,
|
|
13
|
+
import { botDir, botNames, displayName, readBot } from './bot.js';
|
|
14
14
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
15
15
|
import { dropRemovedHooks, installHook } from './hooks.js';
|
|
16
16
|
import { writePermissions } from './permissions.js';
|
|
@@ -233,7 +233,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
233
233
|
// Orca is asked first and the book is written after: nothing that takes time
|
|
234
234
|
// happens while the book is held, because a session's own hook may be writing
|
|
235
235
|
// its id into that same file at any moment.
|
|
236
|
-
const { change, ...orca } = orcaProject(home,
|
|
236
|
+
const { change, ...orca } = orcaProject(home, title);
|
|
237
237
|
await updateBook(home, (book) => { book.orca = orca; });
|
|
238
238
|
// Orca's window does not see a project made or renamed on the command line
|
|
239
239
|
// until it is told (#224).
|
package/src/run.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
// `obk run --timeout <seconds> -- <command…>`: one command run with a hard time
|
|
2
|
-
// limit (#532). macOS has no `timeout`, and the fleet's form for a bounded run,
|
|
3
|
-
// `perl -e 'alarm …; exec @ARGV' -- <command>`, cannot be allowed by a narrow
|
|
4
|
-
// rule, since perl runs whatever its arguments say. A rule for this command
|
|
5
|
-
// names the real command after `--`, and the kit judges that command as a rule
|
|
6
|
-
// of its own (permissions.js). It runs whatever its arguments say too, so it is
|
|
7
|
-
// never in the default set (ADR 0050).
|
|
8
|
-
//
|
|
9
|
-
// The command runs with no shell, in a process group of its own, so the time
|
|
10
|
-
// limit stops what it started as well. Its exit code comes back as it is; one
|
|
11
|
-
// ended by a signal the kit did not send comes back as 128 and the signal's
|
|
12
|
-
// number, as a shell gives it.
|
|
13
|
-
|
|
14
|
-
import { spawn } from 'node:child_process';
|
|
15
|
-
import { constants } from 'node:os';
|
|
16
|
-
|
|
17
|
-
/** The exit code of a run the time limit stopped, as GNU timeout gives it. */
|
|
18
|
-
const TIMED_OUT = 124;
|
|
19
|
-
|
|
20
|
-
/** The exit codes a shell gives a command it cannot find, and one it cannot run. */
|
|
21
|
-
const NOT_FOUND = 127;
|
|
22
|
-
const CANNOT_RUN = 126;
|
|
23
|
-
|
|
24
|
-
/** How long the group has after SIGTERM before SIGKILL, as `retire` gives a session's processes (#537). */
|
|
25
|
-
const GRACE_MS = 3000;
|
|
26
|
-
|
|
27
|
-
/** How often the kit looks whether anything of the group still runs. */
|
|
28
|
-
const LOOK_MS = 100;
|
|
29
|
-
|
|
30
|
-
/** The longest time limit a timer holds, in seconds: about 24 days. */
|
|
31
|
-
const LONGEST = Math.floor((2 ** 31 - 1) / 1000);
|
|
32
|
-
|
|
33
|
-
/** The signals that end the kit and that it passes on to the command's group, so it is not left running. */
|
|
34
|
-
const PASSED_ON = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
35
|
-
|
|
36
|
-
const USE = 'obk run takes --timeout <seconds>, then --, then the command: obk run --timeout 300 -- go test ./...';
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The time limit and the command from `obk run`'s arguments, or a refusal.
|
|
40
|
-
* Only `--timeout` goes before `--`; everything after it is the command's.
|
|
41
|
-
*/
|
|
42
|
-
export function parseRun(args) {
|
|
43
|
-
const end = args.indexOf('--');
|
|
44
|
-
const options = end === -1 ? args : args.slice(0, end);
|
|
45
|
-
let value;
|
|
46
|
-
for (let at = 0; at < options.length; at += 1) {
|
|
47
|
-
const option = options[at];
|
|
48
|
-
if (option === '--timeout' && at + 1 < options.length && value === undefined) {
|
|
49
|
-
value = options[at + 1];
|
|
50
|
-
at += 1;
|
|
51
|
-
} else if (option.startsWith('--timeout=') && value === undefined) {
|
|
52
|
-
value = option.slice('--timeout='.length);
|
|
53
|
-
} else {
|
|
54
|
-
throw new Error(`run does not take ${option}. ${USE}. Nothing was run.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (value === undefined) throw new Error(`run needs --timeout <seconds>: how long the command may run. ${USE}. Nothing was run.`);
|
|
58
|
-
if (!/^\d+$/.test(value) || Number(value) === 0 || Number(value) > LONGEST) {
|
|
59
|
-
throw new Error(`run's --timeout is a whole number of seconds from 1 to ${LONGEST}, and got: ${value === '' ? 'nothing' : value}. Nothing was run.`);
|
|
60
|
-
}
|
|
61
|
-
const command = end === -1 ? [] : args.slice(end + 1);
|
|
62
|
-
if (command.length === 0) throw new Error(`run needs the command after --. ${USE}. Nothing was run.`);
|
|
63
|
-
return { seconds: Number(value), command };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Run the command with its time limit, its input and output the caller's, and
|
|
68
|
-
* give back the exit code `obk run` exits with. The run ends when nothing of
|
|
69
|
-
* the command's group runs any more: a process the command started and left
|
|
70
|
-
* running is held to the same limit, and to a signal the kit passes on.
|
|
71
|
-
*/
|
|
72
|
-
export function runBounded({ seconds, command }) {
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
// `detached` makes the command the leader of a group of its own, so its
|
|
75
|
-
// pid, taken here at spawn, is the group's id.
|
|
76
|
-
const child = spawn(command[0], command.slice(1), { stdio: 'inherit', detached: true });
|
|
77
|
-
// The leader's exit code, once it has exited.
|
|
78
|
-
let exitCode;
|
|
79
|
-
let stopping = false;
|
|
80
|
-
let result;
|
|
81
|
-
let killAt;
|
|
82
|
-
let deadline;
|
|
83
|
-
let look;
|
|
84
|
-
const signal = (pid, name) => {
|
|
85
|
-
try {
|
|
86
|
-
process.kill(pid, name);
|
|
87
|
-
return true;
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
// Each process of the group gets a signal once. The leader gets it by its
|
|
93
|
-
// own pid as well only when it may have left the group: when nothing is
|
|
94
|
-
// left in the group, or for SIGKILL, which no handler sees. And only
|
|
95
|
-
// while it has not exited, since its pid is free again after that.
|
|
96
|
-
const group = (name) => {
|
|
97
|
-
const sent = signal(-child.pid, name);
|
|
98
|
-
if (exitCode === undefined && name !== 0 && (!sent || name === 'SIGKILL')) signal(child.pid, name);
|
|
99
|
-
return sent;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const finish = (code) => {
|
|
103
|
-
clearTimeout(deadline);
|
|
104
|
-
clearTimeout(look);
|
|
105
|
-
for (const name of PASSED_ON) process.off(name, passOn);
|
|
106
|
-
resolve(code);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// Once the leader has exited or the group is being stopped: look until
|
|
110
|
-
// nothing of the group runs, and send SIGKILL when the grace is over.
|
|
111
|
-
const watch = () => {
|
|
112
|
-
if (exitCode !== undefined && !group(0)) return finish(result ?? exitCode);
|
|
113
|
-
if (killAt !== undefined && Date.now() >= killAt) {
|
|
114
|
-
group('SIGKILL');
|
|
115
|
-
killAt = undefined;
|
|
116
|
-
}
|
|
117
|
-
look = setTimeout(watch, LOOK_MS);
|
|
118
|
-
return undefined;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Stop the group: `name` first, then SIGKILL to what still runs after the
|
|
122
|
-
// grace. obk run exits with `code`, or with the leader's own when it is
|
|
123
|
-
// undefined.
|
|
124
|
-
const stop = (name, code) => {
|
|
125
|
-
if (stopping) return;
|
|
126
|
-
stopping = true;
|
|
127
|
-
result = code;
|
|
128
|
-
clearTimeout(deadline);
|
|
129
|
-
group(name);
|
|
130
|
-
killAt = Date.now() + GRACE_MS;
|
|
131
|
-
clearTimeout(look);
|
|
132
|
-
watch();
|
|
133
|
-
};
|
|
134
|
-
const passOn = (name) => stop(name, undefined);
|
|
135
|
-
|
|
136
|
-
child.on('error', (error) => {
|
|
137
|
-
const missing = error.code === 'ENOENT';
|
|
138
|
-
process.stderr.write(`obk: run could not start ${command[0]}: ${missing ? 'there is no such command' : error.code ?? error.message}.\n`);
|
|
139
|
-
finish(missing ? NOT_FOUND : CANNOT_RUN);
|
|
140
|
-
});
|
|
141
|
-
if (child.pid === undefined) return;
|
|
142
|
-
for (const name of PASSED_ON) process.on(name, passOn);
|
|
143
|
-
|
|
144
|
-
child.on('exit', (code, name) => {
|
|
145
|
-
exitCode = code ?? 128 + constants.signals[name];
|
|
146
|
-
clearTimeout(look);
|
|
147
|
-
watch();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
deadline = setTimeout(() => {
|
|
151
|
-
process.stderr.write(`obk: run stopped ${command.join(' ')}: it ran past its time limit of ${seconds} seconds, and its whole process group was stopped.\n`);
|
|
152
|
-
stop('SIGTERM', TIMED_OUT);
|
|
153
|
-
}, seconds * 1000);
|
|
154
|
-
});
|
|
155
|
-
}
|