@namzu/sdk 59.0.0 → 60.1.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 (370) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  6. package/dist/bridge/a2a/mapper.js +16 -0
  7. package/dist/bridge/a2a/mapper.js.map +1 -1
  8. package/dist/bridge/acp/update.d.ts.map +1 -1
  9. package/dist/bridge/acp/update.js +1 -0
  10. package/dist/bridge/acp/update.js.map +1 -1
  11. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  12. package/dist/bridge/sse/mapper.js +12 -0
  13. package/dist/bridge/sse/mapper.js.map +1 -1
  14. package/dist/compaction/context-window.d.ts +53 -0
  15. package/dist/compaction/context-window.d.ts.map +1 -1
  16. package/dist/compaction/context-window.js +141 -2
  17. package/dist/compaction/context-window.js.map +1 -1
  18. package/dist/constants/continuation.d.ts +9 -0
  19. package/dist/constants/continuation.d.ts.map +1 -1
  20. package/dist/constants/continuation.js +9 -0
  21. package/dist/constants/continuation.js.map +1 -1
  22. package/dist/contracts/session/api.d.ts +7 -1
  23. package/dist/contracts/session/api.d.ts.map +1 -1
  24. package/dist/execution/workflow/index.d.ts +3 -1
  25. package/dist/execution/workflow/index.d.ts.map +1 -1
  26. package/dist/execution/workflow/index.js +2 -1
  27. package/dist/execution/workflow/index.js.map +1 -1
  28. package/dist/execution/workflow/journal.d.ts +24 -4
  29. package/dist/execution/workflow/journal.d.ts.map +1 -1
  30. package/dist/execution/workflow/journal.js +131 -9
  31. package/dist/execution/workflow/journal.js.map +1 -1
  32. package/dist/execution/workflow/prelude.d.ts +2 -0
  33. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  34. package/dist/execution/workflow/prelude.js +49 -8
  35. package/dist/execution/workflow/prelude.js.map +1 -1
  36. package/dist/execution/workflow/resume.d.ts +59 -0
  37. package/dist/execution/workflow/resume.d.ts.map +1 -0
  38. package/dist/execution/workflow/resume.js +273 -0
  39. package/dist/execution/workflow/resume.js.map +1 -0
  40. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  41. package/dist/execution/workflow/runtime.js +112 -8
  42. package/dist/execution/workflow/runtime.js.map +1 -1
  43. package/dist/execution/workflow/types.d.ts +105 -1
  44. package/dist/execution/workflow/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  50. package/dist/manager/session/turn-recorder.js +1 -0
  51. package/dist/manager/session/turn-recorder.js.map +1 -1
  52. package/dist/plugin/compat.d.ts +24 -0
  53. package/dist/plugin/compat.d.ts.map +1 -0
  54. package/dist/plugin/compat.js +149 -0
  55. package/dist/plugin/compat.js.map +1 -0
  56. package/dist/plugin/loader.d.ts +2 -1
  57. package/dist/plugin/loader.d.ts.map +1 -1
  58. package/dist/plugin/loader.js +6 -0
  59. package/dist/plugin/loader.js.map +1 -1
  60. package/dist/pricing/index.d.ts.map +1 -1
  61. package/dist/pricing/index.js +2 -1
  62. package/dist/pricing/index.js.map +1 -1
  63. package/dist/process/kill-tree.d.ts +44 -0
  64. package/dist/process/kill-tree.d.ts.map +1 -1
  65. package/dist/process/kill-tree.js +102 -4
  66. package/dist/process/kill-tree.js.map +1 -1
  67. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  69. package/dist/prompt/coding-agent-doctrine.js +9 -1
  70. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  71. package/dist/provider/collect-chat-completion.d.ts.map +1 -1
  72. package/dist/provider/collect-chat-completion.js +4 -0
  73. package/dist/provider/collect-chat-completion.js.map +1 -1
  74. package/dist/provider/fallback.d.ts.map +1 -1
  75. package/dist/provider/fallback.js +11 -0
  76. package/dist/provider/fallback.js.map +1 -1
  77. package/dist/provider/idle-timeout.d.ts.map +1 -1
  78. package/dist/provider/idle-timeout.js +5 -0
  79. package/dist/provider/idle-timeout.js.map +1 -1
  80. package/dist/provider/mock.d.ts +2 -0
  81. package/dist/provider/mock.d.ts.map +1 -1
  82. package/dist/provider/mock.js +16 -8
  83. package/dist/provider/mock.js.map +1 -1
  84. package/dist/provider/model-window-suffix.d.ts +18 -0
  85. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  86. package/dist/provider/model-window-suffix.js +78 -0
  87. package/dist/provider/model-window-suffix.js.map +1 -0
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +5 -3
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +5 -4
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/public-types.d.ts +1 -0
  96. package/dist/public-types.d.ts.map +1 -1
  97. package/dist/registry/command/kernel-commands.d.ts +1 -4
  98. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  99. package/dist/registry/command/kernel-commands.js +0 -28
  100. package/dist/registry/command/kernel-commands.js.map +1 -1
  101. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  102. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  103. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  104. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  105. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  106. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  107. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  108. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  109. package/dist/runtime/jobs/liveness.d.ts +67 -0
  110. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  111. package/dist/runtime/jobs/liveness.js +94 -0
  112. package/dist/runtime/jobs/liveness.js.map +1 -0
  113. package/dist/runtime/jobs/registry.d.ts +21 -0
  114. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  115. package/dist/runtime/jobs/registry.js +104 -2
  116. package/dist/runtime/jobs/registry.js.map +1 -1
  117. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  118. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  119. package/dist/runtime/jobs/wake-budget.js +48 -0
  120. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  121. package/dist/runtime/query/detached-calls.d.ts +65 -0
  122. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  123. package/dist/runtime/query/detached-calls.js +116 -0
  124. package/dist/runtime/query/detached-calls.js.map +1 -0
  125. package/dist/runtime/query/executor.d.ts +4 -1
  126. package/dist/runtime/query/executor.d.ts.map +1 -1
  127. package/dist/runtime/query/executor.js +90 -6
  128. package/dist/runtime/query/executor.js.map +1 -1
  129. package/dist/runtime/query/index.d.ts +34 -0
  130. package/dist/runtime/query/index.d.ts.map +1 -1
  131. package/dist/runtime/query/index.js +32 -5
  132. package/dist/runtime/query/index.js.map +1 -1
  133. package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
  134. package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
  135. package/dist/runtime/query/iteration/effort-clamp.js +42 -0
  136. package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
  137. package/dist/runtime/query/iteration/index.d.ts +34 -0
  138. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  139. package/dist/runtime/query/iteration/index.js +199 -6
  140. package/dist/runtime/query/iteration/index.js.map +1 -1
  141. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  142. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  143. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  144. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  145. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  147. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  148. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  149. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  150. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  151. package/dist/runtime/query/iteration/stream-turn.js +6 -0
  152. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  153. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  154. package/dist/runtime/query/session-storage.js +8 -3
  155. package/dist/runtime/query/session-storage.js.map +1 -1
  156. package/dist/runtime/query/steering.d.ts +29 -3
  157. package/dist/runtime/query/steering.d.ts.map +1 -1
  158. package/dist/runtime/query/steering.js +58 -3
  159. package/dist/runtime/query/steering.js.map +1 -1
  160. package/dist/runtime/query/tooling.d.ts +1 -0
  161. package/dist/runtime/query/tooling.d.ts.map +1 -1
  162. package/dist/runtime/query/tooling.js +1 -0
  163. package/dist/runtime/query/tooling.js.map +1 -1
  164. package/dist/runtime/system-events.d.ts +5 -0
  165. package/dist/runtime/system-events.d.ts.map +1 -1
  166. package/dist/runtime/system-events.js +7 -1
  167. package/dist/runtime/system-events.js.map +1 -1
  168. package/dist/sandbox/denial.d.ts +11 -0
  169. package/dist/sandbox/denial.d.ts.map +1 -0
  170. package/dist/sandbox/denial.js +61 -0
  171. package/dist/sandbox/denial.js.map +1 -0
  172. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  173. package/dist/sandbox/file-walk-program.js +31 -13
  174. package/dist/sandbox/file-walk-program.js.map +1 -1
  175. package/dist/sandbox/file-walk.d.ts +2 -0
  176. package/dist/sandbox/file-walk.d.ts.map +1 -1
  177. package/dist/sandbox/file-walk.js +64 -13
  178. package/dist/sandbox/file-walk.js.map +1 -1
  179. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  180. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  181. package/dist/sandbox/ignore-matcher.js +11 -0
  182. package/dist/sandbox/ignore-matcher.js.map +1 -0
  183. package/dist/sandbox/ignore-source.d.ts +26 -0
  184. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  185. package/dist/sandbox/ignore-source.js +221 -0
  186. package/dist/sandbox/ignore-source.js.map +1 -0
  187. package/dist/sandbox/provider/local.d.ts.map +1 -1
  188. package/dist/sandbox/provider/local.js +11 -1
  189. package/dist/sandbox/provider/local.js.map +1 -1
  190. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  191. package/dist/scheduler/completion-inbox.js +1 -0
  192. package/dist/scheduler/completion-inbox.js.map +1 -1
  193. package/dist/session/log-hash.d.ts.map +1 -1
  194. package/dist/session/log-hash.js +18 -1
  195. package/dist/session/log-hash.js.map +1 -1
  196. package/dist/store/memory/disk.d.ts.map +1 -1
  197. package/dist/store/memory/disk.js +1 -1
  198. package/dist/store/memory/disk.js.map +1 -1
  199. package/dist/store/memory/markdown.d.ts.map +1 -1
  200. package/dist/store/memory/markdown.js +1 -1
  201. package/dist/store/memory/markdown.js.map +1 -1
  202. package/dist/store/memory/operation-lock.d.ts +20 -4
  203. package/dist/store/memory/operation-lock.d.ts.map +1 -1
  204. package/dist/store/memory/operation-lock.js +235 -58
  205. package/dist/store/memory/operation-lock.js.map +1 -1
  206. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  207. package/dist/store/session-index/sqlite.js +169 -16
  208. package/dist/store/session-index/sqlite.js.map +1 -1
  209. package/dist/tools/builtins/bash.d.ts.map +1 -1
  210. package/dist/tools/builtins/bash.js +10 -4
  211. package/dist/tools/builtins/bash.js.map +1 -1
  212. package/dist/tools/builtins/computer-use.d.ts +35 -0
  213. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  214. package/dist/tools/builtins/computer-use.js +512 -67
  215. package/dist/tools/builtins/computer-use.js.map +1 -1
  216. package/dist/tools/builtins/glob.d.ts +1 -0
  217. package/dist/tools/builtins/glob.d.ts.map +1 -1
  218. package/dist/tools/builtins/glob.js +65 -28
  219. package/dist/tools/builtins/glob.js.map +1 -1
  220. package/dist/tools/builtins/grep.d.ts +2 -0
  221. package/dist/tools/builtins/grep.d.ts.map +1 -1
  222. package/dist/tools/builtins/grep.js +94 -40
  223. package/dist/tools/builtins/grep.js.map +1 -1
  224. package/dist/tools/builtins/job.d.ts +1 -1
  225. package/dist/tools/builtins/job.d.ts.map +1 -1
  226. package/dist/tools/builtins/job.js +34 -7
  227. package/dist/tools/builtins/job.js.map +1 -1
  228. package/dist/tools/builtins/search-support.d.ts +44 -0
  229. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  230. package/dist/tools/builtins/search-support.js +89 -0
  231. package/dist/tools/builtins/search-support.js.map +1 -0
  232. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  233. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  234. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  235. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  236. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  237. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  238. package/dist/tools/builtins/wait-for-job.js +13 -2
  239. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  240. package/dist/tools/command-shell.d.ts.map +1 -1
  241. package/dist/tools/command-shell.js +14 -5
  242. package/dist/tools/command-shell.js.map +1 -1
  243. package/dist/turn/reporter.d.ts.map +1 -1
  244. package/dist/turn/reporter.js +11 -0
  245. package/dist/turn/reporter.js.map +1 -1
  246. package/dist/types/agent/query.d.ts +2 -0
  247. package/dist/types/agent/query.d.ts.map +1 -1
  248. package/dist/types/computer-use/index.d.ts +129 -13
  249. package/dist/types/computer-use/index.d.ts.map +1 -1
  250. package/dist/types/computer-use/index.js.map +1 -1
  251. package/dist/types/message/index.d.ts +1 -1
  252. package/dist/types/message/index.d.ts.map +1 -1
  253. package/dist/types/message/index.js +1 -0
  254. package/dist/types/message/index.js.map +1 -1
  255. package/dist/types/plugin/index.d.ts +24 -0
  256. package/dist/types/plugin/index.d.ts.map +1 -1
  257. package/dist/types/plugin/index.js +4 -0
  258. package/dist/types/plugin/index.js.map +1 -1
  259. package/dist/types/provider/chat.d.ts +19 -0
  260. package/dist/types/provider/chat.d.ts.map +1 -1
  261. package/dist/types/provider/config.d.ts +7 -0
  262. package/dist/types/provider/config.d.ts.map +1 -1
  263. package/dist/types/provider/index.d.ts +1 -1
  264. package/dist/types/provider/index.d.ts.map +1 -1
  265. package/dist/types/provider/interface.d.ts +15 -0
  266. package/dist/types/provider/interface.d.ts.map +1 -1
  267. package/dist/types/provider/stream.d.ts +6 -0
  268. package/dist/types/provider/stream.d.ts.map +1 -1
  269. package/dist/types/sandbox/index.d.ts +38 -1
  270. package/dist/types/sandbox/index.d.ts.map +1 -1
  271. package/dist/types/sandbox/index.js.map +1 -1
  272. package/dist/types/session/events.d.ts +34 -0
  273. package/dist/types/session/events.d.ts.map +1 -1
  274. package/dist/types/session/events.js.map +1 -1
  275. package/dist/types/session/records.d.ts +298 -34
  276. package/dist/types/session/records.d.ts.map +1 -1
  277. package/dist/types/session/records.js +35 -1
  278. package/dist/types/session/records.js.map +1 -1
  279. package/dist/types/session/stop-reason.d.ts +11 -0
  280. package/dist/types/session/stop-reason.d.ts.map +1 -1
  281. package/dist/types/tool/index.d.ts +30 -0
  282. package/dist/types/tool/index.d.ts.map +1 -1
  283. package/dist/types/tool/index.js.map +1 -1
  284. package/dist/types/tool/presentation.d.ts +8 -0
  285. package/dist/types/tool/presentation.d.ts.map +1 -1
  286. package/package.json +1 -1
  287. package/src/agents/QueryAgent.ts +1 -0
  288. package/src/bridge/a2a/mapper.ts +19 -0
  289. package/src/bridge/acp/update.ts +6 -1
  290. package/src/bridge/sse/mapper.ts +13 -0
  291. package/src/compaction/context-window.ts +163 -2
  292. package/src/constants/continuation.ts +13 -0
  293. package/src/contracts/session/api.ts +6 -0
  294. package/src/execution/workflow/index.ts +14 -1
  295. package/src/execution/workflow/journal.ts +161 -11
  296. package/src/execution/workflow/prelude.ts +51 -8
  297. package/src/execution/workflow/resume.ts +313 -0
  298. package/src/execution/workflow/runtime.ts +127 -9
  299. package/src/execution/workflow/types.ts +109 -1
  300. package/src/index.ts +17 -0
  301. package/src/manager/session/turn-recorder.ts +1 -0
  302. package/src/plugin/compat.ts +173 -0
  303. package/src/plugin/loader.ts +7 -1
  304. package/src/pricing/index.ts +2 -1
  305. package/src/process/kill-tree.ts +106 -4
  306. package/src/prompt/coding-agent-doctrine.ts +9 -1
  307. package/src/provider/collect-chat-completion.ts +3 -0
  308. package/src/provider/fallback.ts +17 -0
  309. package/src/provider/idle-timeout.ts +9 -0
  310. package/src/provider/mock.ts +19 -9
  311. package/src/provider/model-window-suffix.ts +85 -0
  312. package/src/provider/retry.ts +9 -0
  313. package/src/public-runtime.ts +21 -2
  314. package/src/public-types.ts +1 -0
  315. package/src/registry/command/kernel-commands.ts +1 -32
  316. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  317. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  318. package/src/runtime/jobs/liveness.ts +144 -0
  319. package/src/runtime/jobs/registry.ts +114 -2
  320. package/src/runtime/jobs/wake-budget.ts +55 -0
  321. package/src/runtime/query/detached-calls.ts +135 -0
  322. package/src/runtime/query/executor.ts +92 -10
  323. package/src/runtime/query/index.ts +65 -7
  324. package/src/runtime/query/iteration/effort-clamp.ts +46 -0
  325. package/src/runtime/query/iteration/index.ts +260 -6
  326. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  327. package/src/runtime/query/iteration/phases/context.ts +9 -0
  328. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  329. package/src/runtime/query/iteration/stream-turn.ts +5 -0
  330. package/src/runtime/query/session-storage.ts +8 -3
  331. package/src/runtime/query/steering.ts +77 -6
  332. package/src/runtime/query/tooling.ts +2 -0
  333. package/src/runtime/system-events.ts +12 -1
  334. package/src/sandbox/denial.ts +79 -0
  335. package/src/sandbox/file-walk-program.ts +32 -13
  336. package/src/sandbox/file-walk.ts +75 -12
  337. package/src/sandbox/ignore-matcher.ts +46 -0
  338. package/src/sandbox/ignore-source.ts +220 -0
  339. package/src/sandbox/provider/local.ts +11 -1
  340. package/src/scheduler/completion-inbox.ts +1 -0
  341. package/src/session/log-hash.ts +21 -0
  342. package/src/store/memory/disk.ts +1 -0
  343. package/src/store/memory/markdown.ts +1 -0
  344. package/src/store/memory/operation-lock.ts +288 -57
  345. package/src/store/session-index/sqlite.ts +171 -16
  346. package/src/tools/builtins/bash.ts +11 -4
  347. package/src/tools/builtins/computer-use.ts +593 -68
  348. package/src/tools/builtins/glob.ts +79 -32
  349. package/src/tools/builtins/grep.ts +107 -43
  350. package/src/tools/builtins/job.ts +42 -12
  351. package/src/tools/builtins/search-support.ts +107 -0
  352. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  353. package/src/tools/builtins/wait-for-job.ts +14 -2
  354. package/src/tools/command-shell.ts +13 -5
  355. package/src/turn/reporter.ts +12 -0
  356. package/src/types/agent/query.ts +2 -0
  357. package/src/types/computer-use/index.ts +157 -15
  358. package/src/types/message/index.ts +5 -3
  359. package/src/types/plugin/index.ts +8 -0
  360. package/src/types/provider/chat.ts +20 -0
  361. package/src/types/provider/config.ts +7 -0
  362. package/src/types/provider/index.ts +1 -0
  363. package/src/types/provider/interface.ts +20 -0
  364. package/src/types/provider/stream.ts +6 -0
  365. package/src/types/sandbox/index.ts +40 -1
  366. package/src/types/session/events.ts +65 -5
  367. package/src/types/session/records.ts +39 -2
  368. package/src/types/session/stop-reason.ts +11 -0
  369. package/src/types/tool/index.ts +30 -0
  370. package/src/types/tool/presentation.ts +8 -0
