@namzu/sdk 59.0.0 → 60.0.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 (260) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/compaction/context-window.d.ts +53 -0
  3. package/dist/compaction/context-window.d.ts.map +1 -1
  4. package/dist/compaction/context-window.js +141 -2
  5. package/dist/compaction/context-window.js.map +1 -1
  6. package/dist/execution/workflow/index.d.ts +3 -1
  7. package/dist/execution/workflow/index.d.ts.map +1 -1
  8. package/dist/execution/workflow/index.js +2 -1
  9. package/dist/execution/workflow/index.js.map +1 -1
  10. package/dist/execution/workflow/journal.d.ts +24 -4
  11. package/dist/execution/workflow/journal.d.ts.map +1 -1
  12. package/dist/execution/workflow/journal.js +131 -9
  13. package/dist/execution/workflow/journal.js.map +1 -1
  14. package/dist/execution/workflow/prelude.d.ts +2 -0
  15. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  16. package/dist/execution/workflow/prelude.js +49 -8
  17. package/dist/execution/workflow/prelude.js.map +1 -1
  18. package/dist/execution/workflow/resume.d.ts +59 -0
  19. package/dist/execution/workflow/resume.d.ts.map +1 -0
  20. package/dist/execution/workflow/resume.js +273 -0
  21. package/dist/execution/workflow/resume.js.map +1 -0
  22. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  23. package/dist/execution/workflow/runtime.js +112 -8
  24. package/dist/execution/workflow/runtime.js.map +1 -1
  25. package/dist/execution/workflow/types.d.ts +105 -1
  26. package/dist/execution/workflow/types.d.ts.map +1 -1
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/plugin/compat.d.ts +24 -0
  32. package/dist/plugin/compat.d.ts.map +1 -0
  33. package/dist/plugin/compat.js +149 -0
  34. package/dist/plugin/compat.js.map +1 -0
  35. package/dist/plugin/loader.d.ts +2 -1
  36. package/dist/plugin/loader.d.ts.map +1 -1
  37. package/dist/plugin/loader.js +6 -0
  38. package/dist/plugin/loader.js.map +1 -1
  39. package/dist/pricing/index.d.ts.map +1 -1
  40. package/dist/pricing/index.js +2 -1
  41. package/dist/pricing/index.js.map +1 -1
  42. package/dist/process/kill-tree.d.ts +44 -0
  43. package/dist/process/kill-tree.d.ts.map +1 -1
  44. package/dist/process/kill-tree.js +102 -4
  45. package/dist/process/kill-tree.js.map +1 -1
  46. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  47. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  48. package/dist/prompt/coding-agent-doctrine.js +9 -1
  49. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  50. package/dist/provider/model-window-suffix.d.ts +18 -0
  51. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  52. package/dist/provider/model-window-suffix.js +77 -0
  53. package/dist/provider/model-window-suffix.js.map +1 -0
  54. package/dist/public-runtime.d.ts +5 -3
  55. package/dist/public-runtime.d.ts.map +1 -1
  56. package/dist/public-runtime.js +5 -4
  57. package/dist/public-runtime.js.map +1 -1
  58. package/dist/public-types.d.ts +1 -0
  59. package/dist/public-types.d.ts.map +1 -1
  60. package/dist/registry/command/kernel-commands.d.ts +1 -4
  61. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  62. package/dist/registry/command/kernel-commands.js +0 -28
  63. package/dist/registry/command/kernel-commands.js.map +1 -1
  64. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  65. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  66. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  67. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  68. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  69. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  70. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  71. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  72. package/dist/runtime/jobs/liveness.d.ts +67 -0
  73. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  74. package/dist/runtime/jobs/liveness.js +94 -0
  75. package/dist/runtime/jobs/liveness.js.map +1 -0
  76. package/dist/runtime/jobs/registry.d.ts +21 -0
  77. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  78. package/dist/runtime/jobs/registry.js +104 -2
  79. package/dist/runtime/jobs/registry.js.map +1 -1
  80. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  81. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  82. package/dist/runtime/jobs/wake-budget.js +48 -0
  83. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  84. package/dist/runtime/query/detached-calls.d.ts +65 -0
  85. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  86. package/dist/runtime/query/detached-calls.js +116 -0
  87. package/dist/runtime/query/detached-calls.js.map +1 -0
  88. package/dist/runtime/query/executor.d.ts +4 -1
  89. package/dist/runtime/query/executor.d.ts.map +1 -1
  90. package/dist/runtime/query/executor.js +90 -6
  91. package/dist/runtime/query/executor.js.map +1 -1
  92. package/dist/runtime/query/index.d.ts +20 -0
  93. package/dist/runtime/query/index.d.ts.map +1 -1
  94. package/dist/runtime/query/index.js +31 -5
  95. package/dist/runtime/query/index.js.map +1 -1
  96. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  97. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  98. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  99. package/dist/runtime/query/iteration/phases/context.d.ts +7 -0
  100. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  101. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  102. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  105. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  106. package/dist/runtime/query/session-storage.js +8 -3
  107. package/dist/runtime/query/session-storage.js.map +1 -1
  108. package/dist/runtime/query/steering.d.ts +29 -3
  109. package/dist/runtime/query/steering.d.ts.map +1 -1
  110. package/dist/runtime/query/steering.js +58 -3
  111. package/dist/runtime/query/steering.js.map +1 -1
  112. package/dist/runtime/query/tooling.d.ts +1 -0
  113. package/dist/runtime/query/tooling.d.ts.map +1 -1
  114. package/dist/runtime/query/tooling.js +1 -0
  115. package/dist/runtime/query/tooling.js.map +1 -1
  116. package/dist/runtime/system-events.d.ts +5 -0
  117. package/dist/runtime/system-events.d.ts.map +1 -1
  118. package/dist/runtime/system-events.js +7 -1
  119. package/dist/runtime/system-events.js.map +1 -1
  120. package/dist/sandbox/denial.d.ts +11 -0
  121. package/dist/sandbox/denial.d.ts.map +1 -0
  122. package/dist/sandbox/denial.js +61 -0
  123. package/dist/sandbox/denial.js.map +1 -0
  124. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  125. package/dist/sandbox/file-walk-program.js +31 -13
  126. package/dist/sandbox/file-walk-program.js.map +1 -1
  127. package/dist/sandbox/file-walk.d.ts +2 -0
  128. package/dist/sandbox/file-walk.d.ts.map +1 -1
  129. package/dist/sandbox/file-walk.js +64 -13
  130. package/dist/sandbox/file-walk.js.map +1 -1
  131. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  132. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  133. package/dist/sandbox/ignore-matcher.js +11 -0
  134. package/dist/sandbox/ignore-matcher.js.map +1 -0
  135. package/dist/sandbox/ignore-source.d.ts +26 -0
  136. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  137. package/dist/sandbox/ignore-source.js +221 -0
  138. package/dist/sandbox/ignore-source.js.map +1 -0
  139. package/dist/sandbox/provider/local.d.ts.map +1 -1
  140. package/dist/sandbox/provider/local.js +11 -1
  141. package/dist/sandbox/provider/local.js.map +1 -1
  142. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  143. package/dist/scheduler/completion-inbox.js +1 -0
  144. package/dist/scheduler/completion-inbox.js.map +1 -1
  145. package/dist/session/log-hash.d.ts.map +1 -1
  146. package/dist/session/log-hash.js +18 -1
  147. package/dist/session/log-hash.js.map +1 -1
  148. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  149. package/dist/store/session-index/sqlite.js +169 -16
  150. package/dist/store/session-index/sqlite.js.map +1 -1
  151. package/dist/tools/builtins/bash.d.ts.map +1 -1
  152. package/dist/tools/builtins/bash.js +10 -4
  153. package/dist/tools/builtins/bash.js.map +1 -1
  154. package/dist/tools/builtins/computer-use.d.ts +35 -0
  155. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  156. package/dist/tools/builtins/computer-use.js +512 -67
  157. package/dist/tools/builtins/computer-use.js.map +1 -1
  158. package/dist/tools/builtins/glob.d.ts +1 -0
  159. package/dist/tools/builtins/glob.d.ts.map +1 -1
  160. package/dist/tools/builtins/glob.js +65 -28
  161. package/dist/tools/builtins/glob.js.map +1 -1
  162. package/dist/tools/builtins/grep.d.ts +2 -0
  163. package/dist/tools/builtins/grep.d.ts.map +1 -1
  164. package/dist/tools/builtins/grep.js +94 -40
  165. package/dist/tools/builtins/grep.js.map +1 -1
  166. package/dist/tools/builtins/job.d.ts +1 -1
  167. package/dist/tools/builtins/job.d.ts.map +1 -1
  168. package/dist/tools/builtins/job.js +34 -7
  169. package/dist/tools/builtins/job.js.map +1 -1
  170. package/dist/tools/builtins/search-support.d.ts +44 -0
  171. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  172. package/dist/tools/builtins/search-support.js +89 -0
  173. package/dist/tools/builtins/search-support.js.map +1 -0
  174. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  175. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  176. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  177. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  178. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  179. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  180. package/dist/tools/builtins/wait-for-job.js +13 -2
  181. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  182. package/dist/tools/command-shell.d.ts.map +1 -1
  183. package/dist/tools/command-shell.js +14 -5
  184. package/dist/tools/command-shell.js.map +1 -1
  185. package/dist/types/computer-use/index.d.ts +129 -13
  186. package/dist/types/computer-use/index.d.ts.map +1 -1
  187. package/dist/types/computer-use/index.js.map +1 -1
  188. package/dist/types/plugin/index.d.ts +24 -0
  189. package/dist/types/plugin/index.d.ts.map +1 -1
  190. package/dist/types/plugin/index.js +4 -0
  191. package/dist/types/plugin/index.js.map +1 -1
  192. package/dist/types/sandbox/index.d.ts +38 -1
  193. package/dist/types/sandbox/index.d.ts.map +1 -1
  194. package/dist/types/sandbox/index.js.map +1 -1
  195. package/dist/types/session/records.d.ts +264 -0
  196. package/dist/types/session/records.d.ts.map +1 -1
  197. package/dist/types/session/records.js +32 -1
  198. package/dist/types/session/records.js.map +1 -1
  199. package/dist/types/tool/index.d.ts +30 -0
  200. package/dist/types/tool/index.d.ts.map +1 -1
  201. package/dist/types/tool/index.js.map +1 -1
  202. package/dist/types/tool/presentation.d.ts +8 -0
  203. package/dist/types/tool/presentation.d.ts.map +1 -1
  204. package/package.json +1 -1
  205. package/src/compaction/context-window.ts +163 -2
  206. package/src/execution/workflow/index.ts +14 -1
  207. package/src/execution/workflow/journal.ts +161 -11
  208. package/src/execution/workflow/prelude.ts +51 -8
  209. package/src/execution/workflow/resume.ts +313 -0
  210. package/src/execution/workflow/runtime.ts +127 -9
  211. package/src/execution/workflow/types.ts +109 -1
  212. package/src/index.ts +17 -0
  213. package/src/plugin/compat.ts +173 -0
  214. package/src/plugin/loader.ts +7 -1
  215. package/src/pricing/index.ts +2 -1
  216. package/src/process/kill-tree.ts +106 -4
  217. package/src/prompt/coding-agent-doctrine.ts +9 -1
  218. package/src/provider/model-window-suffix.ts +84 -0
  219. package/src/public-runtime.ts +21 -2
  220. package/src/public-types.ts +1 -0
  221. package/src/registry/command/kernel-commands.ts +1 -32
  222. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  223. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  224. package/src/runtime/jobs/liveness.ts +144 -0
  225. package/src/runtime/jobs/registry.ts +114 -2
  226. package/src/runtime/jobs/wake-budget.ts +55 -0
  227. package/src/runtime/query/detached-calls.ts +135 -0
  228. package/src/runtime/query/executor.ts +92 -10
  229. package/src/runtime/query/index.ts +50 -7
  230. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  231. package/src/runtime/query/iteration/phases/context.ts +7 -0
  232. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  233. package/src/runtime/query/session-storage.ts +8 -3
  234. package/src/runtime/query/steering.ts +77 -6
  235. package/src/runtime/query/tooling.ts +2 -0
  236. package/src/runtime/system-events.ts +12 -1
  237. package/src/sandbox/denial.ts +79 -0
  238. package/src/sandbox/file-walk-program.ts +32 -13
  239. package/src/sandbox/file-walk.ts +75 -12
  240. package/src/sandbox/ignore-matcher.ts +46 -0
  241. package/src/sandbox/ignore-source.ts +220 -0
  242. package/src/sandbox/provider/local.ts +11 -1
  243. package/src/scheduler/completion-inbox.ts +1 -0
  244. package/src/session/log-hash.ts +21 -0
  245. package/src/store/session-index/sqlite.ts +171 -16
  246. package/src/tools/builtins/bash.ts +11 -4
  247. package/src/tools/builtins/computer-use.ts +593 -68
  248. package/src/tools/builtins/glob.ts +79 -32
  249. package/src/tools/builtins/grep.ts +107 -43
  250. package/src/tools/builtins/job.ts +42 -12
  251. package/src/tools/builtins/search-support.ts +107 -0
  252. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  253. package/src/tools/builtins/wait-for-job.ts +14 -2
  254. package/src/tools/command-shell.ts +13 -5
  255. package/src/types/computer-use/index.ts +157 -15
  256. package/src/types/plugin/index.ts +8 -0
  257. package/src/types/sandbox/index.ts +40 -1
  258. package/src/types/session/records.ts +36 -2
  259. package/src/types/tool/index.ts +30 -0
  260. package/src/types/tool/presentation.ts +8 -0
