@assuredloop/orca-bot-kit 0.27.0 → 0.29.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
@@ -141,19 +141,41 @@ words, `dangerously-skip`
141
141
  ([ADR 0015](docs/adr/0015-three-approval-levels-auto-by-default.md)). Only
142
142
  `obk permission approval` changes it, after your yes: `session add` and
143
143
  `session change` take no approval, and refuse an extra argument that sets one
144
- ([ADR 0041](docs/adr/0041-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
144
+ ([ADR 0051](docs/adr/0051-permission-changes-and-risky-operations-have-commands-of-their-own.md))
145
+ or that widens what the session may write or run, such as `--add-dir`, Claude
146
+ Code's `--settings` or Codex's `--profile`. On Codex, `-c` passes only for a
147
+ short list of keys known to be safe, such as `model_reasoning_effort`
148
+ ([ADR 0049](docs/adr/0049-extra-arguments-that-widen-a-session-are-refused.md)).
149
+ A Codex session's work dir stays in its bot folder: Codex may write in its work
150
+ dir, so `session add` and `session change` refuse one outside the bot folder for
151
+ a Codex session
152
+ ([ADR 0045](docs/adr/0045-a-codex-sessions-work-dir-stays-in-its-bot-folder.md)).
153
+
154
+ A bot or a session can also run on [Pi](https://pi.dev) (`--harness pi`, tested
155
+ with Pi 1.1.0). Pi asks before no tool call, so a Pi session is always
156
+ `dangerously-skip`, and the kit refuses `auto` and `ask` for it. Sign in to Pi
157
+ yourself first: `obk up` asks Pi which models it can use, and starts nothing
158
+ when Pi has none, or not the one the session names. The kit gives a Pi session
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. Mail, temporary sessions, skills and
161
+ health for a Pi session come later, and so do `obk init --harness pi` and
162
+ `obk session clear` and `compact` on Pi.
145
163
 
146
164
  Every bot gets the kit's default permission rules, written by the kit with
147
165
  nobody asked: its own commands for this bots folder, reading a long message,
148
- `git add` and `git commit`, and Orca's `orca orchestration check --run`
149
- ([ADR 0036](docs/adr/0036-the-kit-writes-every-bots-default-permission-rules.md)).
150
- `obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`
151
- and `obk init` are not in the set, and keep your yes
152
- ([ADR 0041](docs/adr/0041-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
166
+ `git add` and `git commit`
167
+ ([ADR 0050](docs/adr/0050-the-kit-writes-every-bots-default-permission-rules.md)).
168
+ `obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
169
+ `obk bot change`, `obk session add`, `obk session change`, `obk init` and
170
+ `obk run` are not in the set, and keep your yes
171
+ ([ADR 0051](docs/adr/0051-permission-changes-and-risky-operations-have-commands-of-their-own.md)).
153
172
  A rule beyond the set is written by `obk permission allow`, after your yes to
154
- the exact rule. A bot that still holds the `obk restart` or `obk bot create`
155
- rule, which kit 0.26.0 gave every bot, keeps it: `obk rules build` and `obk up`
156
- name it, with the `obk permission disallow` command that takes it back.
173
+ the exact rule. A bot that still holds a rule that the kit gave every bot
174
+ before and that is no longer in the set keeps it: the rule of `obk restart`,
175
+ `obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
176
+ `obk session mail` or `obk session nudge`, or of Orca's
177
+ `orca orchestration check --run`. `obk rules build` and `obk up` name it, with
178
+ the `obk permission disallow` command that takes it back.
157
179
 
158
180
  Neither command touches Orca. `obk up` is what makes it all real there: an Orca
159
181
  project per bot, a tab per session, each started at the bot home with its own
@@ -451,6 +473,7 @@ obk permission disallow --bots /path/to/my-bots --bot api-bot --rule 'Bash(gh pr
451
473
  obk permission approval --bots /path/to/my-bots --bot api-bot --session daily --approval ask
452
474
  obk permission approval --bots /path/to/my-bots --bot api-bot --temps --approval dangerously-skip
453
475
  obk session change --bots /path/to/my-bots --bot api-bot --session daily --model opus
476
+ obk session change --bots /path/to/my-bots --bot bot-father --session daily --harness codex
454
477
  obk skills remove --bots /path/to/my-bots --bot api-bot --skill kit:obk-tdd
455
478
  obk pause --bots /path/to/my-bots --bot api-bot [--session daily]
456
479
  obk unpause --bots /path/to/my-bots --bot api-bot [--session daily]
@@ -458,8 +481,11 @@ obk retire --bots /path/to/my-bots --bot api-bot [--session daily]
458
481
  ```
459
482
 
460
483
  A change to a charter or a setting is written at once, and a running session
461
- takes it when it next starts. A session keeps its harness: to move one, retire
462
- it and add another. A pause closes the tabs the way a restart does, with the
484
+ takes it when it next starts. A session can move to the other harness with
485
+ `--harness`, Bot Father's management session too: it then starts a new
486
+ conversation there, the book keeps the old one in its history, and its model,
487
+ effort and context go back to the new harness's defaults unless you give them
488
+ too. A pause closes the tabs the way a restart does, with the
463
489
  same refusals, and one more: it also refuses a tab with only its shell in front
464
490
  whose conversation the book does not name. `obk up` leaves what is paused
465
491
  closed until `obk unpause` brings it back with its conversations. Retiring a
@@ -514,7 +540,9 @@ obk groom --bots /path/to/my-bots --on --at 04:00 --run-on codex --model gpt-6-s
514
540
  Each fire then makes a temporary Codex session of the grooming session's, named
515
541
  `groom-<date and time>`, which does the grooming, mails its report back, and is
516
542
  retired; the grooming session sends the report on to the management session. A
517
- run an earlier fire left behind is retired at the next one.
543
+ run an earlier fire left behind is retired at the next one. Each run also
544
+ carries the grooming session's own prompt, as `--on` finds it: after you change
545
+ that prompt, run `obk groom --at <HH:MM>` alone, which keeps the runs on Codex.
518
546
 
519
547
  ## When something is wrong
520
548
 
@@ -549,6 +577,13 @@ What it looks for:
549
577
  `<bot>.<session>` every fleet's sessions had before 0.10.0, which a session of
550
578
  another fleet can write to. Its next start through the kit gives it one of its
551
579
  own, so the finding names the `obk restart … --session` that does it;
580
+ - **a session whose mail waits and Orca has not told it**: mail older than an
581
+ hour that is unread and that Orca's notice never reached, while the session's
582
+ harness is idle now. Orca can stop telling a session that sat idle through an
583
+ Orca restart or update, so the finding names the `obk restart … --session`
584
+ that opens a new tab, which Orca tells. It counts the letters and gives the
585
+ time of the oldest; it never shows their subject or text. A busy session is
586
+ not named: Orca tells it when its turn ends;
552
587
  - **a process still running in a retired session's work dir**: one from before
553
588
  retire stopped them, or one it could not stop, with its pid and command;
554
589
  - **leftovers no book owns**: an Orca project inside your bots folder with no
@@ -671,9 +706,14 @@ real Orca and the real harnesses on your own machine. No CI runner can do that,
671
706
  so these are run by hand before a change that touches Orca or a harness is
672
707
  merged. A system test touches only what it creates and removes it afterwards,
673
708
  except the mailbox Runs Orca gives no way to delete, which the run names; it
674
- never closes a tab it did not open. The trust its harnesses write into your
675
- `~/.codex/config.toml` and `~/.claude.json` for the run's own throwaway folders
676
- is taken out again after the run, and named; nothing else in them is changed.
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.
677
717
  An Orca project the run leaves in one of its own throwaway folders is named,
678
718
  with its setup id, and fails the run; the command removes none itself.
679
719
 
package/SETUP.md CHANGED
@@ -38,15 +38,26 @@ 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.1.0, for a bot on Pi
41
42
  ```
42
43
 
43
44
  The Orca line must come back with `ok: true` and `result.runtime.reachable:
44
- true`. Anything else means Orca is not running, whatever else it says: ask them
45
- to start it, then run the line again.
45
+ true`. When it answers that Orca is not running or not reachable, ask them to
46
+ start it, then run the line again.
47
+
48
+ When the line fails with an error instead of an answer, the error is the
49
+ problem. It does not show that Orca is stopped. Tell them the error as the CLI
50
+ printed it, and do not ask them to start Orca for it. For example, `EPERM` with
51
+ `process.cwd failed` or `uv_cwd` means that the CLI cannot read the folder the
52
+ shell is in, while Orca can be running. Run the line again from their home
53
+ folder.
46
54
 
47
55
  On the Orca CLI path: `/usr/local/bin/orca` is a root-only symlink on some
48
56
  machines and fails for an ordinary user, so use the full path above. If Orca
49
- lives somewhere else here, find it and set `OBK_ORCA` to it.
57
+ lives somewhere else here, find it and set `OBK_ORCA` to it. If that is another
58
+ build of Orca with its own data folder, also set `OBK_ORCA_DATA` to the full
59
+ path of that folder, for example `$HOME/Library/Application Support/orca-lean`.
60
+ The kit reads Orca's launch arguments from the `profiles` folder there.
50
61
 
51
62
  If Node is older than 24.21.0, say so and stop. The kit's book of sessions is a
52
63
  SQLite write transaction through Node's own `node:sqlite`, and 24.21.0 is the
@@ -132,8 +143,9 @@ Read everything it prints. Three parts of it matter to them:
132
143
  not stop Bot Father for the kit's own commands, reading a long message
133
144
  beside the bots folder, and its commits. The kit writes this set for every
134
145
  bot, with nobody asked. Say in a sentence what it allows, and that
135
- `obk permission`, `obk retire`, `obk pause` and `obk init` are not in it and
136
- keep their yes;
146
+ `obk permission`, `obk retire`, `obk pause`, `obk restart`, `obk bot create`,
147
+ `obk bot change`, `obk session add`, `obk session change`, `obk init` and
148
+ `obk run` are not in it and keep their yes;
137
149
  - **anything it says about Orca's own default launch arguments.** Orca adds
138
150
  these to every agent it launches, relaunches and resumes. When they carry a
139
151
  permission bypass, every session runs in that mode whatever approval level the
@@ -192,21 +204,22 @@ that folder itself, exactly as you gave it to `obk init`:
192
204
  "$ORCA" terminal read --terminal <handle> --screen --json
193
205
  ```
194
206
 
195
- What you will see, and the usual answer. Each goes as one
207
+ What you will see, and the usual answer. Each key goes as one
196
208
  `"$ORCA" terminal send --terminal <handle> --text …`, with the return inside the
197
209
  text and **no** `--enter`, because a menu takes a return as the keypress it is
198
- waiting for:
210
+ waiting for. Where the Send cell names `obk` commands, send no keys: the kit
211
+ answers that screen, by the command and its arguments below the table.
199
212
 
200
213
  | On screen | Send | Which is | Proven on |
201
214
  |---|---|---|---|
202
215
  | Claude Code's folder trust list | `\x1b[B\r` | down, then return: its selection starts on **No, exit** | Claude Code 2.1.283 |
203
216
  | Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `\r` | return: its selection starts on **Trust and continue** | Codex 0.160.0 |
204
- | Codex's `Hooks need review` | `\x1b[B\r` | down, then return, to **Trust all and continue**; if the selection already starts there, return alone | Codex 0.160.0 |
217
+ | Codex's `Hooks need review` | `obk session trust-hooks`, or `obk temp trust-hooks` for a temporary session | the kit checks the hooks, then goes to **Trust all and continue** | Codex 0.160.0 |
205
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) |
206
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 |
207
220
  | `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
208
- | Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which cancels it (**Not now**). On 2.1.283 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`. **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 |
209
- | 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 | `\x1b[B\r` | down, then return, to **2. Not now**: its selection starts on **1. Yes**. Read the screen after the down arrow, and send 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 |
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, `Also scan shell history` shows `true` and the foot starts `←/→ to change usage`. On 2.1.302, it shows `false` and the foot starts `←/→ to change`. **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 once, in #604 |
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 |
210
223
 
211
224
  Each key counts on where the selection starts, as it did on the version named.
212
225
  Read the screen in front of you before you send: if the selection is somewhere
@@ -233,8 +246,9 @@ that a narrow permission rule of the kit's default set allows it rather than a
233
246
  rule to type into any tab. On Codex's hooks review, the maker runs
234
247
  `obk temp trust-hooks --bots <their path> --name <session>`. On Claude Code's
235
248
  `Teach auto mode` screen, it runs `obk temp answer --bots <their path> --name <session>`.
236
- That command reads the screen and answers only when it holds one of the two
237
- shapes in the table exactly as the kit knows it: Esc for the 2.1.283 form, and
249
+ That command reads the screen and answers only when it holds one of the
250
+ shapes in the table exactly as the kit knows it: Esc for the 2.1.283 and
251
+ 2.1.302 forms, and
238
252
  **2. Not now** for the 2.1.289 list. On the list it sends the arrows, reads the
239
253
  screen again, and sends the return only with the selection on **2. Not now**.
240
254
  It refuses any other screen, types nothing then, and prints what it saw.
@@ -298,8 +312,10 @@ obk health --bots <their path>
298
312
 
299
313
  It reads the setup and says what it found, and writes nothing at all. It exits 1
300
314
  when it found something, so do not read a non-zero exit as a broken setup. The
301
- one exception is an error with no findings, saying Orca is not answering: that
302
- is a real problem, and the fix is to start Orca and run it again.
315
+ one exception is an error with no findings: that is a real problem. Tell them
316
+ the error as it is. When it says Orca is not answering, run the Orca line from
317
+ step 1 again, and go by what that gives: start Orca only when it answers that
318
+ Orca is not running.
303
319
 
304
320
  Relay what it says in plain words. Nothing it prints is a verdict: which findings
305
321
  matter is for them, or for Bot Father, to judge. If something looks wrong to you,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/rules/mail.md CHANGED
@@ -8,9 +8,9 @@ To reach another session, ask the kit for the road rather than guessing an
8
8
  address: `"${OBK_CLI:-obk}" message to --bots <bots> --to <bot>/<session>`. It
9
9
  answers with the address and the command that carries it.
10
10
 
11
- If your harness refuses a message, do not send it again by another road. Tell
12
- your maker or your user, with the harness's reason. A refusal can mean a missing
13
- permission rule, which they can add through the kit after the user's yes.
11
+ If your harness refuses a message, do not send it again by another road. Tell your maker or your
12
+ user, with the harness's reason. A refusal can mean a missing permission rule. Send the refusal to
13
+ whoever adds your rules: your maker, your user or your fleet's manager.
14
14
 
15
15
  Mail is queued, not an interruption. When Orca says you have mail, read it
16
16
  with `"${OBK_CLI:-obk}" message check --bots <bots>`, never a bare `orca
@@ -213,10 +213,14 @@ its commits. The kit writes this default set itself, with nobody asked:
213
213
  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
- pause`, `obk restart`, `obk bot create` and `obk init`. A bot that still holds
217
- the `obk restart` or `obk bot create` rule from kit 0.26.0 keeps it, and `obk
218
- rules build` and `obk up` name it with the `obk permission disallow` command
219
- that takes it back; run that command only after the user's yes. Never write a
216
+ pause`, `obk restart`, `obk bot create`, `obk bot change`, `obk session add`,
217
+ `obk session change`, `obk init` and `obk run`. A bot that still holds a rule that the
218
+ kit gave every bot before and that is no longer in the set (`obk restart`,
219
+ `obk bot create`, `obk bot change`, `obk session add`, `obk session change`,
220
+ `obk session mail`, `obk session nudge`, or Orca's `orca orchestration check
221
+ --run`) keeps it, and `obk rules build` and `obk up` name
222
+ it with the `obk permission disallow` command that takes it back; run that
223
+ command only after the user's yes. Never write a
220
224
  permission rule into a settings or rules file by hand.
221
225
 
222
226
  The charter decides what else the bot needs. What it lets the bot do without
@@ -241,7 +245,11 @@ out; the kit refuses it, and it refuses a rule of its own default set too. Keep
241
245
  each rule narrow: a program and what it does. The kit refuses a rule that lets
242
246
  the bot run any command, a program with any arguments, or a shell or
243
247
  interpreter with any arguments; a user who wants one for a bot adds it to that
244
- bot's settings themselves. Propose no rule for what the charter does not grant.
248
+ bot's settings themselves. For a test run with a time limit, propose `obk run`
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.
245
253
 
246
254
  A bot that should learn across its sessions can use its harness's own memory,
247
255
  turned on for that bot alone, whatever the user's own setting is for everything
@@ -284,26 +292,29 @@ sometimes. Read each screen with `<orca> terminal read --terminal <handle>
284
292
  --screen`, where `<handle>` is the terminal the kit named for the tab and
285
293
  `<orca>` is the Orca CLI the kit uses: `$OBK_ORCA` when that is set, and
286
294
  otherwise `/Applications/Orca.app/Contents/Resources/bin/orca`. A bare
287
- `orca` can fail. Send each answer as one `<orca> terminal send --terminal
288
- <handle> --text …`, with the return inside the text and no `--enter`:
295
+ `orca` can fail. Send each key as one `<orca> terminal send --terminal
296
+ <handle> --text …`, with the return inside the text and no `--enter`. Where
297
+ the Send cell names `obk` commands, send no keys: the kit answers that
298
+ screen, by the commands below, the first for a long-lived session and the
299
+ second for a temporary one.
289
300
 
290
301
  | On screen | Send | Which is | Proven on |
291
302
  |---|---|---|---|
292
303
  | Claude Code's trust list | `\x1b[B\r` | down, return: off **No, exit** | Claude Code 2.1.283 |
293
304
  | Codex's `Trust this folder?` | `\r` | return: it starts on yes | Codex 0.160.0 |
294
- | Codex's `Hooks need review` | `\x1b[B\r` | **Trust all and continue** | Codex 0.160.0 |
305
+ | Codex's hooks review | `obk session trust-hooks`, `obk temp trust-hooks` | trust | Codex 0.160.0 |
295
306
  | Codex's `Where should the new conversation run?` | `\r` | **Current checkout** | Codex 0.160.0 |
296
307
  | Codex's update offer, `1. Update now` | `\r` | **Update now**, where it starts | Codex 0.156.1 |
297
308
  | `[oh-my-zsh] Would you like to update?` | `n` | no: the user updates it | oh-my-zsh bf77e35 |
298
- | Claude Code's `Teach auto mode…` | `\x1b` | Esc: Not now; no return | Not yet proven, below |
299
- | The same, 2.1.289's list | `\x1b[B\r` | down, return: **2. Not now** | Not yet proven, below |
309
+ | Claude Code's Teach form | `obk session answer`, `obk temp answer` | Esc | Not yet proven, below |
310
+ | Its 2.1.289 list | `obk session answer`, `obk temp answer` | 2. Not now | Not yet proven, below |
300
311
 
301
312
  Each key counts on where the selection starts on the version named. Read the
302
313
  screen before you send: if the selection is elsewhere or the options have
303
314
  moved, go to the answer with the arrows, then return. Never a digit and then
304
315
  return: on Codex's update offer that took the highlighted **Update now**,
305
316
  whatever the digit was. Codex's trust said `1. Yes, continue` on older
306
- versions. Its hooks review sometimes starts on `2` already: then return alone.
317
+ versions.
307
318
  Its update offer was proven on 0.156.1; 0.157.1 showed it with the selection on
308
319
  Update now, not pressed, and 0.158.0 offered 0.160.0, taken on 2026-10-02 by
309
320
  Bot Father's restarts.
@@ -314,8 +325,10 @@ shell history. Esc cancels it (Not now) in its code; that is not yet proven
314
325
  live, though the form was seen live twice on 2.1.283. On 2.1.278 it was a
315
326
  numbered list, answered with `2. Not now`, and on 2.1.289 it is one again:
316
327
  `1. Yes`, `2. Not now`, `3. Don't show again`, above the input box, its
317
- selection on 1. Yes (seen live, not answered, in #489). Go down to 2. Not now,
318
- read the screen, and only then return. Codex's hooks
328
+ selection on 1. Yes (seen live, not answered, in #489). The kit goes down to
329
+ 2. Not now, reads the screen, and only then returns. On 2.1.302 it is the
330
+ form again, with `Also scan shell history` on `false` and a foot that starts
331
+ `←/→ to change` (seen live in #604); the kit sends it Esc too. Codex's hooks
319
332
  question matters most. The kit's hook is how the book learns which
320
333
  conversation the session is running, and until it is answered the
321
334
  conversation has not started. Codex's trust applies to the repository root,
@@ -324,15 +337,15 @@ conversation runs (0.156.1, 0.157.1, 0.158.0 and 0.160.0 all did); the answer is
324
337
  always the current checkout, the bot home, and never `2. New worktree`:
325
338
  the kit never makes a git worktree.
326
339
 
327
- The hooks review of a Codex temporary session you made yourself is the one
328
- exception: answer it with `obk temp trust-hooks --bots <bots> --name
340
+ The hooks review of a Codex temporary session you made yourself is answered
341
+ through the kit: answer it with `obk temp trust-hooks --bots <bots> --name
329
342
  <session>`, not with the keys. It reads the screen, chooses **Trust all and
330
343
  continue** only if the review is there, and checks that the review went. A
331
344
  permission rule can allow that one command, where it cannot allow keys sent
332
345
  into any tab. The same goes for Claude Code's `Teach auto mode` screen on a
333
346
  temporary session you made: answer it with `obk temp answer --bots <bots>
334
347
  --name <session>`. It answers only a shape in the table exactly as the kit
335
- knows it (Esc on the 2.1.283 form, 2. Not now on the 2.1.289 list, the return
348
+ knows it (Esc on the 2.1.283 and 2.1.302 forms, 2. Not now on the 2.1.289 list, the return
336
349
  only after a second look), refuses any other screen, and checks that it went. The kit's
337
350
  default set allows it, as it allows `obk temp trust-hooks`.
338
351
 
@@ -171,9 +171,10 @@ For a bot with its own memory, the report gets a short section:
171
171
  - **A proposed cut**, when it is near or over the limit: which entries could go
172
172
  or be merged, and why.
173
173
 
174
- A contradiction is not yours to fix. Send it to that bot's own session, as Bot
175
- Father's management session, whose pass this is: `obk message to --from
176
- bot-father/<its session> --to <bot>/<session>` gives the road. Where the road
174
+ A contradiction is not yours to fix. Send it to that bot's own session, from
175
+ your own session, for Bot Father's pass: `obk message to --to <bot>/<session>`,
176
+ run in your own tab, gives the road. In a session's tab the kit sends only as
177
+ that session (#570), so name no other with `--from`. Where the road
177
178
  is the mailbox, `obk message send` carries it, whatever you run on. Where it is
178
179
  an address, that road is Claude Code's own messaging, and it is yours only if
179
180
  you run on Claude Code yourself; if you do not, put the finding in your report
package/src/book.js CHANGED
@@ -6,8 +6,9 @@
6
6
  // theirs. Beside each session's tab it holds the harness session id that
7
7
  // session is running under, and every id it ran under before.
8
8
 
9
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
9
+ import { closeSync, constants, existsSync, fstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import path from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
11
12
  import { DatabaseSync } from 'node:sqlite';
12
13
  import { isDeepStrictEqual } from 'node:util';
13
14
  import { parse, stringify } from 'yaml';
@@ -85,11 +86,18 @@ function withHistoryListed(entry) {
85
86
  * is, never half of it, and a writer that dies leaves the old file in place:
86
87
  * the new text goes to a file of this process's own and is moved over the book,
87
88
  * which is one operation the file system either did or did not do.
89
+ *
90
+ * Whatever is already at that file's name is taken away and the file is made
91
+ * new, never written through: a session can leave a link there in its own bot
92
+ * folder, which would take the book's text out of it, and the move would make
93
+ * the book that link (#599). A link made there in between makes the write fail,
94
+ * and the old book stays whole.
88
95
  */
89
96
  export function writeBook(home, book) {
90
97
  const file = bookFile(home);
91
98
  const half = `${file}.${process.pid}.part`;
92
- writeFileSync(half, `${HEADER}${stringify(book)}`);
99
+ rmSync(half, { force: true });
100
+ writeFileSync(half, `${HEADER}${stringify(book)}`, { flag: 'wx' });
93
101
  renameSync(half, file);
94
102
  }
95
103
 
@@ -171,17 +179,40 @@ function lockOn(file, waitMs, how = 'IMMEDIATE') {
171
179
  // Only while this process holds no turn on it: closing any descriptor of a
172
180
  // file lets go of every lock the process holds on that file, and SQLite's
173
181
  // own are among them. A file a turn is held on was written already.
182
+ //
183
+ // A session can write its bot's folder of locks, so a link, a FIFO or a
184
+ // folder may sit at the lock's name, and a lock taken through a link would
185
+ // make or write a file anywhere the kit may write (#605). So the kit opens
186
+ // the file itself without following a link, and SQLite opens it only if it
187
+ // is there and makes nothing. A link put at the name only while SQLite
188
+ // opens it is followed, and SQLite names the file it opened, so a file of
189
+ // any other name is closed before it is locked or written. And the turn
190
+ // ends with ROLLBACK, so SQLite never writes the file it locks.
191
+ //
192
+ // What is left, accepted by the architect on #605: SQLite reads the first
193
+ // 100 bytes of the file it opens before the kit can ask its name, so such a
194
+ // link can have a file outside read into SQLite's memory and dropped; and a
195
+ // FIFO put there that instant can hang the command. A lock taken by name
196
+ // cannot close that without a lock Node does not have.
174
197
  if (!heldHere.has(file)) {
175
198
  try {
176
199
  mkdirSync(path.dirname(file), { recursive: true });
177
- closeSync(openSync(file, 'a'));
200
+ plainFile(file);
178
201
  } catch (error) {
179
202
  throw new LockNotWritable(file, error);
180
203
  }
181
204
  }
182
205
 
183
- const db = new DatabaseSync(file);
206
+ let db;
207
+ try {
208
+ db = new DatabaseSync(new URL(`${pathToFileURL(file).href}?mode=rw`));
209
+ } catch (error) {
210
+ throw new LockNotWritable(file, error);
211
+ }
184
212
  try {
213
+ if (db.location() !== path.join(realpathSync(path.dirname(file)), path.basename(file))) {
214
+ throw new LockNotWritable(file, notPlain(`a link at it led SQLite to ${db.location()}`));
215
+ }
185
216
  // A writer that arrives while another is working waits for it rather than
186
217
  // failing at once, and gives up saying so rather than waiting for ever.
187
218
  db.exec(`PRAGMA busy_timeout = ${waitMs}`);
@@ -201,7 +232,7 @@ function lockOn(file, waitMs, how = 'IMMEDIATE') {
201
232
  return {
202
233
  release() {
203
234
  try {
204
- db.exec('COMMIT');
235
+ db.exec('ROLLBACK');
205
236
  } finally {
206
237
  db.close();
207
238
  const left = heldHere.get(file) - 1;
@@ -212,6 +243,30 @@ function lockOn(file, waitMs, how = 'IMMEDIATE') {
212
243
  };
213
244
  }
214
245
 
246
+ /**
247
+ * Make the lock file at `file` if it is not there, and open it for writing
248
+ * without following a link or waiting on a FIFO. Anything at the name but a
249
+ * plain file is refused.
250
+ */
251
+ function plainFile(file) {
252
+ let fd;
253
+ try {
254
+ fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK);
255
+ } catch (error) {
256
+ // A link, a folder and a FIFO with no reader, in that order.
257
+ if (['ELOOP', 'EISDIR', 'ENXIO'].includes(error.code)) throw notPlain(error.code);
258
+ throw error;
259
+ }
260
+ try {
261
+ if (!fstatSync(fd).isFile()) throw notPlain('not a plain file');
262
+ } finally {
263
+ closeSync(fd);
264
+ }
265
+ }
266
+
267
+ const NOT_PLAIN = 'ENOTPLAIN';
268
+ const notPlain = (message) => Object.assign(new Error(message), { code: NOT_PLAIN });
269
+
215
270
  /** How many turns this process holds on each lock file, by its path. */
216
271
  const heldHere = new Map();
217
272
 
@@ -256,11 +311,36 @@ const botLocks = (home) => {
256
311
  */
257
312
  class LockNotWritable extends Error {
258
313
  constructor(file, why) {
259
- super(`the kit cannot write its lock ${file} (${why.code ?? why.message}), so it did not take the turn, and nothing was done. A Codex session started before this version of the kit cannot write its bot's folder of locks: restart this session with the kit. Elsewhere, make ${path.dirname(file)} writable.`);
314
+ super(why.code === NOT_PLAIN
315
+ ? `something other than a plain file is at the name of the kit's lock ${file} (${why.message}), so the kit did not take the turn, and nothing was done. Remove ${file}; the kit makes it again.`
316
+ : `the kit cannot write its lock ${file} (${why.code ?? why.message}), so it did not take the turn, and nothing was done. A Codex session started before this version of the kit cannot write its bot's folder of locks: restart this session with the kit. Elsewhere, make ${path.dirname(file)} writable.`);
260
317
  this.code = why.code;
261
318
  }
262
319
  }
263
320
 
321
+ /**
322
+ * The right to change the bot's `bot.yaml` (#595), held from the read a command
323
+ * makes its change on until that change is written. Two commands that change
324
+ * one bot at the same time then each change what the other left, and neither
325
+ * change is lost.
326
+ *
327
+ * The book's kind of lock, on a file of its own beside the book's: `temp make`
328
+ * writes bot.yaml while it holds the book's, and nothing takes the book's while
329
+ * it holds this one, so neither waits on the other.
330
+ */
331
+ export function takeBotTurn(home) {
332
+ const file = botLockFile(home);
333
+ try {
334
+ return lockOn(file, WAIT_MS);
335
+ } catch (error) {
336
+ if (error instanceof LockNotWritable) throw error;
337
+ throw new Error(`waited ${WAIT_MS / 1000} seconds for something else to finish writing ${path.join(home, 'bot.yaml')} and it did not (${error.message}). bot.yaml was not changed. Try again; if nothing is running, remove ${file}, which is where writers take their turn.`);
338
+ }
339
+ }
340
+
341
+ /** Beside the book's lock, with the bot's name: the lock files of its sessions carry one more part. */
342
+ const botLockFile = (home) => path.join(botLocks(home), `${encodeURIComponent(path.basename(realpathSync(home)))}.bot.lock`);
343
+
264
344
  /**
265
345
  * One session's turn at its mailbox (#321): held by `session mailbox` for its
266
346
  * whole step, by `message check` in the session's own tab for its bind, read and
@@ -468,10 +548,11 @@ export function rememberSession(entry = {}, id, ended) {
468
548
 
469
549
  /**
470
550
  * The session entry once the id it held has turned out to have no conversation
471
- * behind it: the id goes into the history with that reason, and the entry holds
472
- * none until the harness names the new one.
551
+ * behind it on the session's harness: the id goes into the history with that
552
+ * reason, and anything else worth knowing about it, such as the harness it ran
553
+ * on, and the entry holds none until the harness names the new one.
473
554
  */
474
- export function forgetSession(entry, ended) {
555
+ export function forgetSession(entry, ended, more = {}) {
475
556
  const { session, ...rest } = entry;
476
- return { ...rest, history: [...(entry.history ?? []), { session, ended, at: new Date().toISOString() }] };
557
+ return { ...rest, history: [...(entry.history ?? []), { session, ended, ...more, at: new Date().toISOString() }] };
477
558
  }