@letrquan/book 0.2.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 (177) hide show
  1. package/CHANGELOG.md +3771 -40
  2. package/README.md +135 -1311
  3. package/dist/additional-roots-VAWRQ625.js +32 -0
  4. package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-2AAOF5B5.js +28 -0
  7. package/dist/chunk-2AAOF5B5.js.map +1 -0
  8. package/dist/chunk-32WBCUAN.js +660 -0
  9. package/dist/chunk-32WBCUAN.js.map +1 -0
  10. package/dist/chunk-4DKHLZRI.js +120 -0
  11. package/dist/chunk-4DKHLZRI.js.map +1 -0
  12. package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
  13. package/dist/chunk-62EEAUID.js.map +1 -0
  14. package/dist/chunk-AHC7OU75.js +1124 -0
  15. package/dist/chunk-AHC7OU75.js.map +1 -0
  16. package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
  17. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  18. package/dist/chunk-B5FC3MYZ.js +395 -0
  19. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  20. package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
  21. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  22. package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
  23. package/dist/chunk-E5WLQSKA.js.map +1 -0
  24. package/dist/chunk-ECLC4CIJ.js +1181 -0
  25. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  26. package/dist/chunk-EEX6QNXV.js +127 -0
  27. package/dist/chunk-EEX6QNXV.js.map +1 -0
  28. package/dist/chunk-EFXVSQ5J.js +931 -0
  29. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  30. package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
  31. package/dist/chunk-ENLB7NYV.js.map +1 -0
  32. package/dist/chunk-FPR7SKIE.js +6926 -0
  33. package/dist/chunk-FPR7SKIE.js.map +1 -0
  34. package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
  35. package/dist/chunk-HFB74CLM.js.map +1 -0
  36. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  37. package/dist/chunk-HOZXKOE3.js.map +1 -0
  38. package/dist/chunk-IDYWN37T.js +36 -0
  39. package/dist/chunk-IDYWN37T.js.map +1 -0
  40. package/dist/chunk-J4F2QLM2.js +273 -0
  41. package/dist/chunk-J4F2QLM2.js.map +1 -0
  42. package/dist/chunk-JGF5HLSI.js +1065 -0
  43. package/dist/chunk-JGF5HLSI.js.map +1 -0
  44. package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
  45. package/dist/chunk-KDNWMKXV.js.map +1 -0
  46. package/dist/chunk-KXAYUIKP.js +90 -0
  47. package/dist/chunk-KXAYUIKP.js.map +1 -0
  48. package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
  49. package/dist/chunk-MEOB2DZA.js.map +1 -0
  50. package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
  51. package/dist/chunk-MV5PQNIA.js.map +1 -0
  52. package/dist/chunk-N2G4Q7ME.js +2672 -0
  53. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  54. package/dist/chunk-OJBMDM5H.js +299 -0
  55. package/dist/chunk-OJBMDM5H.js.map +1 -0
  56. package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
  57. package/dist/chunk-PTDC7ZUP.js.map +1 -0
  58. package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
  59. package/dist/chunk-REASC2QG.js.map +1 -0
  60. package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
  61. package/dist/chunk-RT5SATF7.js.map +1 -0
  62. package/dist/chunk-SBTU5BEC.js +114 -0
  63. package/dist/chunk-SBTU5BEC.js.map +1 -0
  64. package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
  65. package/dist/chunk-SWPDKLNM.js.map +1 -0
  66. package/dist/chunk-UCFJ43OS.js +520 -0
  67. package/dist/chunk-UCFJ43OS.js.map +1 -0
  68. package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
  69. package/dist/chunk-W7SLYS3M.js.map +1 -0
  70. package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
  71. package/dist/chunk-WV3O5PGX.js.map +1 -0
  72. package/dist/chunk-XHAO3K7I.js +18 -0
  73. package/dist/chunk-XHAO3K7I.js.map +1 -0
  74. package/dist/chunk-XJRJVXJR.js +1050 -0
  75. package/dist/chunk-XJRJVXJR.js.map +1 -0
  76. package/dist/chunk-YIL6HTJX.js +663 -0
  77. package/dist/chunk-YIL6HTJX.js.map +1 -0
  78. package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
  79. package/dist/chunk-ZPSZZUWF.js.map +1 -0
  80. package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
  81. package/dist/context-C3RAP6K5.js +41 -0
  82. package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
  83. package/dist/index.js +947 -203
  84. package/dist/index.js.map +1 -1
  85. package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
  86. package/dist/interactive-assets-73AQO5WP.js.map +1 -0
  87. package/dist/job-runner.js +166 -111
  88. package/dist/job-runner.js.map +1 -1
  89. package/dist/job-supervisor.d.ts +2 -0
  90. package/dist/job-supervisor.js +121 -0
  91. package/dist/job-supervisor.js.map +1 -0
  92. package/dist/loader-DQNPDKOX.js +27 -0
  93. package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
  94. package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
  95. package/dist/memory-store-I6Z54VUN.js +51 -0
  96. package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
  97. package/dist/openai-compatible-IPUNBJ53.js +16 -0
  98. package/dist/permission-approvals-Q5IMFEZI.js +15 -0
  99. package/dist/permissions-7Y35X6HZ.js +62 -0
  100. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  101. package/dist/sandbox-N2RY72GN.js +42 -0
  102. package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
  103. package/dist/scrollback-GD76GNM4.js.map +1 -0
  104. package/dist/sdk.d.ts +2739 -2069
  105. package/dist/sdk.js +77 -42
  106. package/dist/sdk.js.map +1 -1
  107. package/dist/settings-loader-KDBH45AQ.js +43 -0
  108. package/dist/settings-loader-KDBH45AQ.js.map +1 -0
  109. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  110. package/dist/settings-removed-3ATWJEF3.js.map +1 -0
  111. package/dist/shell-selection-Z5WORZU4.js +31 -0
  112. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  113. package/package.json +25 -24
  114. package/dist/app-MOXXWEOY.js.map +0 -1
  115. package/dist/chunk-24AXE6SP.js.map +0 -1
  116. package/dist/chunk-3U5IBM24.js.map +0 -1
  117. package/dist/chunk-574C6MQR.js.map +0 -1
  118. package/dist/chunk-5GDZ22YP.js +0 -449
  119. package/dist/chunk-5GDZ22YP.js.map +0 -1
  120. package/dist/chunk-5KLVH3PY.js.map +0 -1
  121. package/dist/chunk-5RDRAO4B.js.map +0 -1
  122. package/dist/chunk-7N4J557C.js.map +0 -1
  123. package/dist/chunk-A7JLBK2Y.js +0 -3506
  124. package/dist/chunk-A7JLBK2Y.js.map +0 -1
  125. package/dist/chunk-ANSJPYGI.js.map +0 -1
  126. package/dist/chunk-BTZZUCLI.js +0 -264
  127. package/dist/chunk-BTZZUCLI.js.map +0 -1
  128. package/dist/chunk-C7D4NJ2D.js +0 -217
  129. package/dist/chunk-C7D4NJ2D.js.map +0 -1
  130. package/dist/chunk-CAGOB2N4.js.map +0 -1
  131. package/dist/chunk-DGMQEI6Y.js.map +0 -1
  132. package/dist/chunk-E75IPST5.js.map +0 -1
  133. package/dist/chunk-EYGD6X2T.js +0 -180
  134. package/dist/chunk-EYGD6X2T.js.map +0 -1
  135. package/dist/chunk-GTVB4HEM.js +0 -765
  136. package/dist/chunk-GTVB4HEM.js.map +0 -1
  137. package/dist/chunk-J7MIZ5CA.js.map +0 -1
  138. package/dist/chunk-KWHQ2XXL.js.map +0 -1
  139. package/dist/chunk-L6LK2QIQ.js +0 -657
  140. package/dist/chunk-L6LK2QIQ.js.map +0 -1
  141. package/dist/chunk-MAQXCUR4.js +0 -1007
  142. package/dist/chunk-MAQXCUR4.js.map +0 -1
  143. package/dist/chunk-S4XL7HOM.js +0 -402
  144. package/dist/chunk-S4XL7HOM.js.map +0 -1
  145. package/dist/chunk-URLQAXTR.js.map +0 -1
  146. package/dist/chunk-V4BOYR52.js.map +0 -1
  147. package/dist/chunk-VZQXX3WW.js.map +0 -1
  148. package/dist/chunk-WKBO5R4O.js.map +0 -1
  149. package/dist/chunk-XIFFTATU.js.map +0 -1
  150. package/dist/context-ZC5IFEAX.js +0 -25
  151. package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
  152. package/dist/loader-4IDS2RIE.js +0 -15
  153. package/dist/openai-compatible-RHZUBHV6.js +0 -10
  154. package/dist/permission-approvals-76PVREFD.js +0 -14
  155. package/dist/permissions-CHCKTFQQ.js +0 -28
  156. package/dist/sandbox-CNVAQDA7.js +0 -23
  157. package/dist/scrollback-LUKLIBVI.js.map +0 -1
  158. package/dist/settings-loader-M2QP5TX2.js +0 -21
  159. package/dist/shell-selection-V6QAYXPX.js +0 -17
  160. package/patches/ink+6.8.0.patch +0 -13
  161. package/scripts/apply-ink-patch.mjs +0 -29
  162. package/scripts/ink-patch.mjs +0 -59
  163. package/scripts/verify-ink-patch.mjs +0 -11
  164. /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  165. /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  166. /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
  167. /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  168. /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
  169. /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  170. /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  171. /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  172. /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  173. /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  174. /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  175. /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  176. /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  177. /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