@@ -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)
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The ignore-file matcher, as plain JavaScript source text.
3
+ *
4
+ * One implementation serves two runtimes. The host walker evaluates this text
5
+ * once (see `ignore-matcher.ts`); the guest program (`file-walk-program.ts`)
6
+ * embeds the same text, because that program travels as a single `node -e`
7
+ * argument with stdin closed and cannot import a package. Keeping it as
8
+ * source text, not a serialised function, means a host bundler cannot rewrite
9
+ * it, and the two walks cannot drift apart.
10
+ *
11
+ * Rules follow the gitignore format: `#` comments, `\#` and `\!` escapes,
12
+ * unescaped trailing spaces dropped, `!` negation, a leading or inner `/`
13
+ * anchors a rule to the file that holds it, a trailing `/` matches
14
+ * directories only, and `*`, `?`, `[...]`, `**` as git defines them. A deeper
15
+ * file beats a shallower one, `.ignore` beats `.gitignore` in the same
16
+ * directory, and the last matching line of a file wins. A rule this matcher
17
+ * does not understand is dropped (an unclosed `[`, or more than three
18
+ * separate `*` in one rule, which could backtrack for seconds), so a visible
19
+ * file is never hidden by one.
20
+ *
21
+ * Nothing here is allowed to contain a backtick or a `${`: it lives inside a
22
+ * template literal. Every helper is prefixed `ign` so that, inlined into the
23
+ * guest program, it cannot collide with that program's own names.
24
+ */
25
+ export const IGNORE_SOURCE = String.raw`
26
+ const IGN_MAX_BYTES = 262144;
27
+ const IGN_MAX_RULES = 10000;
28
+ const IGN_MAX_PATTERN = 1024;
29
+ const IGN_MAX_STARS = 3;
30
+ const IGN_VCS = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl'];
31
+ const IGN_POSIX_CLASSES = {
32
+ alpha: 'a-zA-Z', digit: '0-9', alnum: 'a-zA-Z0-9', upper: 'A-Z', lower: 'a-z',
33
+ space: ' \\t\\n\\r\\f\\v', punct: '!-\\/:-@\\[-\`{-~', xdigit: '0-9A-Fa-f',
34
+ };
35
+ function ignEscape(c) { return /[\\^$.*+?()[\]{}|\/-]/.test(c) ? '\\' + c : c; }
36
+ function ignClass(body) {
37
+ let negate = false;
38
+ if (body[0] === '!' || body[0] === '^') { negate = true; body = body.slice(1); }
39
+ let out = '';
40
+ for (let i = 0; i < body.length; i++) {
41
+ const c = body[i];
42
+ if (c === '[' && body[i + 1] === ':') {
43
+ const end = body.indexOf(':]', i + 2);
44
+ const name = end < 0 ? '' : body.slice(i + 2, end);
45
+ if (IGN_POSIX_CLASSES[name]) { out += IGN_POSIX_CLASSES[name]; i = end + 1; continue; }
46
+ }
47
+ if (c === '\\') { i++; if (i < body.length) out += ignEscape(body[i]); continue; }
48
+ if (c === '-') { out += (out === '' || i === body.length - 1) ? '\\-' : '-'; continue; }
49
+ out += (c === '[' || c === ']' || c === '^') ? '\\' + c : c;
50
+ }
51
+ if (out === '') return null;
52
+ return '[' + (negate ? '^/' : '') + out + ']';
53
+ }
54
+ function ignTranslate(p) {
55
+ let out = '';
56
+ let i = 0;
57
+ let stars = 0;
58
+ while (i < p.length) {
59
+ const c = p[i];
60
+ if (c === '\\') {
61
+ if (i + 1 >= p.length) return null;
62
+ out += ignEscape(p[i + 1]); i += 2; continue;
63
+ }
64
+ if (c === '*') {
65
+ let j = i;
66
+ while (p[j] === '*') j++;
67
+ if (j - i >= 2 && (i === 0 || p[i - 1] === '/') && (j === p.length || p[j] === '/')) {
68
+ if (j === p.length) out += '.*';
69
+ else { out += '(?:.*/)?'; j++; }
70
+ i = j; continue;
71
+ }
72
+ stars++;
73
+ out += '[^/]*'; i = j; continue;
74
+ }
75
+ if (c === '?') { out += '[^/]'; i++; continue; }
76
+ if (c === '[') {
77
+ let j = i + 1;
78
+ if (p[j] === '!' || p[j] === '^') j++;
79
+ if (p[j] === ']') j++;
80
+ while (j < p.length && p[j] !== ']') { if (p[j] === '\\') j++; j++; }
81
+ if (j >= p.length) return null;
82
+ const cls = ignClass(p.slice(i + 1, j));
83
+ if (cls === null) return null;
84
+ out += cls; i = j + 1; continue;
85
+ }
86
+ out += ignEscape(c); i++;
87
+ }
88
+ // Each separate star can backtrack against the others: a rule with many of
89
+ // them takes time exponential in their number on one long name. Real rules
90
+ // use one or two; a rule past the cap is dropped, like any rule not understood.
91
+ if (stars > IGN_MAX_STARS) return null;
92
+ return out;
93
+ }
94
+ /** One line to one rule, or null when the line holds none. */
95
+ function ignRule(raw) {
96
+ let line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
97
+ while (line.endsWith(' ') && !line.endsWith('\\ ')) line = line.slice(0, -1);
98
+ if (line === '' || line[0] === '#' || line.length > IGN_MAX_PATTERN) return null;
99
+ let negate = false;
100
+ if (line[0] === '!') { negate = true; line = line.slice(1); }
101
+ let dirOnly = false;
102
+ while (line.length > 1 && line.endsWith('/')) { dirOnly = true; line = line.slice(0, -1); }
103
+ if (line === '/' || line === '') return null;
104
+ const anchored = line.indexOf('/') >= 0;
105
+ if (line[0] === '/') line = line.slice(1);
106
+ line = line.replace(/(\*\*\/)+/g, '**/');
107
+ if ((line.match(/\*\*/g) || []).length > 4) return null;
108
+ let body;
109
+ try { body = ignTranslate(line); } catch (error) { return null; }
110
+ if (body === null) return null;
111
+ try {
112
+ return { negate, dirOnly, re: new RegExp((anchored ? '^' : '^(?:.*/)?') + body + '$') };
113
+ } catch (error) { return null; }
114
+ }
115
+ function ignParse(text) {
116
+ if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
117
+ if (text.length > IGN_MAX_BYTES) text = text.slice(0, text.lastIndexOf('\n', IGN_MAX_BYTES) + 1);
118
+ const rules = [];
119
+ for (const line of text.split('\n')) {
120
+ const rule = ignRule(line);
121
+ if (rule) rules.push(rule);
122
+ if (rules.length >= IGN_MAX_RULES) break;
123
+ }
124
+ return rules;
125
+ }
126
+ /** 0: no rule matches. 1: ignored. 2: re-included. The last matching rule wins. */
127
+ function ignDecide(rules, relative, isDirectory) {
128
+ for (let i = rules.length - 1; i >= 0; i--) {
129
+ const rule = rules[i];
130
+ if (rule.dirOnly && !isDirectory) continue;
131
+ if (rule.re.test(relative)) return rule.negate ? 2 : 1;
132
+ }
133
+ return 0;
134
+ }
135
+ /**
136
+ * The layered rules for one walk. The caller enters a directory before it
137
+ * reads it and leaves it after, so the layers follow the depth-first stack.
138
+ * deps: fs (promises), path, extraFiles (global exclude files, host only).
139
+ */
140
+ function ignCreate(deps) {
141
+ const fs = deps.fs;
142
+ const path = deps.path;
143
+ const layers = [];
144
+ const frames = [];
145
+ let ancestorsInRepo = false;
146
+ const posix = value => path.sep === '/' ? value : value.split(path.sep).join('/');
147
+ const lstat = async file => { try { return await fs.lstat(file); } catch (error) { return null; } };
148
+ const load = async file => {
149
+ const info = await lstat(file);
150
+ if (!info || !info.isFile() || info.size > IGN_MAX_BYTES * 4) return [];
151
+ try { return ignParse(await fs.readFile(file, 'utf8')); } catch (error) { return []; }
152
+ };
153
+ const loadDirectory = async (directory, inRepo) => {
154
+ const rules = [];
155
+ if (inRepo) rules.push(...await load(path.join(directory, '.gitignore')));
156
+ rules.push(...await load(path.join(directory, '.ignore')));
157
+ return rules;
158
+ };
159
+ const push = (directory, rules) => {
160
+ layers.push({ base: posix(directory).replace(/\/$/, ''), rules });
161
+ };
162
+ return {
163
+ /** Find the enclosing repository and load the layers above the root. */
164
+ async init(root) {
165
+ let directory = root;
166
+ let repo = null;
167
+ for (;;) {
168
+ const info = await lstat(path.join(directory, '.git'));
169
+ if (info) { repo = { directory, isDirectory: info.isDirectory() }; break; }
170
+ const parent = path.dirname(directory);
171
+ if (parent === directory) break;
172
+ directory = parent;
173
+ }
174
+ if (!repo || repo.directory === root) return;
175
+ ancestorsInRepo = true;
176
+ const chain = [];
177
+ for (let up = path.dirname(root); ; up = path.dirname(up)) {
178
+ chain.unshift(up);
179
+ if (up === repo.directory || path.dirname(up) === up) break;
180
+ }
181
+ for (const [index, directory] of chain.entries()) {
182
+ const rules = [];
183
+ if (index === 0) {
184
+ for (const file of deps.extraFiles || []) rules.push(...await load(file));
185
+ if (repo.isDirectory) rules.push(...await load(path.join(directory, '.git', 'info', 'exclude')));
186
+ }
187
+ rules.push(...await loadDirectory(directory, true));
188
+ push(directory, rules);
189
+ }
190
+ },
191
+ async enter(directory) {
192
+ const parentInRepo = frames.length ? frames[frames.length - 1] : ancestorsInRepo;
193
+ let inRepo = parentInRepo;
194
+ const rules = [];
195
+ if (!parentInRepo) {
196
+ const info = await lstat(path.join(directory, '.git'));
197
+ if (info) {
198
+ inRepo = true;
199
+ for (const file of deps.extraFiles || []) rules.push(...await load(file));
200
+ if (info.isDirectory()) rules.push(...await load(path.join(directory, '.git', 'info', 'exclude')));
201
+ }
202
+ }
203
+ rules.push(...await loadDirectory(directory, inRepo));
204
+ frames.push(inRepo);
205
+ push(directory, rules);
206
+ },
207
+ leave() { frames.pop(); layers.pop(); },
208
+ isIgnored(absolute, isDirectory) {
209
+ const target = posix(absolute);
210
+ for (let i = layers.length - 1; i >= 0; i--) {
211
+ const layer = layers[i];
212
+ if (layer.rules.length === 0 || !target.startsWith(layer.base + '/')) continue;
213
+ const decision = ignDecide(layer.rules, target.slice(layer.base.length + 1), isDirectory);
214
+ if (decision) return decision === 1;
215
+ }
216
+ return false;
217
+ },
218
+ };
219
+ }
220
+ `
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os'
14
14
  import { dirname, isAbsolute, join, parse, resolve } from 'node:path'