@@ -64,17 +64,31 @@ export interface SteeringChannel {
64
64
  }
65
65
 
66
66
  export class SteeringBinding implements SteeringChannel {
67
- private queued: string[] = []
67
+ private queued: { readonly text: string; readonly key?: string }[] = []
68
68
 
69
- steer(text: string): void {
69
+ /**
70
+ * `key` names what the text is about (a job id), so the text can be
71
+ * taken back with {@link withdraw} while it is still undelivered.
72
+ */
73
+ steer(text: string, key?: string): void {
70
74
  const trimmed = text.trim()
71
75
  if (!trimmed) return
72
- this.queued.push(trimmed)
76
+ this.queued.push(key === undefined ? { text: trimmed } : { text: trimmed, key })
77
+ }
78
+
79
+ /**
80
+ * Take back every undelivered text queued under `key`. Returns whether any
81
+ * was queued. Text already drained has been delivered and is not touched.
82
+ */
83
+ withdraw(key: string): boolean {
84
+ const before = this.queued.length
85
+ this.queued = this.queued.filter((entry) => entry.key !== key)
86
+ return this.queued.length !== before
73
87
  }
74
88
 
75
89
  drain(): string | undefined {
76
90
  if (this.queued.length === 0) return undefined
77
- const joined = this.queued.join('\n')
91
+ const joined = this.queued.map((entry) => entry.text).join('\n')
78
92
  this.queued = []
79
93
  return joined
80
94
  }
@@ -84,6 +98,60 @@ export class SteeringBinding implements SteeringChannel {
84
98
  }
85
99
  }
86
100
 
101
+ /**
102
+ * The most output a job-exit notice carries inline. A job whose whole output
103
+ * fits is told with its result; a larger one is read with the job tool, so a
104
+ * noisy build cannot turn every exit into a large message.
105
+ */
106
+ export const JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES = 2048
107
+
108
+ /**
109
+ * One job's exit as the text the model reads. The output is inlined only when
110
+ * the whole of it fits {@link JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES} and none was
111
+ * dropped by the retention cap; it is untrusted process output, so it rides in
112
+ * the system-event envelope rather than in the notice's own words.
113
+ */
114
+ export function formatJobExitNotice(
115
+ job: Pick<BackgroundJob, 'id' | 'command' | 'status' | 'exitCode' | 'signal'> &
116
+ Partial<Pick<BackgroundJob, 'exitedAt'>>,
117
+ output?: {
118
+ readonly chunk: string
119
+ readonly nextOffset: number
120
+ readonly droppedBytes: number
121
+ },
122
+ ): string {
123
+ const outcome =
124
+ job.status === 'killed'
125
+ ? 'was stopped'
126
+ : `exited with code ${job.exitCode ?? 'unknown'}${job.signal ? ` (${job.signal})` : ''}`
127
+ const endedAt = job.exitedAt === undefined ? '' : ` at ${new Date(job.exitedAt).toISOString()}`
128
+ const head = `Background job ${job.id} (${job.command}) ${outcome}${endedAt}.`
129
+ if (
130
+ output === undefined ||
131
+ output.chunk.length === 0 ||
132
+ output.droppedBytes > 0 ||
133
+ output.nextOffset > JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES
134
+ ) {
135
+ return `${head} Read its output with the job tool if you need it.`
136
+ }
137
+ return `${head}\n${formatSystemEvent({
138
+ kind: 'job',
139
+ id: job.id,
140
+ status: job.status === 'killed' ? 'cancelled' : job.exitCode === 0 ? 'completed' : 'failed',
141
+ summary: `Output of ${job.id}`,
142
+ source: `job ${job.id}: ${oneMetadataLine(job.command)}`,
143
+ body: {
144
+ envelope: {
145
+ kind: 'job-output',
146
+ attributes: { job: job.id },
147
+ provenance:
148
+ 'Everything the job printed. The command, or whatever it reads, wrote it; it is not the operator.',
149
+ },
150
+ content: output.chunk,
151
+ },
152
+ })}`
153
+ }
154
+
87
155
  /** What the model reads when a background job it started has ended. */
88
156
  export function formatJobNote(text: string): string {
89
157
  return frameNotice('job', '[Background job update]', text)
@@ -116,7 +184,7 @@ const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/s
116
184
  */
117
185
  export function formatMonitorEvent(
118
186
  event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'> &
119
- Partial<Pick<MonitorEvent, 'transport'>>,
187
+ Partial<Pick<MonitorEvent, 'transport' | 'at'>>,
120
188
  command: string,
121
189
  ): string {
122
190
  return formatSystemEvent({
@@ -126,6 +194,7 @@ export function formatMonitorEvent(
126
194
  summary: `Monitor event: "${oneMetadataLine(event.description)}"`,
127
195
  source: `monitor ${event.jobId}: ${oneMetadataLine(command)}`,
128
196
  more: `job {"action":"kill","id":"${event.jobId}"} stops it`,
197
+ ...(event.at !== undefined ? { at: event.at } : {}),
129
198
  body: {
130
199
  envelope: {
131
200
  kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
@@ -170,7 +239,8 @@ export function formatMonitorEnd(
170
239
  | 'transport'
171
240
  | 'closeCode'
172
241
  | 'closeReason'
173
- >,
242
+ > &
243
+ Partial<Pick<BackgroundJob, 'exitedAt'>>,
174
244
  stats: { readonly stdoutChars: number },
175
245
  ): string {
176
246
  const end = describeMonitorEnd(job, stats)
@@ -181,6 +251,7 @@ export function formatMonitorEnd(
181
251
  summary: end.summary,
182
252
  source: `monitor ${job.id}: ${oneMetadataLine(job.command)}`,
183
253
  more: `job {"action":"read","id":"${job.id}"} reads its output`,
254
+ ...(job.exitedAt !== undefined ? { at: job.exitedAt } : {}),
184
255
  })
185
256
  }
186
257
 
@@ -46,6 +46,7 @@ export interface ToolingBootstrapConfig {
46
46
  backgroundJobOwner?: string
47
47
  /** Where `wait_for_job` records wait-intent; see the executor's own field. */
48
48
  onJobAwaited?: (id: string) => void
49
+ onJobCollected?: (id: string) => void
49
50
  /** See `ToolExecutorConfig.monitorTimeoutCapMs`. */
50
51
  monitorTimeoutCapMs?: number
51
52
  /** See `ToolExecutorConfig.onMonitorStarted`. */
@@ -110,6 +111,7 @@ export class ToolingBootstrap {
110
111
  ...(config.backgroundJobs ? { backgroundJobs: config.backgroundJobs } : {}),
111
112
  ...(config.backgroundJobOwner ? { backgroundJobOwner: config.backgroundJobOwner } : {}),
112
113
  ...(config.onJobAwaited ? { onJobAwaited: config.onJobAwaited } : {}),
114
+ ...(config.onJobCollected ? { onJobCollected: config.onJobCollected } : {}),
113
115
  ...(config.monitorTimeoutCapMs !== undefined
114
116
  ? { monitorTimeoutCapMs: config.monitorTimeoutCapMs }
115
117
  : {}),
@@ -102,6 +102,11 @@ export interface SystemEvent {
102
102
  readonly more?: string
103
103
  /** Omit for an event with nothing to show; renders as `(no output)`. */
104
104
  readonly body?: SystemEventBody
105
+ /**
106
+ * When it happened (epoch ms). Rendered as an `at` attribute in ISO form so
107
+ * the model can tell a fresh event from one that waited in a queue.
108
+ */
109
+ readonly at?: number
105
110
  }
106
111
 
107
112
  export interface FormatSystemEventOptions {
@@ -209,6 +214,12 @@ function renderBody(body: SystemEventBody | undefined, options: FormatSystemEven
209
214
  )
210
215
  }
211
216
 
217
+ function atAttribute(at: number | undefined): string {
218
+ if (at === undefined || !Number.isFinite(at)) return ''
219
+ const date = new Date(at)
220
+ return Number.isNaN(date.getTime()) ? '' : ` at="${date.toISOString()}"`
221
+ }
222
+
212
223
  /**
213
224
  * Render one asynchronous event as provider-required user-role context.
214
225
  *
@@ -248,7 +259,7 @@ export function formatSystemEvent(
248
259
  ])
249
260
  const closingTag = `</system-event-${nonce}>`
250
261
  const lines = [
251
- `<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}">`,
262
+ `<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}"${atAttribute(event.at)}>`,
252
263
  SYSTEM_EVENT_HEADER,
253
264
  nonceClosureStatement(closingTag),
254
265
  `summary: ${summary}`,
@@ -0,0 +1,79 @@
1
+ import type { SandboxEnvironment, SandboxExecAttribution } from '../types/sandbox/index.js'
2
+
3
+ /**
4
+ * Reading a sandboxed command's failure: did the command fail, did the
5
+ * sandbox refuse it, or did the launcher never get as far as running it?
6
+ *
7
+ * The model cannot tell these apart from an exit code. A denied write looks
8
+ * like any other failure, so it retries the same command and burns turns
9
+ * on something no retry can fix; a launcher that could not set up its
10
+ * namespaces looks like the command crashing. Both call for a different
11
+ * answer (ask for the escape, or report a runner fault).
12
+ *
13
+ * This is inference from stderr text, not a verdict from the kernel, and
14
+ * the wording says so. It only ever adds a note; the result is unchanged.
15
+ */
16
+
17
+ /** Launcher setup failures print this prefix before any user command runs. */
18
+ const RUNNER_PREFIX: Partial<Record<SandboxEnvironment, RegExp>> = {
19
+ 'linux-bwrap': /^bwrap: (?!execvp )/m,
20
+ 'linux-namespace': /^unshare: /m,
21
+ 'macos-seatbelt': /^sandbox-exec: /m,
22
+ }
23
+
24
+ const EROFS = /Read-only file system|\bEROFS\b/i
25
+ const PERMISSION = /Permission denied|Operation not permitted|\bEACCES\b|\bEPERM\b/i
26
+ const ABSOLUTE_PATH = /(?:^|[\s'"`:(])(\/[^\s'"`:)]+)/g
27
+
28
+ /** Only tiers that confine file writes can produce a sandbox denial. */
29
+ const CONFINES_FILES: ReadonlySet<SandboxEnvironment> = new Set(['linux-bwrap', 'macos-seatbelt'])
30
+
31
+ function within(path: string, root: string): boolean {
32
+ return path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`)
33
+ }
34
+
35
+ function pathsIn(line: string): string[] {
36
+ return [...line.matchAll(ABSOLUTE_PATH)].map((m) => m[1] as string)
37
+ }
38
+
39
+ export interface AttributionInput {
40
+ readonly environment: SandboxEnvironment
41
+ readonly exitCode: number
42
+ readonly stderr: string
43
+ readonly timedOut: boolean
44
+ /** Paths the sandbox lets the command write: root, added directories, scratch. */
45
+ readonly allowedRoots: readonly string[]
46
+ }
47
+
48
+ export function attributeSandboxFailure(
49
+ input: AttributionInput,
50
+ ): SandboxExecAttribution | undefined {
51
+ if (input.exitCode === 0 || input.timedOut || input.stderr === '') return undefined
52
+
53
+ const runner = RUNNER_PREFIX[input.environment]
54
+ if (runner?.test(input.stderr)) {
55
+ const line = input.stderr.split('\n').find((l) => runner.test(l)) ?? ''
56
+ return {
57
+ kind: 'runner-failure',
58
+ message: `The sandbox launcher failed before the command could run (${line.trim()}). This is a runner fault, not a failure of the command; retrying the same command will not help.`,
59
+ }
60
+ }
61
+
62
+ if (!CONFINES_FILES.has(input.environment)) return undefined
63
+
64
+ for (const line of input.stderr.split('\n')) {
65
+ if (EROFS.test(line)) return denial()
66
+ if (!PERMISSION.test(line)) continue
67
+ const paths = pathsIn(line)
68
+ if (paths.some((p) => !input.allowedRoots.some((root) => within(p, root)))) return denial()
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ function denial(): SandboxExecAttribution {
74
+ return {
75
+ kind: 'denial',
76
+ message:
77
+ 'This looks like a sandbox denial: the command touched a path the sandbox does not allow it to write. This is inferred from the error text, not proven. Retrying the same command will fail the same way; if the action is needed, ask to run it outside the sandbox instead.',
78
+ }
79
+ }
@@ -1,5 +1,7 @@
1
+ import { IGNORE_SOURCE } from './ignore-source.js'
2
+
1
3
  /** Stable guest code: host bundlers must never rewrite a function we serialize. */
2
- export const FILE_WALK_PROGRAM = String.raw`
4
+ export const FILE_WALK_PROGRAM = `${IGNORE_SOURCE}${String.raw`
3
5
  const fs = require('node:fs/promises');
4
6
  const path = require('node:path');
5
7
  const plan = JSON.parse(process.argv[1]);
@@ -9,10 +11,17 @@ const write = async value => {
9
11
  await new Promise(resolve => process.stdout.once('drain', resolve));
10
12
  }
11
13
  };
14
+ let stopped;
15
+ let visitedTotal = 0;
16
+ let ignoredDirs = 0;
12
17
  const visit = count => {
13
- if (count > plan.maxVisitedEntries) {
18
+ visitedTotal = count;
19
+ if (count <= plan.maxVisitedEntries) return true;
20
+ if (plan.failOnLimit) {
14
21
  throw Object.assign(new Error('File search stopped after examining ' + plan.maxVisitedEntries + ' entries; narrow its root or pattern.'), {code:'ERR_FILE_WALK_LIMIT'});
15
22
  }
23
+ stopped = 'entry-limit';
24
+ return false;
16
25
  };
17
26
  const patterns = plan.patterns.map(set => set.map(part =>
18
27
  typeof part === 'object' && part !== null ? new RegExp(part.source, part.flags + 's') : part));
@@ -41,9 +50,13 @@ async function run() {
41
50
  let emitted = 0;
42
51
  const matchers = plan.expressions.map(({source, flags}) => new RegExp(source, flags + 's'));
43
52
  const maxDepth = plan.maxDepth === null ? Infinity : plan.maxDepth;
44
- const open = async (absolute, relative, depth) => {
45
- try { stack.push({dir: await fs.opendir(absolute), relative, depth}); }
46
- catch (error) { if (!missing(error)) throw error; }
53
+ const ignore = plan.respectIgnore ? ignCreate({fs, path, extraFiles: []}) : null;
54
+ const open = async (absolute, relative, depth, layered) => {
55
+ try {
56
+ const dir = await fs.opendir(absolute);
57
+ if (layered && ignore) await ignore.enter(absolute);
58
+ stack.push({dir, relative, depth, layered});
59
+ } catch (error) { if (!missing(error)) throw error; }
47
60
  };
48
61
  try {
49
62
  if (plan.patterns.length === 0) return;
@@ -53,36 +66,42 @@ async function run() {
53
66
  if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
54
67
  const rootInfo = await fs.lstat(plan.root);
55
68
  if (rootInfo.isFile()) {
56
- visit(++visited);
69
+ if (!visit(++visited)) return;
57
70
  if (matchers.some(matcher => matcher.test(path.basename(plan.root)))) {
58
71
  await write({type:'entry', path:plan.root, size:rootInfo.size});
59
72
  }
60
73
  return;
61
74
  }
75
+ if (ignore) { await ignore.init(plan.root); await ignore.enter(plan.root); }
62
76
  let start = plan.root;
63
77
  for (const part of plan.prefix) {
64
78
  start = path.join(start, part);
65
79
  let info;
66
80
  try { info = await fs.lstat(start); }
67
81
  catch (error) { if (missing(error)) return; throw error; }
68
- visit(++visited);
82
+ if (!visit(++visited)) return;
69
83
  if (!info.isDirectory()) return;
84
+ if (ignore) await ignore.enter(start);
70
85
  }
71
86
  if (plan.prefix.length >= maxDepth) return;
72
- await open(start, plan.prefix.join('/'), plan.prefix.length);
87
+ await open(start, plan.prefix.join('/'), plan.prefix.length, false);
73
88
  while (stack.length) {
74
89
  const frame = stack[stack.length - 1];
75
90
  const entry = await frame.dir.read();
76
- if (!entry) { stack.pop(); await frame.dir.close(); continue; }
77
- visit(++visited);
91
+ if (!entry) { stack.pop(); if (frame.layered && ignore) ignore.leave(); await frame.dir.close(); continue; }
92
+ if (!visit(++visited)) return;
93
+ if (IGN_VCS.includes(entry.name)) continue;
78
94
  const relative = frame.relative ? frame.relative + '/' + entry.name : entry.name;
79
95
  const absolute = path.join(plan.root, relative);
80
96
  const depth = frame.depth + 1;
81
97
  if (entry.isDirectory()) {
82
- if (depth < maxDepth && canDescend(relative)) await open(absolute, relative, depth);
98
+ if (depth >= maxDepth || !canDescend(relative)) continue;
99
+ if (ignore && ignore.isIgnored(absolute, true)) { ignoredDirs++; continue; }
100
+ await open(absolute, relative, depth, true);
83
101
  continue;
84
102
  }
85
103
  if (!entry.isFile() || !matchers.some(matcher => matcher.test(relative))) continue;
104
+ if (ignore && ignore.isIgnored(absolute, false)) continue;
86
105
  let info;
87
106
  try { info = await fs.lstat(absolute); }
88
107
  catch (error) { if (missing(error)) continue; throw error; }
@@ -95,7 +114,7 @@ async function run() {
95
114
  }
96
115
  }
97
116
  run().then(
98
- () => write({type:'done'}),
117
+ () => write({type:'done', visited:visitedTotal, ignoredDirs, stopped}),
99
118
  async error => { await write({type:'error', message:error.message, code:error.code}); process.exitCode = 1; },
100
119
  );
101
- `
120
+ `}`
@@ -1,4 +1,4 @@
1
- import { isAbsolute, posix } from 'node:path'
1
+ import { isAbsolute, join, posix } from 'node:path'
2
2
  import braceExpansion from 'brace-expansion'
3
3
  import { GLOBSTAR, Minimatch } from 'minimatch'
4
4
  import type {
@@ -6,9 +6,14 @@ import type {
6
6
  SandboxExecResult,
7
7
  SandboxFileEntry,
8
8
  SandboxWalkFilesOptions,
9
+ SandboxWalkSummary,
9
10
  } from '../types/sandbox/index.js'
10
11
  import { subscribeToAbort } from '../utils/abort.js'
11
12
  import { FILE_WALK_PROGRAM } from './file-walk-program.js'
13
+ import { VCS_NAMES, createIgnoreStack } from './ignore-matcher.js'
14
+
15
+ /** Entries a walk examines before it stops with what it has: a safety valve, not a budget. */
16
+ export const DEFAULT_MAX_VISITED_ENTRIES = 500_000
12
17
 
13
18
  interface WalkPlan {
14
19
  root: string
@@ -18,6 +23,8 @@ interface WalkPlan {
18
23
  maxEntries: number
19
24
  maxVisitedEntries: number
20
25
  includeHidden: boolean
26
+ respectIgnore: boolean
27
+ failOnLimit: boolean
21
28
  patterns: (string | { source: string; flags: string } | null)[][]
22
29
  }
23
30
 
@@ -103,8 +110,13 @@ function planWalk(root: string, options: SandboxWalkFilesOptions): WalkPlan {
103
110
  expressions,
104
111
  maxDepth: Number.isFinite(maxDepth) ? maxDepth : null,
105
112
  maxEntries: positiveInteger(options.maxEntries, 'maxEntries'),
106
- maxVisitedEntries: positiveInteger(options.maxVisitedEntries ?? 20_000, 'maxVisitedEntries'),
113
+ maxVisitedEntries: positiveInteger(
114
+ options.maxVisitedEntries ?? DEFAULT_MAX_VISITED_ENTRIES,
115
+ 'maxVisitedEntries',
116
+ ),
107
117
  includeHidden: options.includeHidden ?? false,
118
+ respectIgnore: options.respectIgnore ?? true,
119
+ failOnLimit: options.failOnLimit ?? false,
108
120
  patterns: sets.map((set) =>
109
121
  set.map((segment) =>
110
122
  segment === GLOBSTAR
@@ -151,13 +163,25 @@ function directoryMatcher(plan: WalkPlan): (relative: string) => boolean {
151
163
  async function* executeWalk(
152
164
  plan: WalkPlan,
153
165
  signal?: AbortSignal,
166
+ onSummary?: (summary: SandboxWalkSummary) => void,
154
167
  ): AsyncGenerator<SandboxFileEntry> {
155
168
  const fs = await import('node:fs/promises')
156
169
  const path = await import('node:path')
157
170
  const { EventEmitter } = await import('node:events')
158
- const open: { dir: import('node:fs').Dir; relative: string; depth: number }[] = []
171
+ const open: {
172
+ dir: import('node:fs').Dir
173
+ relative: string
174
+ depth: number
175
+ /** Whether leaving this directory also drops its ignore layer. */
176
+ layered: boolean
177
+ }[] = []
159
178
  let visited = 0
160
179
  let emitted = 0
180
+ let ignoredDirectories = 0
181
+ let stopped: SandboxWalkSummary['stopped']
182
+ const ignore = plan.respectIgnore
183
+ ? createIgnoreStack({ fs, path, extraFiles: globalIgnoreFiles() })
184
+ : undefined
161
185
  const matchers = plan.expressions.map(({ source, flags }) => new RegExp(source, `${flags}s`))
162
186
  const canDescend = directoryMatcher(plan)
163
187
  const maxDepth = plan.maxDepth ?? Number.POSITIVE_INFINITY
@@ -204,9 +228,11 @@ async function* executeWalk(
204
228
  dispose?.()
205
229
  }
206
230
  }
231
+ /** False once the safety valve trips; the walk then ends with what it has. */
207
232
  const noteVisit = () => {
208
233
  visited += 1
209
- if (visited > plan.maxVisitedEntries) {
234
+ if (visited <= plan.maxVisitedEntries) return true
235
+ if (plan.failOnLimit) {
210
236
  throw Object.assign(
211
237
  new Error(
212
238
  `File search stopped after examining ${plan.maxVisitedEntries} entries; narrow its root or pattern.`,
@@ -214,6 +240,8 @@ async function* executeWalk(
214
240
  { code: 'ERR_FILE_WALK_LIMIT' },
215
241
  )
216
242
  }
243
+ stopped = 'entry-limit'
244
+ return false
217
245
  }
218
246
  try {
219
247
  check()
@@ -231,11 +259,14 @@ async function* executeWalk(
231
259
  const rootInfo = await wait(fs.lstat(plan.root))
232
260
  check()
233
261
  if (rootInfo.isFile()) {
234
- noteVisit()
262
+ if (!noteVisit()) return
235
263
  if (matchers.some((matcher) => matcher.test(path.basename(plan.root))))
236
264
  yield { path: plan.root, size: rootInfo.size }
237
265
  return
238
266
  }
267
+ // An explicit root is never itself ignored: only what is below it is tested.
268
+ await ignore?.init(plan.root)
269
+ await ignore?.enter(plan.root)
239
270
  let start = plan.root
240
271
  // Never follow a static-prefix symlink to speed up a pattern: traversal
241
272
  // and its optimized starting point must have the same link policy.
@@ -249,22 +280,29 @@ async function* executeWalk(
249
280
  if (missing(error)) return
250
281
  throw error
251
282
  }
252
- noteVisit()
283
+ if (!noteVisit()) return
253
284
  if (!info.isDirectory()) return
285
+ await ignore?.enter(start)
254
286
  }
255
287
  if (plan.prefix.length >= maxDepth) return
256
- const openDirectory = async (absolute: string, relative: string, depth: number) => {
288
+ const openDirectory = async (
289
+ absolute: string,
290
+ relative: string,
291
+ depth: number,
292
+ layered: boolean,
293
+ ) => {
257
294
  check()
258
295
  try {
259
296
  const dir = await wait(fs.opendir(absolute), (late) => {
260
297
  void close(late).catch(() => {})
261
298
  })
262
- open.push({ dir, relative, depth })
299
+ if (layered) await ignore?.enter(absolute)
300
+ open.push({ dir, relative, depth, layered })
263
301
  } catch (error) {
264
302
  if (!missing(error)) throw error
265
303
  }
266
304
  }
267
- await openDirectory(start, plan.prefix.join('/'), plan.prefix.length)
305
+ await openDirectory(start, plan.prefix.join('/'), plan.prefix.length, false)
268
306
  while (open.length > 0) {
269
307
  check()
270
308
  const frame = open[open.length - 1] as (typeof open)[number]
@@ -272,18 +310,26 @@ async function* executeWalk(
272
310
  check()
273
311
  if (!entry) {
274
312
  open.pop()
313
+ if (frame.layered) ignore?.leave()
275
314
  await close(frame.dir)
276
315
  continue
277
316
  }
278
- noteVisit()
317
+ if (!noteVisit()) return
318
+ if (VCS_NAMES.has(entry.name)) continue
279
319
  const relative = frame.relative ? `${frame.relative}/${entry.name}` : entry.name
280
320
  const absolute = path.join(plan.root, relative)
281
321
  const depth = frame.depth + 1
282
322
  if (entry.isDirectory()) {
283
- if (depth < maxDepth && canDescend(relative)) await openDirectory(absolute, relative, depth)
323
+ if (depth >= maxDepth || !canDescend(relative)) continue
324
+ if (ignore?.isIgnored(absolute, true)) {
325
+ ignoredDirectories += 1
326
+ continue
327
+ }
328
+ await openDirectory(absolute, relative, depth, true)
284
329
  continue
285
330
  }
286
331
  if (!entry.isFile() || !matchers.some((matcher) => matcher.test(relative))) continue
332
+ if (ignore?.isIgnored(absolute, false)) continue
287
333
  let info: import('node:fs').Stats
288
334
  try {
289
335
  info = await wait(fs.lstat(absolute))
@@ -303,16 +349,24 @@ async function* executeWalk(
303
349
  void close(dir).catch(() => {})
304
350
  } else await close(dir)
305
351
  }
352
+ onSummary?.({ visited, ignoredDirectories, ...(stopped ? { stopped } : {}) })
306
353
  }
307
354
  }
308
355
 
356
+ /** Global exclude files, lowest precedence. Only the host has them. */
357
+ function globalIgnoreFiles(): string[] {
358
+ const home = process.env.HOME ?? process.env.USERPROFILE
359
+ const base = process.env.XDG_CONFIG_HOME || (home ? join(home, '.config') : undefined)
360
+ return base ? [join(base, 'git', 'ignore')] : []
361
+ }
362
+
309
363
  /** Internal host path; the caller first resolves its authorized filesystem root. */
310
364
  export async function* walkFilesLocally(
311
365
  rootPath: string,
312
366
  options: SandboxWalkFilesOptions,
313
367
  ): AsyncGenerator<SandboxFileEntry> {
314
368
  options.signal?.throwIfAborted()
315
- yield* executeWalk(planWalk(rootPath, options), options.signal)
369
+ yield* executeWalk(planWalk(rootPath, options), options.signal, options.onSummary)
316
370
  }
317
371
 
318
372
  /** The existing sandbox execution seam; no separate worker protocol is needed. */
@@ -346,6 +400,7 @@ export async function* walkFilesViaExec(
346
400
  let terminal = false
347
401
  let received = 0
348
402
  let failure: unknown
403
+ const outcome: { summary?: SandboxWalkSummary } = {}
349
404
  const reject = (error: unknown) => {
350
405
  failure ??= error instanceof Error ? error : new Error(String(error))
351
406
  controller.abort(error)
@@ -358,6 +413,13 @@ export async function* walkFilesViaExec(
358
413
  const record = value as Record<string, unknown>
359
414
  if (record.type === 'done') {
360
415
  terminal = true
416
+ const count = (value: unknown) =>
417
+ typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0
418
+ outcome.summary = {
419
+ visited: count(record.visited),
420
+ ignoredDirectories: count(record.ignoredDirs),
421
+ ...(record.stopped === 'entry-limit' ? { stopped: 'entry-limit' as const } : {}),
422
+ }
361
423
  return
362
424
  }
363
425
  if (record.type === 'error' && typeof record.message === 'string') {
@@ -462,6 +524,7 @@ export async function* walkFilesViaExec(
462
524
  }
463
525
  } finally {
464
526
  dispose?.()
527
+ if (outcome.summary) options.onSummary?.(outcome.summary)
465
528
  if (!settled) controller.abort(new Error('File walk consumer stopped'))
466
529
  await pending
467
530
  // A rejected cancellation can mean remote work is still alive. Preserve
@@ -0,0 +1,46 @@
1
+ import { IGNORE_SOURCE } from './ignore-source.js'
2
+
3
+ /** One compiled ignore rule. */
4
+ export interface IgnoreRule {
5
+ readonly negate: boolean
6
+ readonly dirOnly: boolean
7
+ readonly re: RegExp
8
+ }
9
+
10
+ /** Layered ignore rules for one depth-first walk; see `ignore-source.ts`. */
11
+ export interface IgnoreStack {
12
+ /** Find the enclosing repository and load the layers above the walk root. */
13
+ init(root: string): Promise<void>
14
+ /** Load a directory's rule files; call before reading it. */
15
+ enter(directory: string): Promise<void>
16
+ /** Drop the layer of the directory most recently entered. */
17
+ leave(): void
18
+ isIgnored(absolute: string, isDirectory: boolean): boolean
19
+ }
20
+
21
+ export interface IgnoreDeps {
22
+ readonly fs: unknown
23
+ readonly path: unknown
24
+ /** Global exclude files, lowest precedence. The guest has none. */
25
+ readonly extraFiles?: readonly string[]
26
+ }
27
+
28
+ interface IgnoreModule {
29
+ parse(text: string): IgnoreRule[]
30
+ decide(rules: readonly IgnoreRule[], relative: string, isDirectory: boolean): 0 | 1 | 2
31
+ create(deps: IgnoreDeps): IgnoreStack
32
+ vcs: readonly string[]
33
+ }
34
+
35
+ // The same text is embedded in the guest program, so both walks share it.
36
+ const module_ = new Function(
37
+ `${IGNORE_SOURCE}\nreturn { parse: ignParse, decide: ignDecide, create: ignCreate, vcs: IGN_VCS }`,
38
+ )() as IgnoreModule
39
+
40
+ /** Parse the text of one ignore file into rules; a line it cannot read is dropped. */
41
+ export const parseIgnoreRules = module_.parse
42
+ /** 0: no rule matches, 1: ignored, 2: re-included. The last matching rule wins. */
43
+ export const decideIgnore = module_.decide
44
+ export const createIgnoreStack = module_.create
45
+ /** Version-control directories that no walk ever descends into. */
46
+ export const VCS_NAMES: ReadonlySet<string> = new Set(module_.vcs)