@@ -5,67 +5,116 @@ import {
5
5
  runHostReview,
6
6
  toElicitationFields,
7
7
  validateElicitationContent
8
- } from "./chunk-V4BOYR52.js";
8
+ } from "./chunk-ENLB7NYV.js";
9
9
  import {
10
+ SessionRuntime,
10
11
  buildAgentCompletionMessage,
11
12
  buildPlanApprovalQuestion,
13
+ classifyAbortReason,
12
14
  createAgentRunContext,
13
15
  createSessionHistoryTools,
14
16
  createTerminalOutcome,
17
+ finalAnswerText,
15
18
  getOrCreateAgentManager,
19
+ maskAtGate,
16
20
  normalizePersistedTodos,
17
21
  planApprovalFromUserQuestionResponse,
18
22
  planStopDecision,
19
- shouldCompact,
23
+ seedShellIds,
24
+ separateInlineReasoning,
20
25
  takeAgentCompletionBatch,
21
- usagePressureTokens
22
- } from "./chunk-XIFFTATU.js";
26
+ uniqueAgentDisplayName
27
+ } from "./chunk-KDNWMKXV.js";
23
28
  import {
29
+ TRUSTED_SETTINGS_LOCATION,
30
+ collectSkippedInstructionNotices,
31
+ collectSkippedReviewNotices,
32
+ compactionGate,
24
33
  createBuiltinCommandRegistry,
25
34
  estimateUsageCost,
35
+ formatGoalStatus,
26
36
  getPackageVersion,
37
+ goalKickoffPrompt,
27
38
  planWasLost,
28
39
  resolveContextLimit,
29
- resolvePermissionMode
30
- } from "./chunk-A7JLBK2Y.js";
40
+ resolvePermissionMode,
41
+ usageAtGate,
42
+ usagePressureTokens
43
+ } from "./chunk-FPR7SKIE.js";
44
+ import {
45
+ onAbort,
46
+ seedObservationLedger,
47
+ throwIfAborted
48
+ } from "./chunk-N2G4Q7ME.js";
49
+ import {
50
+ formatIgnoredWorkspaceSandboxKey,
51
+ ignoredWorkspaceSandboxKeys,
52
+ isUserSettingsFile,
53
+ loadSettingsFile,
54
+ settingsLayerPaths,
55
+ workspaceLayerThatEnabledSandbox
56
+ } from "./chunk-UCFJ43OS.js";
31
57
  import {
58
+ collectSkippedCommandNotices,
32
59
  commandEnforcementContext,
33
60
  discoverCommands,
34
61
  parseSlashInput,
35
62
  resolveCommandBody
36
- } from "./chunk-E75IPST5.js";
63
+ } from "./chunk-AJZWLKLQ.js";
37
64
  import {
38
65
  consoleMcpDiagnosticSink,
39
66
  resolveMcpServerList
40
- } from "./chunk-3U5IBM24.js";
67
+ } from "./chunk-MEOB2DZA.js";
68
+ import {
69
+ isProcessAlive
70
+ } from "./chunk-32WBCUAN.js";
71
+ import {
72
+ buildChildEnv
73
+ } from "./chunk-XHAO3K7I.js";
74
+ import {
75
+ collectDeclaredDirectories,
76
+ partitionProjectDirectories
77
+ } from "./chunk-4DKHLZRI.js";
41
78
  import {
42
- observationKey,
79
+ getPrimaryArg,
43
80
  toolFailure,
81
+ toolResultErrorMessage,
44
82
  toolSuccess
45
- } from "./chunk-5GDZ22YP.js";
46
- import {
47
- loadSettingsFile
48
- } from "./chunk-BTZZUCLI.js";
83
+ } from "./chunk-YIL6HTJX.js";
49
84
  import {
50
85
  partitionProjectAllowRules
51
- } from "./chunk-WKBO5R4O.js";
86
+ } from "./chunk-MV5PQNIA.js";
87
+ import {
88
+ LEGACY_BASE_URL_ID,
89
+ describeApiKeyDeclaration,
90
+ describeDroppedOverride,
91
+ describeOrigin,
92
+ describeReservedProvider,
93
+ describeUnaffectedSelection,
94
+ describeWithheldSelection,
95
+ providerTrustCommand
96
+ } from "./chunk-B5FC3MYZ.js";
52
97
  import {
53
98
  collectDeclaredHooks,
54
- partitionProjectHooks
55
- } from "./chunk-ANSJPYGI.js";
99
+ partitionProjectHooks,
100
+ renderUntrustedText
101
+ } from "./chunk-WV3O5PGX.js";
56
102
  import {
57
103
  resolveBookHome
58
- } from "./chunk-24AXE6SP.js";
104
+ } from "./chunk-ZPSZZUWF.js";
59
105
 
60
106
  // src/run-status.ts
61
107
  import {
62
108
  closeSync,
63
109
  mkdirSync,
64
110
  openSync,
111
+ readdirSync,
65
112
  readFileSync,
66
113
  renameSync,
67
114
  rmSync,
68
115
  statfsSync,
116
+ statSync,
117
+ unlinkSync,
69
118
  writeSync
70
119
  } from "fs";
71
120
  import { join } from "path";
@@ -115,6 +164,43 @@ function readRunStatus(sessionId, home = resolveBookHome()) {
115
164
  return void 0;
116
165
  }
117
166
  }