15
15
  import { NAMZU } from '../../constants/telemetry/index.js'
16
16
  import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
17
+ import { attributeSandboxFailure } from '../denial.js'
17
18
 
18
19
  import {
19
20
  SANDBOX_DEFAULT_TIMEOUT_MS,
@@ -1084,8 +1085,16 @@ class LocalSandbox implements Sandbox {
1084
1085
  terminate('SIGKILL')
1085
1086
  }
1086
1087
  const timedOut = cancellation === 'deadline'
1088
+ const exitCode = code ?? (timedOut ? 124 : 1)
1089
+ const attribution = attributeSandboxFailure({
1090
+ environment: this.environment,
1091
+ exitCode,
1092
+ stderr: stderr.text,
1093
+ timedOut,
1094
+ allowedRoots: [...this.roots, tmpdir(), '/tmp', '/var/tmp', '/dev'],
1095
+ })
1087
1096
  resolvePromise({
1088
- exitCode: code ?? (timedOut ? 124 : 1),
1097
+ exitCode,
1089
1098
  stdout: stdout.text,
1090
1099
  stderr: stderr.text,
1091
1100
  signal: signal ?? undefined,
@@ -1096,6 +1105,7 @@ class LocalSandbox implements Sandbox {
1096
1105
  // gone. The tool layer already renders the flag.
1097
1106
  stdoutTruncated: stdout.truncated,
1098
1107
  stderrTruncated: stderr.truncated,
1108
+ ...(attribution !== undefined ? { attribution } : {}),
1099
1109
  })
1100
1110
  })