167
+ var RUN_RECORD_NAME = /\.json$/;
168
+ var RUN_TEMP_NAME = /\.json\..+\.tmp$/;
169
+ function recordsLiveRun(path) {
170
+ try {
171
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
172
+ if (!parsed || typeof parsed !== "object") return false;
173
+ const record = parsed;
174
+ if (record.terminal || record.crash) return false;
175
+ const pid = record.pid;
176
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
177
+ return isProcessAlive(pid);
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+ function cleanupRunStatus(days, home = resolveBookHome(), now = Date.now()) {
183
+ const cutoff = now - Math.max(1, days) * 864e5;
184
+ const directory = runStatusDir(home);
185
+ let removed = 0;
186
+ try {
187
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
188
+ if (!entry.isFile()) continue;
189
+ const isRecord = RUN_RECORD_NAME.test(entry.name);
190
+ if (!isRecord && !RUN_TEMP_NAME.test(entry.name)) continue;
191
+ const path = join(directory, entry.name);
192
+ try {
193
+ if (statSync(path).mtimeMs >= cutoff) continue;
194
+ if (isRecord && recordsLiveRun(path)) continue;
195
+ unlinkSync(path);
196
+ removed++;
197
+ } catch {
198
+ }
199
+ }
200
+ } catch {
201
+ }
202
+ return removed;
203
+ }
118
204
  var RunStatusWriter = class {
119
205
  seed;
120
206
  last;
@@ -249,6 +335,10 @@ async function performPrintReview(effect, env, command) {
249
335
  // Every reviewer, lens and verifier agent is spawned under the host's run,
250
336
  // so the whole pipeline is billed and budgeted as one root.
251
337
  runner: reviewRunnerFor(env.agents.manager(), {
338
+ // The stored session the run belongs to, as the TUI passes its own. Without it
339
+ // each agent's tool-use records carry a made-up `agent:<id>` session id that
340
+ // matches no session file, and `book tool-stats` counts every one separately.
341
+ parentSessionId: env.agents.runContext.sessionId,
252
342
  rootRunId: env.agents.runContext.rootRunId,
253
343
  parentRunId: env.agents.runContext.runId
254
344
  })
@@ -265,12 +355,40 @@ function createPrintEffectRegistry() {
265
355
  return new PrintEffectRegistry().register("send-prompt", (effect) => ({
266
356
  kind: "prompt",
267
357
  prompt: effect.prompt,
268
- commandContext: effect.context
358
+ commandContext: effect.context,
359
+ projectCommand: effect.projectCommand
269
360
  })).register("local-message", (effect, _env, command) => {
270
361
  if (effect.isError) throw new PrintCommandUsageError(command, effect.content);
271
362
  return { kind: "handled", command, output: effect.content };
272
- }).register("review", performPrintReview);
363
+ }).register("review", performPrintReview).register("set-goal", (effect, env, command) => {
364
+ if (!env.goal) throw new UnsupportedPrintCommandError(command, GOAL_REFUSAL);
365
+ env.goal.set(effect.condition);
366
+ const kickoff = goalKickoffPrompt(effect.condition);
367
+ return {
368
+ kind: "prompt",
369
+ prompt: kickoff,
370
+ commandContext: {
371
+ command: {
372
+ name: "goal",
373
+ description: "Set a goal Book checks before stopping",
374
+ body: kickoff,
375
+ source: "project"
376
+ },
377
+ resolvedBody: kickoff
378
+ },
379
+ sessionNameSource: effect.condition
380
+ };
381
+ }).register("clear-goal", (_effect, env, command) => {
382
+ if (!env.goal) throw new UnsupportedPrintCommandError(command, GOAL_REFUSAL);
383
+ const cleared = env.goal.clear();
384
+ return {
385
+ kind: "handled",
386
+ command,
387
+ output: cleared ? formatGoalStatus({ state: "cleared", condition: cleared.condition }) : "No goal set"
388
+ };
389
+ });
273
390
  }
391
+ var GOAL_REFUSAL = "\u2715 /goal needs a host that checks goals; this one does not.";
274
392
  function printBuiltinContext(env, commands) {
275
393
  return {
276
394
  workspace: env.config.workspace,
@@ -283,6 +401,7 @@ function printBuiltinContext(env, commands) {
283
401
  runtimeConfig: env.config,
284
402
  mode: env.mode,
285
403
  usage: null,
404
+ activeGoal: env.goal?.get(),
286
405
  turnDurationMs: 0,
287
406
  contextHistory: [],
288
407
  compactBoundaries: [],
@@ -299,6 +418,9 @@ function supportedCommandHelp(builtins, commands) {
299
418
  const lines = ["Commands supported in print mode:"];
300
419
  for (const definition of builtins.getDefinitions()) {
301
420
  if (definition.nonInteractive) lines.push(` /${definition.name} \u2014 ${definition.description}`);
421
+ for (const subcommand of definition.nonInteractiveSubcommands ?? []) {
422
+ lines.push(` /${definition.name} ${subcommand} \u2014 ${definition.description}`);
423
+ }
302
424
  }
303
425
  const custom = commands.map((command) => `/${command.name}`);
304
426
  lines.push(
@@ -326,8 +448,10 @@ async function resolvePrintCommand(input, env, deps = {}) {
326
448
  const commands = deps.commands ?? discoverCommands(env.config.workspace);
327
449
  const context = printBuiltinContext(env, commands);
328
450
  const resolved = builtins.resolve(parsed.name, context);
329
- if (resolved) {
330
- if (!resolved.definition.nonInteractive) {
451
+ if (resolved && !resolved.definition.tuiOnly) {
452
+ const subcommand = parsed.rawArguments.trim().split(/\s+/)[0] ?? "";
453
+ const allowedHere = resolved.definition.nonInteractive || (resolved.definition.nonInteractiveSubcommands?.includes(subcommand) ?? false);
454
+ if (!allowedHere) {
331
455
  throw unsupported(
332
456
  parsed.name,
333
457
  "needs an interactive session: it changes host state instead of producing a prompt.",
@@ -356,7 +480,8 @@ async function resolvePrintCommand(input, env, deps = {}) {
356
480
  commands
357
481
  );
358
482
  }
359
- return dispatch.kind === "handled" ? { ...dispatch, command: parsed.name } : dispatch;
483
+ if (dispatch.kind === "handled") return { ...dispatch, command: parsed.name };
484
+ return dispatch.kind === "prompt" ? { ...dispatch, sessionNameSource: dispatch.sessionNameSource ?? input.trim() } : dispatch;
360
485
  }
361
486
  const custom = commands.find((command) => command.name === parsed.name);
362
487
  if (!custom) return { kind: "passthrough", prompt: input };
@@ -375,7 +500,9 @@ async function resolvePrintCommand(input, env, deps = {}) {
375
500
  kind: "prompt",
376
501
  prompt: body,
377
502
  commandContext: commandEnforcementContext(custom, body),
378
- shellErrors
503
+ sessionNameSource: input.trim(),
504
+ shellErrors,
505
+ projectCommand: custom.source === "project"
379
506
  };
380
507
  }
381
508
 
@@ -406,12 +533,17 @@ var EVENT_TYPES = /* @__PURE__ */ new Set([
406
533
  "agent_message",
407
534
  "agent_completion",
408
535
  "agent_permission",
536
+ "agent_notice",
409
537
  "evidence_update",
538
+ "attempt_discarded",
539
+ "retry",
410
540
  "agent_apply",
411
541
  "hook_event",
412
542
  "mode_change",
413
543
  "plan_approval",
544
+ "command_result",
414
545
  "prompt_suggestions",
546
+ "notice",
415
547
  "error",
416
548
  "result",
417
549
  "done"
@@ -490,11 +622,71 @@ function createStreamParser(onEvent, options = {}) {
490
622
  };
491
623
  }
492
624
 
625
+ // src/reader-gone.ts
626
+ import { Writable } from "stream";
627
+ var READER_GONE_CODES = /* @__PURE__ */ new Set([
628
+ "EPIPE",
629
+ "EOF",
630
+ "ERR_STREAM_DESTROYED",
631
+ "ECONNRESET"
632
+ ]);
633
+ function isReaderGoneError(error) {
634
+ const code = error?.code;
635
+ return code !== void 0 && READER_GONE_CODES.has(code);
636
+ }
637
+ var READER_FLUSH_CAP_MS = 1e3;
638
+ function createReaderFlush(stdout, options = {}) {
639
+ if (!(stdout instanceof Writable)) return void 0;
640
+ const capMs = options.capMs ?? READER_FLUSH_CAP_MS;
641
+ const controller = new AbortController();
642
+ let pending = 0;
643
+ let slow = false;
644
+ const waiters = /* @__PURE__ */ new Set();
645
+ const drained = () => {
646
+ if (pending > 0) return;
647
+ slow = false;
648
+ for (const release of [...waiters]) release();
649
+ waiters.clear();
650
+ };
651
+ return {
652
+ signal: controller.signal,
653
+ write(line) {
654
+ pending++;
655
+ stdout.write(line, (error) => {
656
+ if (error && isReaderGoneError(error)) controller.abort();
657
+ pending--;
658
+ drained();
659
+ });
660
+ },
661
+ hold(signal) {
662
+ if (pending === 0 || slow || signal?.aborted) return Promise.resolve();
663
+ return new Promise((resolve) => {
664
+ let settled = false;
665
+ const finish = () => {
666
+ if (settled) return;
667
+ settled = true;
668
+ clearTimeout(timer);
669
+ signal?.removeEventListener("abort", finish);
670
+ waiters.delete(finish);
671
+ resolve();
672
+ };
673
+ const timer = setTimeout(() => {
674
+ slow = true;
675
+ finish();
676
+ }, capMs);
677
+ waiters.add(finish);
678
+ signal?.addEventListener("abort", finish, { once: true });
679
+ if (signal?.aborted) finish();
680
+ });
681
+ }
682
+ };
683
+ }
684
+
493
685
  // src/headless.ts
494
686
  function carriedCostUsd(usage, models) {
495
687
  let worst = null;
496
688
  for (const model of models) {
497
- const quote = estimateUsageCost(model, usage);
689
+ const quote = estimateUsageCost(model, usage, "pool");
498
690
  if (quote.status !== "known") return null;
499
691
  worst = worst === null ? quote.costUsd : Math.max(worst, quote.costUsd);
500
692
  }
@@ -509,26 +701,69 @@ async function readPipedPrompt(stream) {
509
701
  const prompt = chunks.join("").trim();
510
702
  return prompt || void 0;
511
703
  }
704
+ function destroyOnAbort(stream, signal) {
705
+ if (!signal) return () => {
706
+ };
707
+ return onAbort(signal, () => {
708
+ const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
709
+ stream.destroy?.(reason);
710
+ });
711
+ }
712
+ async function raceAbort(promise, signal) {
713
+ if (!signal) {
714
+ await promise;
715
+ return true;
716
+ }
717
+ if (signal.aborted) return false;
718
+ let release;
719
+ const aborted = new Promise((resolve) => {
720
+ release = onAbort(signal, () => resolve(false));
721
+ });
722
+ try {
723
+ return await Promise.race([promise.then(() => true), aborted]);
724
+ } finally {
725
+ release?.();
726
+ }
727
+ }
512
728
  async function runHeadless(config, registry, opts) {
513
729
  const mode = resolvePermissionMode(config.settings, opts.mode);
514
730
  const stdout = opts.stdout ?? process.stdout;
731
+ const flush = opts.outputFormat === "stream-json" ? createReaderFlush(stdout) : void 0;
732
+ const runSignal = flush && opts.signal ? AbortSignal.any([opts.signal, flush.signal]) : flush?.signal ?? opts.signal;
515
733
  const emit = (obj) => {
516
734
  if (obj && typeof obj === "object" && "type" in obj) {
517
735
  opts.onEvent?.(obj);
518
736
  }
519
- stdout.write(JSON.stringify(obj) + "\n");
737
+ const line = JSON.stringify(obj) + "\n";
738
+ if (flush) flush.write(line);
739
+ else stdout.write(line);
740
+ };
741
+ const eventState = {
742
+ childLabels: /* @__PURE__ */ new Map(),
743
+ errorReported: false
520
744
  };
521
745
  let disposeCrashHandlers;
522
- const agentSession = new AgentSession();
746
+ let sessionDisposed = false;
747
+ let startedSessionId;
748
+ const agentSession = new AgentSession({
749
+ runtime: new SessionRuntime()
750
+ });
751
+ const disposeSession = () => {
752
+ if (sessionDisposed) return;
753
+ sessionDisposed = true;
754
+ agentSession.dispose("headless_complete");
755
+ };
523
756
  const runtime = agentSession.getRuntime();
524
757
  runtime.todos.push(...normalizePersistedTodos(opts.plan?.todos));
525
758
  if (opts.plan?.tasks?.length) runtime.tasks.push(...opts.plan.tasks);
759
+ agentSession.restoreGoal(opts.goal);
526
760
  runtime.planUnrestored = planWasLost({
527
761
  planRecordExisted: opts.plan !== void 0,
528
762
  priorMessages: opts.history.length,
529
763
  todos: runtime.todos.length,
530
764
  tasks: runtime.tasks.length
531
765
  });
766
+ seedShellIds(runtime.backgroundShells, opts.transcript ?? opts.history, opts.history);
532
767
  try {
533
768
  if (opts.outputFormat === "stream-json") {
534
769
  emit({ type: "system", model: config.model, cwd: config.workspace });
@@ -544,7 +779,11 @@ async function runHeadless(config, registry, opts) {
544
779
  let sessionName = opts.sessionName;
545
780
  let sessionCreated = opts.sessionCreated === true;
546
781
  if (store && !sessionId) {
547
- sessionId = store.create({ cwd: config.workspace, name: sessionName });
782
+ sessionId = store.create({
783
+ cwd: config.workspace,
784
+ name: sessionName,
785
+ driver: config.sessionDriver
786
+ });
548
787
  sessionCreated = true;
549
788
  }
550
789
  if (store && sessionId && !sessionName) {
@@ -567,6 +806,7 @@ async function runHeadless(config, registry, opts) {
567
806
  onHookEvent: createHookEventHandler(opts, emit)
568
807
  }
569
808
  );
809
+ startedSessionId = sessionId;
570
810
  }
571
811
  const runtimeSessionId = sessionId ?? crypto.randomUUID();
572
812
  const statusWriter = new RunStatusWriter({
@@ -577,7 +817,10 @@ async function runHeadless(config, registry, opts) {
577
817
  startedAt: Date.now()
578
818
  });
579
819
  disposeCrashHandlers = statusWriter.installCrashHandlers();
580
- const permissionRequired = async () => "deny";
820
+ const permissionRequired = async () => ({
821
+ result: "deny",
822
+ reason: "no_approver"
823
+ });
581
824
  const userQuestionStatus = opts.onUserQuestionRequired ? "pending" : "unavailable";
582
825
  const askUser = async (request, context) => {
583
826
  try {
@@ -596,14 +839,26 @@ async function runHeadless(config, registry, opts) {
596
839
  if (mode === "bypassPermissions") return "approve";
597
840
  if (!opts.onUserQuestionRequired) return planStopDecision("approval_unavailable");
598
841
  const request = buildPlanApprovalQuestion(plan, crypto.randomUUID());
599
- emitAgentEvent({ type: "user_question", request, status: userQuestionStatus }, opts, emit);
600
- const response = await askUser(request, { signal: opts.signal });
601
- emitAgentEvent({ type: "user_question_result", requestId: request.id, response }, opts, emit);
842
+ emitAgentEvent(
843
+ { type: "user_question", request, status: userQuestionStatus },
844
+ opts,
845
+ emit,
846
+ eventState
847
+ );
848
+ const response = await askUser(request, { signal: runSignal });
849
+ emitAgentEvent(
850
+ { type: "user_question_result", requestId: request.id, response },
851
+ opts,
852
+ emit,
853
+ eventState
854
+ );
602
855
  return planApprovalFromUserQuestionResponse(request, response);
603
856
  };
604
857
  const planNotApplied = {};
605
858
  let lastUsage = null;
606
859
  let lastOutcome = null;
860
+ let mayReclassify = true;
861
+ let finalRunOpeningId;
607
862
  const runResults = [];
608
863
  const recordRunResult = (result2) => {
609
864
  const existing = runResults.findIndex(
@@ -668,15 +923,7 @@ async function runHeadless(config, registry, opts) {
668
923
  const contextHistory = [...opts.history];
669
924
  const transcript = [...opts.transcript ?? opts.history];
670
925
  const compactBoundaries = [...opts.compactBoundaries ?? []];
671
- for (const message of transcript) {
672
- for (const observation of message.fileObservations ?? []) {
673
- const key = observationKey(observation.workspaceId, observation.path);
674
- const current = runtime.fileObservationLedger.get(key);
675
- if (!current || current.timestamp <= observation.timestamp) {
676
- runtime.fileObservationLedger.set(key, observation);
677
- }
678
- }
679
- }
926
+ seedObservationLedger(runtime.fileObservationLedger, transcript);
680
927
  const loopConfig = {
681
928
  ...config,
682
929
  maxTurns: opts.maxTurns ?? config.maxTurns
@@ -688,7 +935,7 @@ async function runHeadless(config, registry, opts) {
688
935
  lastOutcome = event.outcome;
689
936
  onOutcome(event.outcome);
690
937
  }
691
- emitAgentEvent(event, opts, emit);
938
+ emitAgentEvent(event, opts, emit, eventState);
692
939
  },
693
940
  // No header rewrite at turn boundaries: load() derives updatedAt from
694
941
  // the last appended record, so the on-disk header is never authoritative.
@@ -702,6 +949,15 @@ async function runHeadless(config, registry, opts) {
702
949
  emit({ type: "mode_change", mode: newMode });
703
950
  }
704
951
  },
952
+ /**
953
+ * The hold at a tool boundary. The `tool_use` records for the calls about
954
+ * to run have been written by now, so waiting for them to be taken is
955
+ * what turns a reader that has gone into an abort BEFORE the call runs
956
+ * (#340) instead of one call later. Free when the reader has caught up,
957
+ * bounded when it has not, and absent — so a no-op — for every other
958
+ * output format and for a host sink with no callback to answer.
959
+ */
960
+ beforeToolExecution: flush ? () => flush.hold(runSignal) : void 0,
705
961
  onPlanApprovalRequired: async (plan) => {
706
962
  const decision = await decidePlanApproval(plan);
707
963
  if (typeof decision === "object" && decision.decision === "stop") {
@@ -719,6 +975,24 @@ async function runHeadless(config, registry, opts) {
719
975
  },
720
976
  onUserQuestionRequired: askUser,
721
977
  userQuestionStatus,
978
+ onRetry: (phase, attempt, max, delayMs, reason) => {
979
+ const limit = max < 0 ? null : max;
980
+ if (opts.outputFormat === "stream-json") {
981
+ emit({
982
+ type: "retry",
983
+ phase,
984
+ attempt,
985
+ max: limit,
986
+ delay_ms: delayMs,
987
+ ...reason ? { reason } : {}
988
+ });
989
+ } else if (opts.quiet !== true) {
990
+ process.stderr.write(
991
+ `retry: ${phase} attempt ${attempt}${limit !== null ? `/${limit}` : ""} in ${Math.round(delayMs / 1e3)}s${reason ? ` (${reason})` : ""}
992
+ `
993
+ );
994
+ }
995
+ },
722
996
  onHookEvent: createHookEventHandler(opts, emit),
723
997
  onUsage: (nextUsage) => {
724
998
  lastUsage = nextUsage;
@@ -739,7 +1013,50 @@ async function runHeadless(config, registry, opts) {
739
1013
  ...hints,
740
1014
  trigger: "auto",
741
1015
  preContextTokens: usage ? usagePressureTokens(usage) : void 0,
742
- signal: opts.signal,
1016
+ signal: runSignal,
1017
+ onHookEvent: createHookEventHandler(opts, emit)
1018
+ }
1019
+ });
1020
+ if (outcome2.result.status === "compacted") {
1021
+ emitCompactBoundary(opts.outputFormat, emit, outcome2.result);
1022
+ lastUsage = null;
1023
+ }
1024
+ return outcome2.result;
1025
+ },
1026
+ // Deferred compaction (plans/async-compaction-plan.md): same plumbing,
1027
+ // reducer on the snapshot first, judge and record at the next boundary.
1028
+ prepareCompact: async (snapshot, usage, hints) => {
1029
+ const outcome2 = await agentSession.prepareCompact({
1030
+ config,
1031
+ history: snapshot,
1032
+ compactBoundaries,
1033
+ sessionId,
1034
+ transcriptOrdinal: transcript.length,
1035
+ runContext,
1036
+ runtime,
1037
+ options: {
1038
+ ...hints,
1039
+ trigger: "auto",
1040
+ preContextTokens: usage ? usagePressureTokens(usage) : void 0,
1041
+ signal: hints?.signal ?? runSignal,
1042
+ onHookEvent: createHookEventHandler(opts, emit)
1043
+ }
1044
+ });
1045
+ return outcome2.status === "prepared" ? outcome2.prepared : outcome2.result;
1046
+ },
1047
+ commitCompact: async (prepared, history, hints) => {
1048
+ const outcome2 = await agentSession.commitCompact({
1049
+ prepared,
1050
+ history,
1051
+ config,
1052
+ sessionId,
1053
+ transcriptOrdinal: transcript.length,
1054
+ runContext,
1055
+ runtime,
1056
+ timelineStore: store,
1057
+ onCommitted: (_result, boundary) => compactBoundaries.push(boundary),
1058
+ options: {
1059
+ signal: hints?.signal ?? runSignal,
743
1060
  onHookEvent: createHookEventHandler(opts, emit)
744
1061
  }
745
1062
  });
@@ -752,12 +1069,19 @@ async function runHeadless(config, registry, opts) {
752
1069
  onAssistantMessageComplete: (message) => {
753
1070
  if (!transcript.some((item) => item.id === message.id)) transcript.push(message);
754
1071
  if (opts.outputFormat === "stream-json" && message.content) {
755
- emit({ type: "assistant", text: message.content, complete: true });
1072
+ emit({
1073
+ type: "assistant",
1074
+ text: message.content,
1075
+ complete: true,
1076
+ ...message.hostNotice ? { host_notice: true } : {}
1077
+ });
756
1078
  }
757
1079
  }
758
1080
  });
759
- const runParentTurn = async (prompt, displayMessage, userMessage, runContext, commandContext) => {
1081
+ const runParentTurn = async (prompt, displayMessage, userMessage, runContext, commandContext, typedText) => {
1082
+ eventState.errorReported = false;
760
1083
  let runOutcome;
1084
+ finalRunOpeningId = userMessage.id;
761
1085
  lastUsage = null;
762
1086
  let updated;
763
1087
  try {
@@ -770,7 +1094,7 @@ async function runHeadless(config, registry, opts) {
770
1094
  mode,
771
1095
  sessionId: runtimeSessionId,
772
1096
  timelineStore: store,
773
- signal: opts.signal,
1097
+ signal: runSignal,
774
1098
  callbacks: createRunCallbacks(runContext, (outcome2) => {
775
1099
  runOutcome = outcome2;
776
1100
  }),
@@ -783,6 +1107,9 @@ async function runHeadless(config, registry, opts) {
783
1107
  userFileObservations: userMessage.fileObservations,
784
1108
  userAttachments: userMessage.attachments,
785
1109
  userMessageKind: userMessage.kind,
1110
+ // The answer walk is bounded by `openingMessageId`, so a derived opening
1111
+ // message no longer lets it walk past the run.
1112
+ userMessageDerived: userMessage.derivedContent,
786
1113
  manageSessionHooks: sessionId ? false : void 0,
787
1114
  // Completion messages are host-generated notifications, not user
788
1115
  // prompts. Keep UserPromptSubmit hooks from rewriting or blocking them.
@@ -790,6 +1117,8 @@ async function runHeadless(config, registry, opts) {
790
1117
  parentSessionId: runtimeSessionId,
791
1118
  runContext,
792
1119
  runtime,
1120
+ // Only what the user typed invokes a skill by `$name`, as in the TUI.
1121
+ typedText,
793
1122
  // Command frontmatter enforcement, exactly as the TUI passes it.
794
1123
  allowedTools: commandContext?.allowedTools,
795
1124
  modelOverride: commandContext?.modelOverride,
@@ -832,7 +1161,7 @@ async function runHeadless(config, registry, opts) {
832
1161
  config,
833
1162
  mode,
834
1163
  sessionId: runtimeSessionId,
835
- signal: opts.signal,
1164
+ signal: runSignal,
836
1165
  agents: {
837
1166
  // Built only when an agent-backed command (`/review`) asks for it,
838
1167
  // and attached to this session's runtime so it is disposed with the
@@ -843,18 +1172,39 @@ async function runHeadless(config, registry, opts) {
843
1172
  permissionMode: mode,
844
1173
  eventSink: (event) => {
845
1174
  if (event.type === "agent_result") recordManagedRunResult(event.agent);
846
- emitAgentEvent(event, opts, emit);
1175
+ emitAgentEvent(event, opts, emit, eventState);
847
1176
  },
848
1177
  hookEventSink: createHookEventHandler(opts, emit)
849
1178
  }),
850
1179
  runContext
1180
+ },
1181
+ goal: {
1182
+ get: () => agentSession.getGoal(),
1183
+ set: (condition) => {
1184
+ agentSession.setGoal(condition, goalTarget());
1185
+ },
1186
+ clear: () => agentSession.clearGoal(goalTarget())
851
1187
  }
852
1188
  });
1189
+ const goalTarget = () => ({
1190
+ sessionId,
1191
+ timelineStore: store && sessionId ? store : void 0
1192
+ });
853
1193
  const commandResults = [];
854
1194
  let commandUsage = null;
855
1195
  const prompts = [];
856
1196
  if (opts.inputFormat === "text") {
857
- const prompt = opts.prompt ?? await readPipedPrompt(opts.stdin ?? process.stdin);
1197
+ let prompt = opts.prompt;
1198
+ if (prompt === void 0) {
1199
+ const stream = opts.stdin ?? process.stdin;
1200
+ const release = destroyOnAbort(stream, runSignal);
1201
+ try {
1202
+ prompt = await readPipedPrompt(stream);
1203
+ } finally {
1204
+ release();
1205
+ }
1206
+ throwIfAborted(runSignal);
1207
+ }
858
1208
  if (!prompt) {
859
1209
  throw new Error(
860
1210
  "print mode requires a prompt: pass one to -p, pipe one on stdin, or use --input-format stream-json"
@@ -873,9 +1223,15 @@ async function runHeadless(config, registry, opts) {
873
1223
  onDiagnostic: (diagnostic) => diagnostics.push(diagnostic)
874
1224
  }
875
1225
  );
876
- for await (const chunk of stream) {
877
- parser.feed(chunk);
1226
+ const release = destroyOnAbort(stream, runSignal);
1227
+ try {
1228
+ for await (const chunk of stream) {
1229
+ parser.feed(chunk);
1230
+ }
1231
+ } finally {
1232
+ release();
878
1233
  }
1234
+ throwIfAborted(runSignal);
879
1235
  parser.flush();
880
1236
  if (diagnostics.length > 0) throw new Error(diagnostics[0]?.message);
881
1237
  if (opts.prompt) prompts.unshift(opts.prompt);
@@ -896,6 +1252,9 @@ async function runHeadless(config, registry, opts) {
896
1252
  resumedFromRunId: runResults.length === 0 ? opts.resumedFromRunId ?? (opts.history.length > 0 ? [...opts.history].reverse().find((message) => message.role === "user")?.id : void 0) : void 0
897
1253
  });
898
1254
  runtime.runAccounting.startRoot(runContext, opts.maxBudgetUsd);
1255
+ if (store && sessionId) {
1256
+ agentSession.trackRunUsage(runContext.rootRunId, { sessionId, timelineStore: store });
1257
+ }
899
1258
  if (carriedSpend) {
900
1259
  runtime.runAccounting.seedRoot(runContext.rootRunId, carriedSpend);
901
1260
  }
@@ -932,6 +1291,12 @@ async function runHeadless(config, registry, opts) {
932
1291
  } else if (opts.outputFormat === "stream-json" && (dispatch.output !== void 0 || dispatch.data !== void 0)) {
933
1292
  emit({ type: "command_result", ...handled });
934
1293
  }
1294
+ carriedSpend = {
1295
+ usage: accounting.inclusiveUsage,
1296
+ costUsd: accounting.inclusiveCostUsd,
1297
+ persistedUsage: runtime.runAccounting.persistedUsage(runContext.rootRunId),
1298
+ fromRootRunId: runContext.rootRunId
1299
+ };
935
1300
  continue;
936
1301
  }
937
1302
  const prompt = dispatch.prompt;
@@ -948,9 +1313,19 @@ async function runHeadless(config, registry, opts) {
948
1313
  kind: "conversation",
949
1314
  timestamp: Date.now()
950
1315
  };
1316
+ if (config.autoCompactEnabled !== false && usageAtGate(lastUsage, config)) {
1317
+ const pressure = usagePressureTokens(lastUsage);
1318
+ const gate = compactionGate(config);
1319
+ const masked = maskAtGate(contextHistory, gate, pressure, gate);
1320
+ if (masked.maskedCount > 0) {
1321
+ contextHistory.length = 0;
1322
+ contextHistory.push(...masked.history);
1323
+ if (masked.underLine) lastUsage = null;
1324
+ }
1325
+ }
951
1326
  const contextLimit = resolveContextLimit(config);
952
1327
  const hostCompactAttemptKey = `${usagePressureTokens(lastUsage)}:${contextHistory.length}`;
953
- if (config.autoCompactEnabled !== false && contextLimit != null && shouldCompact(lastUsage, contextLimit) && lastHostCompactAttemptKey !== hostCompactAttemptKey) {
1328
+ if (config.autoCompactEnabled !== false && contextLimit != null && usageAtGate(lastUsage, config) && lastHostCompactAttemptKey !== hostCompactAttemptKey) {
954
1329
  lastHostCompactAttemptKey = hostCompactAttemptKey;
955
1330
  try {
956
1331
  const outcome2 = await agentSession.compact({
@@ -971,7 +1346,7 @@ async function runHeadless(config, registry, opts) {
971
1346
  options: {
972
1347
  trigger: "auto",
973
1348
  preContextTokens: usagePressureTokens(lastUsage),
974
- signal: opts.signal,
1349
+ signal: runSignal,
975
1350
  upcomingUserIntent: prompt,
976
1351
  onHookEvent: createHookEventHandler(opts, emit)
977
1352
  }
@@ -988,20 +1363,38 @@ async function runHeadless(config, registry, opts) {
988
1363
  displayMessage: prompt,
989
1364
  userMessage,
990
1365
  sessionName,
1366
+ sessionNameSource: dispatch.kind === "prompt" ? dispatch.sessionNameSource : void 0,
991
1367
  timelineStore: store && sessionId ? store : void 0,
992
1368
  expandShellInput: false,
1369
+ projectCommand: dispatch.kind === "prompt" ? dispatch.projectCommand : void 0,
993
1370
  runtime,
994
- signal: opts.signal
1371
+ signal: runSignal
995
1372
  });
1373
+ for (const notice of recorded.mentionNotices) process.stderr.write(`${notice}
1374
+ `);
996
1375
  sessionName = recorded.sessionName;
997
1376
  const contextMessage = recorded.contextMessage;
998
1377
  transcript.push(userMessage);
999
1378
  rootContexts.set(runContext.rootRunId, runContext);
1000
- await runParentTurn(contextMessage, prompt, userMessage, runContext, commandContext);
1379
+ await runParentTurn(
1380
+ contextMessage,
1381
+ prompt,
1382
+ userMessage,
1383
+ runContext,
1384
+ commandContext,
1385
+ // What the user typed is the line they submitted, whatever it resolved to: a command's
1386
+ // body or a built-in command's prompt is not.
1387
+ submitted
1388
+ );
1001
1389
  const spentSoFar = runtime.runAccounting.snapshotRoot(runContext.rootRunId);
1002
1390
  carriedSpend = {
1003
1391
  usage: spentSoFar.inclusiveUsage,
1004
- costUsd: spentSoFar.inclusiveCostUsd
1392
+ costUsd: spentSoFar.inclusiveCostUsd,
1393
+ persistedUsage: runtime.runAccounting.persistedUsage(runContext.rootRunId),
1394
+ // Naming the root the totals came from: the next root's first record
1395
+ // carries this one's remainder, so this one is closed rather than left to
1396
+ // write the same tokens a second time at the end of the session (#336).
1397
+ fromRootRunId: runContext.rootRunId
1005
1398
  };
1006
1399
  if (planNotApplied.outcome) break;
1007
1400
  }
@@ -1013,7 +1406,11 @@ async function runHeadless(config, registry, opts) {
1013
1406
  )
1014
1407
  );
1015
1408
  while (true) {
1016
- await managedAgentManager.waitForIdle();
1409
+ const idle = !runSignal?.aborted && await raceAbort(managedAgentManager.waitForIdle(), runSignal);
1410
+ if (!idle) {
1411
+ if (mayReclassify) lastOutcome = classifyAbortReason(runSignal?.reason, true);
1412
+ break;
1413
+ }
1017
1414
  const pending = (await managedAgentManager.listPendingCompletions()).filter(
1018
1415
  (notification) => notification.parentSessionId === runtimeSessionId
1019
1416
  );
@@ -1052,10 +1449,13 @@ async function runHeadless(config, registry, opts) {
1052
1449
  displayMessage,
1053
1450
  contextMessage: userMessage.contextContent,
1054
1451
  userMessage,
1452
+ // The local name is deliberately not reassigned from the result: a
1453
+ // notification never names the session (#446).
1454
+ sessionName,
1055
1455
  timelineStore: store && sessionId ? store : void 0,
1056
1456
  expandShellInput: false,
1057
1457
  runtime,
1058
- signal: opts.signal
1458
+ signal: runSignal
1059
1459
  });
1060
1460
  transcript.push(userMessage);
1061
1461
  const parentContext = batch[0]?.rootRunId ? rootContexts.get(batch[0].rootRunId) : void 0;
@@ -1079,6 +1479,10 @@ async function runHeadless(config, registry, opts) {
1079
1479
  }));
1080
1480
  runResults.sort((left, right) => left.context.startedAt - right.context.startedAt);
1081
1481
  const reportedUsage = lastUsage ?? commandUsage;
1482
+ mayReclassify = lastOutcome?.status !== "failed";
1483
+ const lastText = finalRunOpeningId === void 0 ? "" : finalAnswerText(contextHistory, finalRunOpeningId);
1484
+ const inline = separateInlineReasoning(lastText);
1485
+ const answer = inline.found ? inline.content.trimEnd() : lastText;
1082
1486
  const result = {
1083
1487
  messages: contextHistory,
1084
1488
  transcript,
@@ -1089,12 +1493,12 @@ async function runHeadless(config, registry, opts) {
1089
1493
  runs: runResults,
1090
1494
  sessionId,
1091
1495
  plan: planNotApplied.outcome,
1092
- commandResults
1496
+ commandResults,
1497
+ answer
1093
1498
  };
1094
1499
  if (opts.jsonSchema) {
1095
- const text = lastAssistantText(contextHistory);
1096
1500
  try {
1097
- result.structured = JSON.parse(text);
1501
+ result.structured = JSON.parse(answer);
1098
1502
  } catch {
1099
1503
  result.structuredError = "Failed to parse JSON from assistant output";
1100
1504
  }
@@ -1105,7 +1509,7 @@ async function runHeadless(config, registry, opts) {
1105
1509
  config,
1106
1510
  registry,
1107
1511
  contextHistory,
1108
- opts.signal,
1512
+ runSignal,
1109
1513
  store?.readImageAttachment ? (attachment) => store.readImageAttachment(runtimeSessionId, attachment) : void 0
1110
1514
  );
1111
1515
  if (suggestions.length > 0) {
@@ -1122,8 +1526,7 @@ async function runHeadless(config, registry, opts) {
1122
1526
  ${stopped.message}
1123
1527
  `);
1124
1528
  } else {
1125
- const last = lastAssistantText(contextHistory);
1126
- if (last) stdout.write(last + "\n");
1529
+ if (answer) stdout.write(answer + "\n");
1127
1530
  }
1128
1531
  } else if (opts.outputFormat === "json") {
1129
1532
  emit({
@@ -1137,15 +1540,25 @@ ${stopped.message}
1137
1540
  structured: result.structured,
1138
1541
  structuredError: result.structuredError,
1139
1542
  plan: result.plan,
1140
- commandResults
1543
+ commandResults,
1544
+ answer
1141
1545
  }
1142
1546
  });
1143
1547
  } else if (opts.outputFormat === "stream-json") {
1144
1548
  emit({
1145
1549
  type: "result",
1550
+ // At the top level, beside `stopReason`, because that is where a
1551
+ // supervised loop reads the outcome from: `jq -e 'select(.type=="result")
1552
+ // | .outcome.reason=="objective_complete"'` was written against this
1553
+ // record and read a null. `result.outcome` stays for hosts already
1554
+ // reading it there.
1146
1555
  stopReason: outcome.reason,
1556
+ outcome,
1147
1557
  result: {
1148
- messages: contextHistory,
1558
+ // Off by default: a long run's conversation is half a megabyte of JSONL
1559
+ // on one line, which every reader has to buffer before it can look at
1560
+ // the field it came for. `--include-result-messages` asks for it back.
1561
+ ...opts.includeResultMessages ? { messages: contextHistory } : {},
1149
1562
  usage: reportedUsage,
1150
1563
  accounting: result.accounting,
1151
1564
  outcome,
@@ -1154,28 +1567,42 @@ ${stopped.message}
1154
1567
  structured: result.structured,
1155
1568
  structuredError: result.structuredError,
1156
1569
  plan: result.plan,
1157
- commandResults
1570
+ commandResults,
1571
+ answer
1158
1572
  }
1159
1573
  });
1160
1574
  }
1575
+ if (opts.outputFormat !== "stream-json" && opts.runSource !== "sdk" && outcome.status === "failed" && outcome.message && !answer && !planNotApplied.outcome && !eventState.errorReported) {
1576
+ process.stderr.write(`error: ${outcome.message}
1577
+ `);
1578
+ }
1161
1579
  if (sessionId) {
1162
- await agentSession.endLifecycle(config, sessionId, "completion", {
1163
- onHookEvent: createHookEventHandler(opts, emit)
1580
+ const sessionEndReason = outcome.status === "completed" ? "completion" : outcome.status === "failed" ? "error" : outcome.status === "cancelled" || runSignal?.aborted ? "aborted" : "completion";
1581
+ disposeSession();
1582
+ await agentSession.endLifecycle(config, sessionId, sessionEndReason, {
1583
+ onHookEvent: createHookEventHandler(opts, emit),
1584
+ endOutcome: outcome
1164
1585
  });
1165
1586
  }
1166
1587
  return result;
1588
+ } catch (error) {
1589
+ if (startedSessionId) {
1590
+ disposeSession();
1591
+ await agentSession.endLifecycle(config, startedSessionId, runSignal?.aborted ? "aborted" : "error", {
1592
+ onHookEvent: createHookEventHandler(opts, emit),
1593
+ endOutcome: runSignal?.aborted ? classifyAbortReason(runSignal.reason, false) : createTerminalOutcome("failed", "runtime_error", { partialOutput: false })
1594
+ }).catch((hookError) => {
1595
+ console.warn(
1596
+ `SessionEnd hook failed: ${hookError instanceof Error ? hookError.message : String(hookError)}`
1597
+ );
1598
+ });
1599
+ }
1600
+ throw error;
1167
1601
  } finally {
1168
1602
  disposeCrashHandlers?.();
1169
- agentSession.dispose("headless_complete");
1603
+ disposeSession();
1170
1604
  }
1171
1605
  }
1172
- function lastAssistantText(history) {
1173
- for (let i = history.length - 1; i >= 0; i--) {
1174
- const m = history[i];
1175
- if (m.role === "assistant" && m.content) return m.content;
1176
- }
1177
- return "";
1178
- }
1179
1606
  function planApprovalStatus(decision) {
1180
1607
  return typeof decision === "string" ? decision : decision.decision;
1181
1608
  }
@@ -1183,16 +1610,45 @@ function createHookEventHandler(opts, emit) {
1183
1610
  if (!opts.includeHookEvents || opts.outputFormat !== "stream-json") return void 0;
1184
1611
  return (event, payload) => emit({ type: "hook_event", event, ...payload });
1185
1612
  }
1186
- function emitAgentEvent(event, opts, emit) {
1613
+ function emitAgentEvent(event, opts, emit, state) {
1187
1614
  if (event.type === "terminal") return;
1188
1615
  if (event.type === "agent_text_delta" && opts.forwardSubagentText !== true) return;
1189
1616
  opts.onAgentEvent?.(event);
1617
+ if (event.type === "notice") {
1618
+ if (opts.outputFormat === "stream-json") emit({ type: "notice", message: event.message });
1619
+ else console.warn(event.message);
1620
+ return;
1621
+ }
1622
+ if (event.type === "goal_status") {
1623
+ const endsRunFailed = event.event.state === "failed" || event.event.state === "paused";
1624
+ if (opts.outputFormat === "stream-json") emit({ type: "goal_status", ...event.event });
1625
+ else if (opts.outputFormat === "text" && opts.runSource !== "sdk" && (opts.quiet !== true || endsRunFailed)) {
1626
+ process.stderr.write(
1627
+ `goal: ${progressLine(formatGoalStatus(event.event), PROGRESS_NOTICE_MAX)}
1628
+ `
1629
+ );
1630
+ }
1631
+ return;
1632
+ }
1633
+ if (event.type === "agent_notice") {
1634
+ if (opts.outputFormat === "stream-json") {
1635
+ emit({ type: "agent_notice", agentId: event.agentId, message: event.message });
1636
+ } else {
1637
+ process.stderr.write(`notice: ${progressLine(event.message, PROGRESS_NOTICE_MAX)}
1638
+ `);
1639
+ }
1640
+ return;
1641
+ }
1190
1642
  if (event.type === "error") {
1643
+ state.errorReported = true;
1191
1644
  if (opts.outputFormat === "stream-json") emit({ type: "error", error: event.error });
1192
1645
  else process.stderr.write(`error: ${event.error}
1193
1646
  `);
1194
1647
  return;
1195
1648
  }
1649
+ if (opts.outputFormat === "text" && opts.quiet !== true) {
1650
+ writeTextProgress(event, opts, state);
1651
+ }
1196
1652
  if (opts.outputFormat !== "stream-json") return;
1197
1653
  switch (event.type) {
1198
1654
  case "run_started":
@@ -1249,6 +1705,67 @@ function emitAgentEvent(event, opts, emit) {
1249
1705
  break;
1250
1706
  }
1251
1707
  }
1708
+ var PROGRESS_ARG_MAX = 120;
1709
+ var PROGRESS_ERROR_MAX = 160;
1710
+ var PROGRESS_NOTICE_MAX = 400;
1711
+ function progressLine(text, max) {
1712
+ const first = text.trim().split(/\r?\n/, 1)[0];
1713
+ const printable = Array.from(first, (char) => {
1714
+ const code = char.codePointAt(0) ?? 0;
1715
+ const control = code < 32 || code >= 127 && code <= 159 || code === 1564 || code === 8206 || code === 8207 || code >= 8232 && code <= 8238 || code >= 8294 && code <= 8297;
1716
+ return control ? " " : char;
1717
+ }).join("").trimEnd();
1718
+ const chars = Array.from(printable);
1719
+ return chars.length > max ? `${chars.slice(0, max - 1).join("")}\u2026` : printable;
1720
+ }
1721
+ function writeTextProgress(event, opts, state) {
1722
+ if (event.type === "tool_use") {
1723
+ process.stderr.write(`${toolCallProgress(event.toolCall)}
1724
+ `);
1725
+ return;
1726
+ }
1727
+ if (event.type === "tool_result" && opts.verbose === true) {
1728
+ process.stderr.write(` ${toolResultProgress(event.toolResult)}
1729
+ `);
1730
+ return;
1731
+ }
1732
+ if (event.type === "agent_start") {
1733
+ if (!state.childLabels.has(event.agent.id)) {
1734
+ state.childLabels.set(
1735
+ event.agent.id,
1736
+ uniqueAgentDisplayName(event.agent.profile ?? event.agent.name, state.childLabels.values())
1737
+ );
1738
+ }
1739
+ return;
1740
+ }
1741
+ if (event.type === "agent_activity") {
1742
+ writeChildToolProgress(event.agentId, event.activity, opts, state);
1743
+ }
1744
+ }
1745
+ function writeChildToolProgress(agentId, activity, opts, state) {
1746
+ if (activity.kind !== "tool" || !activity.toolCall) return;
1747
+ if (activity.status === "running") {
1748
+ const child = progressLine(state.childLabels.get(agentId) ?? "agent", PROGRESS_ARG_MAX);
1749
+ process.stderr.write(` [${child}] ${toolCallProgress(activity.toolCall)}
1750
+ `);
1751
+ return;
1752
+ }
1753
+ if (opts.verbose === true && activity.result) {
1754
+ process.stderr.write(` ${toolResultProgress(activity.result)}
1755
+ `);
1756
+ }
1757
+ }
1758
+ function toolCallProgress(call) {
1759
+ const arg = progressLine(getPrimaryArg(call.arguments), PROGRESS_ARG_MAX);
1760
+ return `[${progressLine(call.name, PROGRESS_ARG_MAX)}]${arg ? ` ${arg}` : ""}`;
1761
+ }
1762
+ function toolResultProgress(result) {
1763
+ const duration = result.metrics?.durationMs;
1764
+ const target = progressLine(result.presentation?.target ?? "", PROGRESS_ARG_MAX);
1765
+ const error = result.status === "success" ? "" : progressLine(toolResultErrorMessage(result) ?? "", PROGRESS_ERROR_MAX);
1766
+ const detail = [target, error].filter(Boolean).join(": ");
1767
+ return `\u2192 ${result.status}${duration !== void 0 ? ` ${Math.round(duration)}ms` : ""}${detail ? ` ${detail}` : ""}`;
1768
+ }
1252
1769
  function emitCompactBoundary(outputFormat, emit, result) {
1253
1770
  if (outputFormat !== "stream-json") return;
1254
1771
  emit({
@@ -1260,6 +1777,7 @@ function emitCompactBoundary(outputFormat, emit, result) {
1260
1777
  generation: result.generation,
1261
1778
  pre_messages: result.preMessageCount,
1262
1779
  post_messages: result.replacementHistory.length,
1780
+ carried_messages: result.carriedCount,
1263
1781
  post_tokens: result.postContextTokens,
1264
1782
  checkpoint_version: 2,
1265
1783
  strategy: result.strategy,
@@ -1269,12 +1787,17 @@ function emitCompactBoundary(outputFormat, emit, result) {
1269
1787
  // This generation's status above; the conversation's accumulated one here.
1270
1788
  coverage_lifetime_status: result.checkpoint.coverage?.lifetime?.status ?? "complete",
1271
1789
  coverage: result.checkpoint.coverage,
1272
- warning: result.warning
1790
+ warning: result.warning,
1791
+ // The reducer audit, when the host has something to report: suspect inputs
1792
+ // found in the summarized span and inherited constraints the reducer let go of.
1793
+ ...result.checkpoint.audit ? { audit: result.checkpoint.audit } : {},
1794
+ // Deferred compaction: the judge's verdict on the steps taken while the reducer ran.
1795
+ ...result.judge ? { judge: result.judge } : {}
1273
1796
  });
1274
1797
  }
1275
1798
  async function generatePromptSuggestions(config, _registry, history, signal, resolveAttachment) {
1276
- const { chatCompletionStream } = await import("./openai-compatible-RHZUBHV6.js");
1277
- const { buildMessages } = await import("./context-ZC5IFEAX.js");
1799
+ const { chatCompletionStream } = await import("./openai-compatible-IPUNBJ53.js");
1800
+ const { buildMessages } = await import("./context-C3RAP6K5.js");
1278
1801
  const suggestionMessages = await buildMessages(
1279
1802
  config,
1280
1803
  [
@@ -1398,7 +1921,7 @@ var StdioProcessTransport = class {
1398
1921
  return Promise.reject(new Error(`MCP server ${this.name} is not a stdio server`));
1399
1922
  }
1400
1923
  const child = spawn(this.config.command, this.config.args ?? [], {
1401
- env: { ...process.env, ...this.config.env },
1924
+ env: buildChildEnv(process.env, this.config.env),
1402
1925
  cwd: this.config.cwd,
1403
1926
  stdio: ["pipe", "pipe", "pipe"]
1404
1927
  });
@@ -1433,7 +1956,6 @@ var StdioProcessTransport = class {
1433
1956
  child.stdout?.removeListener("data", onStdoutData);
1434
1957
  child.stderr?.removeListener("data", onStderrData);
1435
1958
  child.removeListener("error", onChildError);
1436
- child.stdin?.removeListener("error", onStdinError);
1437
1959
  child.removeListener("exit", onExit);
1438
1960
  child.removeListener("close", onClose);
1439
1961
  this.cleanupListeners = () => {
@@ -1653,7 +2175,7 @@ function registerElicitationHandler(client, name, options) {
1653
2175
  }
1654
2176
  );
1655
2177
  }
1656
- async function connectMcpServer(name, cfg, options) {
2178
+ async function connectMcpServer(name, cfg, options, failure) {
1657
2179
  let handle;
1658
2180
  let liveConnection;
1659
2181
  let transportClosed = false;
@@ -1741,6 +2263,7 @@ async function connectMcpServer(name, cfg, options) {
1741
2263
  }
1742
2264
  return conn;
1743
2265
  } catch (error) {
2266
+ if (failure) failure.error = error;
1744
2267
  const message = redact(error instanceof Error ? error.message : String(error));
1745
2268
  options.onDiagnostic({
1746
2269
  level: "warn",
@@ -1758,6 +2281,23 @@ async function connectMcpServer(name, cfg, options) {
1758
2281
  return null;
1759
2282
  }
1760
2283
  }
2284
+ function rejectedStreamableHttp(error) {
2285
+ const message = error instanceof Error ? error.message : String(error);
2286
+ return /\b(404|405)\b|Not Found|Method Not Allowed/i.test(message);
2287
+ }
2288
+ async function connectRemoteWithSseFallback(name, cfg, options) {
2289
+ const eligible = isRemoteConfig(cfg) && cfg.type !== "sse";
2290
+ if (!eligible) return connectMcpServer(name, cfg, options);
2291
+ const failure = {};
2292
+ const connection = await connectMcpServer(name, cfg, options, failure);
2293
+ if (connection || !rejectedStreamableHttp(failure.error)) return connection;
2294
+ options.onDiagnostic({
2295
+ level: "warn",
2296
+ message: `MCP server "${name}" rejected Streamable HTTP; retrying as legacy SSE. Set "type": "sse" to skip this.`,
2297
+ server: name
2298
+ });
2299
+ return connectMcpServer(name, { ...cfg, type: "sse" }, options);
2300
+ }
1761
2301
  function buildToolDefinition(conn, tool, requestTimeoutMs) {
1762
2302
  const prefixedName = `mcp__${conn.name}__${tool.name}`;
1763
2303
  const live = conn;
@@ -1836,7 +2376,7 @@ async function connectMcpServers(workspace, options = {}) {
1836
2376
  servers = declared.filter((server) => server.source === "user");
1837
2377
  }
1838
2378
  const results = await Promise.allSettled(
1839
- servers.map(({ name, config }) => connectMcpServer(name, config, resolved))
2379
+ servers.map(({ name, config }) => connectRemoteWithSseFallback(name, config, resolved))
1840
2380
  );
1841
2381
  const connections = [];
1842
2382
  for (const [index, result] of results.entries()) {
@@ -1866,9 +2406,51 @@ async function disconnectMcpServers(connections) {
1866
2406
 
1867
2407
  // src/project-approval-notices.ts
1868
2408
  import { join as join2 } from "path";
2409
+ function collectWithheldProviderNotices(gate, workspace) {
2410
+ const notices = [];
2411
+ const approveCommand = (id) => {
2412
+ const { command, note } = providerTrustCommand(workspace, id);
2413
+ return `\`${command}\`${note}`;
2414
+ };
2415
+ for (const dropped of gate.droppedOverrides) {
2416
+ notices.push(`\u26A0 ${describeDroppedOverride(dropped)}`);
2417
+ }
2418
+ for (const entry of gate.partition.reserved) {
2419
+ notices.push(`\u26A0 ${describeReservedProvider(entry)}`);
2420
+ }
2421
+ const pendingIds = new Set(gate.partition.pending.map((entry) => entry.id));
2422
+ for (const entry of gate.partition.pending) {
2423
+ const id = renderUntrustedText(entry.id, 64);
2424
+ const origin = entry.declaration.baseURL ?? entry.declaration.baseUrl;
2425
+ notices.push(
2426
+ `\u26A0 Ignoring workspace-declared provider "${id}" (${describeOrigin(origin)}, key: ${describeApiKeyDeclaration(
2427
+ entry.declaration.apiKey,
2428
+ workspace
2429
+ )}) from ${entry.layers.map((layer) => renderUntrustedText(layer)).join(", ")}: it requires approval. Run ${approveCommand(entry.id)} to review and approve it.`
2430
+ );
2431
+ }
2432
+ for (const selection of gate.withheldSelections) {
2433
+ if (!pendingIds.has(selection.providerId)) continue;
2434
+ notices.push(
2435
+ `\u26A0 ${describeWithheldSelection(selection)} Run ${approveCommand(selection.providerId)} to approve it.`
2436
+ );
2437
+ }
2438
+ for (const selection of gate.unaffectedSelections) {
2439
+ if (!pendingIds.has(selection.providerId)) continue;
2440
+ notices.push(`\u26A0 ${describeUnaffectedSelection(selection)}`);
2441
+ }
2442
+ if (gate.legacyBaseUrl?.state === "pending") {
2443
+ notices.push(
2444
+ `\u26A0 Ignoring baseUrl "${renderUntrustedText(gate.legacyBaseUrl.baseUrl)}" from ${LEGACY_BASE_URL_ID}: it would receive BOOK_API_KEY, so it requires approval. Run ${approveCommand(LEGACY_BASE_URL_ID)} to review and approve it.`
2445
+ );
2446
+ }
2447
+ return notices;
2448
+ }
1869
2449
  function collectWithheldProjectNotices(input) {
1870
- if (!input.settingsEnabled) return [];
1871
- const projectSettings = loadSettingsFile(join2(input.workspace, ".book", "settings.json"));
2450
+ const providerNotices = input.providerGate ? collectWithheldProviderNotices(input.providerGate, input.workspace) : [];
2451
+ if (!input.settingsEnabled) return providerNotices;
2452
+ const projectPath = join2(input.workspace, ".book", "settings.json");
2453
+ const projectSettings = isUserSettingsFile(projectPath) ? null : loadSettingsFile(projectPath);
1872
2454
  const notices = [];
1873
2455
  const allow = partitionProjectAllowRules(
1874
2456
  projectSettings?.permissions?.allow ?? [],
@@ -1893,15 +2475,63 @@ function collectWithheldProjectNotices(input) {
1893
2475
  `\u26A0 Ignoring ${hooks.pending.length} project-declared hook(s) (${summary}): hooks require approval. Run \`book doctor\` to approve them.`
1894
2476
  );
1895
2477
  }
1896
- return notices;
2478
+ const directories = partitionProjectDirectories(
2479
+ collectDeclaredDirectories(input.workspace, projectSettings?.additionalDirectories ?? []),
2480
+ input.settings.projectDirectories
2481
+ );
2482
+ for (const directory of directories.pending) {
2483
+ notices.push(
2484
+ `\u26A0 Ignoring project-declared additionalDirectories entry "${directory.declared}" (really ${directory.realPath}): it requires approval. Run \`book doctor\` to see how to grant it.`
2485
+ );
2486
+ }
2487
+ const ignoredSandboxKeys = ignoredWorkspaceSandboxKeys(projectSettings ?? {});
2488
+ if (ignoredSandboxKeys.length > 0) {
2489
+ const listed = ignoredSandboxKeys.map(formatIgnoredWorkspaceSandboxKey).join(", ");
2490
+ notices.push(
2491
+ `\u26A0 Ignoring sandbox settings declared by ${projectPath}: ${listed}. A file inside the workspace may turn the sandbox on and add deny entries, but not loosen it. ` + TRUSTED_SETTINGS_LOCATION
2492
+ );
2493
+ }
2494
+ const localPath = join2(input.workspace, ".book", "settings.local.json");
2495
+ const localSettings = loadSettingsFile(localPath);
2496
+ const ignoredLocalSandboxKeys = ignoredWorkspaceSandboxKeys(localSettings ?? {});
2497
+ if (ignoredLocalSandboxKeys.length > 0) {
2498
+ const listed = ignoredLocalSandboxKeys.map(formatIgnoredWorkspaceSandboxKey).join(", ");
2499
+ notices.push(
2500
+ `\u26A0 Ignoring sandbox settings declared by ${localPath}: ${listed}. A file inside the workspace may turn the sandbox on and add deny entries, but not loosen it. ` + TRUSTED_SETTINGS_LOCATION
2501
+ );
2502
+ }
2503
+ const userPath = settingsLayerPaths(input.workspace)[0];
2504
+ const activation = workspaceLayerThatEnabledSandbox([
2505
+ { path: userPath, trust: "trusted", document: loadSettingsFile(userPath) },
2506
+ { path: projectPath, trust: "repository", document: projectSettings },
2507
+ { path: localPath, trust: "local", document: localSettings }
2508
+ ]);
2509
+ if (activation) {
2510
+ notices.push(
2511
+ `\u26A0 ${activation} declared sandbox.enabled=true, so sandbox.autoAllowBashIfSandboxed is false for this session: no Bash command is auto-approved. ${TRUSTED_SETTINGS_LOCATION}`
2512
+ );
2513
+ }
2514
+ return [...notices, ...providerNotices];
2515
+ }
2516
+ function collectSkippedProjectFileNotices(workspace) {
2517
+ return [
2518
+ ...collectSkippedInstructionNotices(workspace),
2519
+ ...collectSkippedReviewNotices(workspace),
2520
+ ...collectSkippedCommandNotices(workspace)
2521
+ ];
1897
2522
  }
1898
2523
 
1899
2524
  export {
2525
+ runStatusPath,
1900
2526
  readRunStatus,
2527
+ cleanupRunStatus,
2528
+ isReaderGoneError,
1901
2529
  runHeadless,
1902
2530
  buildMcpToolDefinitions,
1903
2531
  connectMcpServers,
1904
2532
  disconnectMcpServers,
1905
- collectWithheldProjectNotices
2533
+ collectWithheldProviderNotices,
2534
+ collectWithheldProjectNotices,
2535
+ collectSkippedProjectFileNotices
1906
2536
  };
1907
- //# sourceMappingURL=chunk-J7MIZ5CA.js.map
2537
+ //# sourceMappingURL=chunk-E5WLQSKA.js.map