1101
1111
 
@@ -789,6 +789,7 @@ export function formatCompletionNotification(handles: readonly TaskHandle[]): st
789
789
  : []),
790
790
  ...(handle.result?.stopReason ? [`stop_reason: ${handle.result.stopReason}`] : []),
791
791
  ...(durationMs !== undefined ? [`duration_ms: ${durationMs}`] : []),
792
+ ...(handle.completedAt ? [`finished_at: ${new Date(handle.completedAt).toISOString()}`] : []),
792
793
  '',
793
794
  body,
794
795
  // Outside the envelope, deliberately: this sentence is an
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import {
3
+ IgnorableRecordSchema,
3
4
  SESSION_RECORD_MAX_BYTES,
4
5
  type SessionRecord,
5
6
  SessionRecordSchema,
@@ -99,6 +100,18 @@ export function parseSessionLogLine(line: string | Uint8Array): ParsedSessionLog
99
100
  )
100
101
  }
101
102
  const parsed = SessionRecordSchema.safeParse(value)
103
+ if (!parsed.success && isFlaggedIgnorable(value)) {
104
+ const ignorable = IgnorableRecordSchema.safeParse(value)
105
+ if (ignorable.success) {
106
+ // Typed as a record so the chain can carry it; its `type` is one no
107
+ // consumer matches, so it never moves a fold.
108
+ return {
109
+ record: ignorable.data as unknown as SessionRecord,
110
+ length: bytes.byteLength,
111
+ sha256: recordSha256(bytes),
112
+ }
113
+ }
114
+ }
102
115
  if (!parsed.success) {
103
116
  throw new SessionLogLineError(
104
117
  'not-a-record',
@@ -113,3 +126,11 @@ export function parseSessionLogLine(line: string | Uint8Array): ParsedSessionLog
113
126
  sha256: recordSha256(bytes),
114
127
  }
115
128
  }
129
+
130
+ function isFlaggedIgnorable(value: unknown): boolean {
131
+ return (
132
+ typeof value === 'object' &&
133
+ value !== null &&
134
+ (value as { ignorable?: unknown }).ignorable === true
135
+ )
136
+ }