@namzu/sdk 52.4.0 → 52.5.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 (143) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/authorization/gate.d.ts +11 -0
  3. package/dist/authorization/gate.d.ts.map +1 -1
  4. package/dist/authorization/gate.js +35 -0
  5. package/dist/authorization/gate.js.map +1 -1
  6. package/dist/authorization/removal.js +11 -6
  7. package/dist/authorization/removal.js.map +1 -1
  8. package/dist/authorization/rules.d.ts.map +1 -1
  9. package/dist/authorization/rules.js +30 -2
  10. package/dist/authorization/rules.js.map +1 -1
  11. package/dist/authorization/shell-paths.d.ts +20 -0
  12. package/dist/authorization/shell-paths.d.ts.map +1 -1
  13. package/dist/authorization/shell-paths.js +254 -52
  14. package/dist/authorization/shell-paths.js.map +1 -1
  15. package/dist/bridge/acp/server.d.ts +8 -0
  16. package/dist/bridge/acp/server.d.ts.map +1 -1
  17. package/dist/bridge/acp/server.js +7 -0
  18. package/dist/bridge/acp/server.js.map +1 -1
  19. package/dist/manager/resident/outbox.d.ts +4 -4
  20. package/dist/public-runtime.d.ts +2 -1
  21. package/dist/public-runtime.d.ts.map +1 -1
  22. package/dist/public-runtime.js +4 -1
  23. package/dist/public-runtime.js.map +1 -1
  24. package/dist/public-types.d.ts +1 -0
  25. package/dist/public-types.d.ts.map +1 -1
  26. package/dist/runtime/query/auto-review/classifier.d.ts +130 -0
  27. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -0
  28. package/dist/runtime/query/auto-review/classifier.js +244 -0
  29. package/dist/runtime/query/auto-review/classifier.js.map +1 -0
  30. package/dist/runtime/query/auto-review/index.d.ts +7 -0
  31. package/dist/runtime/query/auto-review/index.d.ts.map +1 -0
  32. package/dist/runtime/query/auto-review/index.js +4 -0
  33. package/dist/runtime/query/auto-review/index.js.map +1 -0
  34. package/dist/runtime/query/auto-review/prompt.d.ts +20 -0
  35. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -0
  36. package/dist/runtime/query/auto-review/prompt.js +89 -0
  37. package/dist/runtime/query/auto-review/prompt.js.map +1 -0
  38. package/dist/runtime/query/auto-review/rules.d.ts +49 -0
  39. package/dist/runtime/query/auto-review/rules.d.ts.map +1 -0
  40. package/dist/runtime/query/auto-review/rules.js +81 -0
  41. package/dist/runtime/query/auto-review/rules.js.map +1 -0
  42. package/dist/runtime/query/auto-review/transcript.d.ts +59 -0
  43. package/dist/runtime/query/auto-review/transcript.d.ts.map +1 -0
  44. package/dist/runtime/query/auto-review/transcript.js +116 -0
  45. package/dist/runtime/query/auto-review/transcript.js.map +1 -0
  46. package/dist/runtime/query/auto-review/verdict.d.ts +21 -0
  47. package/dist/runtime/query/auto-review/verdict.d.ts.map +1 -0
  48. package/dist/runtime/query/auto-review/verdict.js +45 -0
  49. package/dist/runtime/query/auto-review/verdict.js.map +1 -0
  50. package/dist/runtime/query/executor.d.ts +7 -0
  51. package/dist/runtime/query/executor.d.ts.map +1 -1
  52. package/dist/runtime/query/executor.js +26 -2
  53. package/dist/runtime/query/executor.js.map +1 -1
  54. package/dist/runtime/query/index.js +2 -2
  55. package/dist/runtime/query/index.js.map +1 -1
  56. package/dist/runtime/query/iteration/phases/context.d.ts +0 -9
  57. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  58. package/dist/runtime/query/iteration/phases/context.js +11 -2
  59. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  60. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/phases/tool-review.js +39 -0
  62. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  63. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  64. package/dist/runtime/query/path-escalation.js +43 -10
  65. package/dist/runtime/query/path-escalation.js.map +1 -1
  66. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  67. package/dist/runtime/query/resume-pending.js +14 -0
  68. package/dist/runtime/query/resume-pending.js.map +1 -1
  69. package/dist/runtime/query/review-policy.d.ts +88 -1
  70. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  71. package/dist/runtime/query/review-policy.js +504 -11
  72. package/dist/runtime/query/review-policy.js.map +1 -1
  73. package/dist/tools/builtins/bash.d.ts.map +1 -1
  74. package/dist/tools/builtins/bash.js +2 -0
  75. package/dist/tools/builtins/bash.js.map +1 -1
  76. package/dist/tools/builtins/classifier-input.d.ts +9 -0
  77. package/dist/tools/builtins/classifier-input.d.ts.map +1 -0
  78. package/dist/tools/builtins/classifier-input.js +20 -0
  79. package/dist/tools/builtins/classifier-input.js.map +1 -0
  80. package/dist/tools/builtins/code-analysis.d.ts +2 -0
  81. package/dist/tools/builtins/code-analysis.d.ts.map +1 -1
  82. package/dist/tools/builtins/code-analysis.js +196 -119
  83. package/dist/tools/builtins/code-analysis.js.map +1 -1
  84. package/dist/tools/builtins/computer-use.d.ts +10 -10
  85. package/dist/tools/builtins/edit.d.ts.map +1 -1
  86. package/dist/tools/builtins/edit.js +12 -0
  87. package/dist/tools/builtins/edit.js.map +1 -1
  88. package/dist/tools/builtins/monitor.d.ts.map +1 -1
  89. package/dist/tools/builtins/monitor.js +3 -0
  90. package/dist/tools/builtins/monitor.js.map +1 -1
  91. package/dist/tools/builtins/web.d.ts.map +1 -1
  92. package/dist/tools/builtins/web.js +3 -0
  93. package/dist/tools/builtins/web.js.map +1 -1
  94. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  95. package/dist/tools/builtins/write-file.js +6 -0
  96. package/dist/tools/builtins/write-file.js.map +1 -1
  97. package/dist/tools/defineTool.d.ts +2 -0
  98. package/dist/tools/defineTool.d.ts.map +1 -1
  99. package/dist/tools/defineTool.js +1 -0
  100. package/dist/tools/defineTool.js.map +1 -1
  101. package/dist/types/acp/index.d.ts +7 -0
  102. package/dist/types/acp/index.d.ts.map +1 -1
  103. package/dist/types/code-analysis/index.d.ts +54 -3
  104. package/dist/types/code-analysis/index.d.ts.map +1 -1
  105. package/dist/types/hitl/index.d.ts +45 -1
  106. package/dist/types/hitl/index.d.ts.map +1 -1
  107. package/dist/types/hitl/index.js.map +1 -1
  108. package/dist/types/tool/index.d.ts +16 -0
  109. package/dist/types/tool/index.d.ts.map +1 -1
  110. package/dist/types/tool/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/authorization/gate.ts +35 -0
  113. package/src/authorization/removal.ts +12 -6
  114. package/src/authorization/rules.ts +27 -2
  115. package/src/authorization/shell-paths.ts +264 -51
  116. package/src/bridge/acp/server.ts +20 -0
  117. package/src/public-runtime.ts +18 -0
  118. package/src/public-types.ts +11 -0
  119. package/src/runtime/query/auto-review/classifier.ts +404 -0
  120. package/src/runtime/query/auto-review/index.ts +23 -0
  121. package/src/runtime/query/auto-review/prompt.ts +97 -0
  122. package/src/runtime/query/auto-review/rules.ts +106 -0
  123. package/src/runtime/query/auto-review/transcript.ts +159 -0
  124. package/src/runtime/query/auto-review/verdict.ts +50 -0
  125. package/src/runtime/query/executor.ts +29 -2
  126. package/src/runtime/query/index.ts +2 -2
  127. package/src/runtime/query/iteration/phases/context.ts +17 -3
  128. package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
  129. package/src/runtime/query/path-escalation.ts +44 -9
  130. package/src/runtime/query/resume-pending.ts +14 -0
  131. package/src/runtime/query/review-policy.ts +591 -14
  132. package/src/tools/builtins/bash.ts +3 -0
  133. package/src/tools/builtins/classifier-input.ts +19 -0
  134. package/src/tools/builtins/code-analysis.ts +211 -119
  135. package/src/tools/builtins/edit.ts +13 -0
  136. package/src/tools/builtins/monitor.ts +4 -0
  137. package/src/tools/builtins/web.ts +3 -0
  138. package/src/tools/builtins/write-file.ts +6 -0
  139. package/src/tools/defineTool.ts +3 -0
  140. package/src/types/acp/index.ts +7 -0
  141. package/src/types/code-analysis/index.ts +51 -3
  142. package/src/types/hitl/index.ts +49 -1
  143. package/src/types/tool/index.ts +17 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared helpers for `ToolDefinition.classifierInput`: what the auto-mode
3
+ * classifier is shown of a call to a built-in tool.
4
+ */
5
+
6
+ /** Cuts `text` to `limit` characters, saying how much was left out. */
7
+ export function clip(text: string, limit: number): string {
8
+ if (text.length <= limit) return text
9
+ return `${text.slice(0, limit)}... [${text.length - limit} more characters]`
10
+ }
11
+
12
+ /** The characters a change adds and removes, in a form short enough to read at a glance. */
13
+ export function describeChange(removed: string | undefined, added: string | undefined): string {
14
+ const parts: string[] = []
15
+ if (removed !== undefined && removed !== '')
16
+ parts.push(`removes ${JSON.stringify(clip(removed, 200))}`)
17
+ if (added !== undefined && added !== '') parts.push(`adds ${JSON.stringify(clip(added, 300))}`)
18
+ return parts.length > 0 ? parts.join(', ') : 'no text change'
19
+ }
@@ -1,4 +1,5 @@
1
1
  import { realpath, stat } from 'node:fs/promises'
2
+ import { isAbsolute, relative, sep } from 'node:path'
2
3
  import { z } from 'zod'
3
4
 
4
5
  import type {
@@ -112,7 +113,14 @@ const functionSourceOp = z.object({
112
113
  ),
113
114
  function_id: z.number().int().min(0).optional(),
114
115
  path: z.string().optional(),
115
- name: z.string().min(1).optional(),
116
+ name: z.string().min(1).optional().describe('`name` or `Class.method`.'),
117
+ line: z
118
+ .number()
119
+ .int()
120
+ .min(1)
121
+ .optional()
122
+ .describe('With `column`: cut a long function around this position, not at its end.'),
123
+ column: z.number().int().min(1).optional(),
116
124
  max_chars: maxChars,
117
125
  ...paging,
118
126
  })
@@ -123,7 +131,10 @@ const callersOp = z.object({
123
131
  .describe(
124
132
  'Call sites of a callee TEXT (not scope-aware), each with its caller. Per file by default.',
125
133
  ),
126
- name: z.string().min(1).describe('Callee text: `foo`, `obj.foo`, `this.foo`.'),
134
+ name: z
135
+ .string()
136
+ .min(1)
137
+ .describe('Callee text: `foo`, `obj.foo`, `this.foo`; or a method as `Class.method`.'),
127
138
  scope: z
128
139
  .enum(['file', 'workspace'])
129
140
  .optional()
@@ -207,122 +218,176 @@ export const CodeAnalysisTool = defineTool({
207
218
  }
208
219
  const root = owner.root
209
220
 
210
- switch (input.operation) {
211
- case 'index': {
212
- const result = await provider.index({
213
- root,
214
- // A narrower path indexes just that part of the root, which is
215
- // how a root too big to index whole is covered in steps.
216
- ...(target !== root ? { scope: target } : {}),
217
- ...(input.force ? { force: true } : {}),
218
- ...signalOf(context),
219
- })
220
- return render(result, (value) => {
221
- const f = value.files
222
- const lines = [
223
- `Indexed ${value.root}: ${f.indexed} files (${f.parsed} parsed, ${f.unchanged} unchanged, ${f.removed} removed, ${f.skipped} skipped, ${f.withParseErrors} with parse errors) in ${value.durationMs} ms.`,
224
- `${value.functions} functions, ${value.strings} string literals, ${value.calls} calls. ${value.persisted ? 'Cached on disk.' : 'Held in memory only; it is gone when this process exits.'}`,
225
- ]
226
- if (value.truncated) lines.push(value.truncated)
227
- const reasons = Object.entries(value.skippedReasons)
228
- if (reasons.length > 0) {
229
- lines.push(`Skipped: ${reasons.map(([why, n]) => `${n} ${why}`).join(', ')}.`)
230
- }
231
- return lines
232
- })
233
- }
234
- case 'find_literal': {
235
- const result = await provider.findLiteral({
236
- root,
237
- text: input.text,
238
- path: target,
239
- ...(input.case_sensitive === false ? { caseSensitive: false } : {}),
240
- ...pageArgs(input),
241
- ...signalOf(context),
242
- })
243
- return renderPage(result, (hit) => {
244
- const where = `${hit.path}:${hit.line}:${hit.column}`
245
- const shown = JSON.stringify(hit.value) + (hit.valueTruncated ? ' (cut)' : '')
246
- const inside = hit.function ? ` in ${describeFunction(hit.function)}` : ' at top level'
247
- return `${where} ${hit.kind} ${shown}${inside}${staleMark(hit.stale)}`
248
- })
249
- }
250
- case 'enclosing_function': {
251
- const result = await provider.enclosingFunction({
252
- root,
253
- path: target,
254
- line: input.line,
255
- ...(input.column !== undefined ? { column: input.column } : {}),
256
- maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
257
- ...signalOf(context),
258
- })
259
- return render(result, (value) => {
260
- if (!value.function) {
261
- return [`${target}:${input.line}:${input.column ?? 1} is not inside any function.`]
221
+ const outcome = await (async (): Promise<ToolResult> => {
222
+ switch (input.operation) {
223
+ case 'index': {
224
+ const result = await provider.index({
225
+ root,
226
+ // A narrower path indexes just that part of the root, which is
227
+ // how a root too big to index whole is covered in steps.
228
+ ...(target !== root ? { scope: target } : {}),
229
+ ...(input.force ? { force: true } : {}),
230
+ ...signalOf(context),
231
+ })
232
+ return render(result, (value) => {
233
+ const f = value.files
234
+ const lines = [
235
+ `Indexed ${value.root}: ${f.indexed} files (${f.parsed} parsed, ${f.unchanged} unchanged, ${f.removed} removed, ${f.skipped} skipped, ${f.withParseErrors} with parse errors) in ${value.durationMs} ms.`,
236
+ `${value.functions} functions, ${value.strings} string literals, ${value.calls} calls. ${value.persisted ? 'Cached on disk.' : 'Held in memory only; it is gone when this process exits.'}`,
237
+ ]
238
+ if (value.truncated) lines.push(value.truncated)
239
+ const reasons = Object.entries(value.skippedReasons)
240
+ if (reasons.length > 0) {
241
+ lines.push(`Skipped: ${reasons.map(([why, n]) => `${n} ${why}`).join(', ')}.`)
242
+ }
243
+ return lines
244
+ })
245
+ }
246
+ case 'find_literal': {
247
+ const result = await provider.findLiteral({
248
+ root,
249
+ text: input.text,
250
+ path: target,
251
+ ...(input.case_sensitive === false ? { caseSensitive: false } : {}),
252
+ ...pageArgs(input),
253
+ ...signalOf(context),
254
+ })
255
+ return renderPage(
256
+ result,
257
+ (hit) => {
258
+ const shown = JSON.stringify(hit.value) + (hit.valueTruncated ? ' (cut)' : '')
259
+ const kind = hit.kind === 'string' ? '' : ` (${hit.kind})`
260
+ const inside = hit.function ? ` in ${shortFunction(hit.function)}` : ' at top level'
261
+ return `${hit.line}:${hit.column} ${shown}${kind}${inside}${staleMark(hit.stale)}`
262
+ },
263
+ (hit) => relativeTo(root, hit.path),
264
+ )
265
+ }
266
+ case 'enclosing_function': {
267
+ const result = await provider.enclosingFunction({
268
+ root,
269
+ path: target,
270
+ line: input.line,
271
+ ...(input.column !== undefined ? { column: input.column } : {}),
272
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
273
+ ...signalOf(context),
274
+ })
275
+ return render(result, (value) => {
276
+ if (!value.function) {
277
+ const place = `${relativeTo(root, target)}:${input.line}:${input.column ?? 1}`
278
+ return [
279
+ value.inClass !== undefined
280
+ ? `${place} is in the body of class ${value.inClass}, outside every method; not inside any function.`
281
+ : `${place} is not inside any function.`,
282
+ ]
283
+ }
284
+ const lines = [describeFunction(value.function, root)]
285
+ if (value.parents.length > 0) {
286
+ lines.push(`parents: ${value.parents.map(shortFunction).join(' < ')}`)
287
+ }
288
+ if (value.source) lines.push(...sourceLines(value.source))
289
+ return lines
290
+ })
291
+ }
292
+ case 'function_source': {
293
+ if (input.function_id === undefined && !(input.path !== undefined && input.name)) {
294
+ return fail('function_source needs `function_id`, or `path` together with `name`.')
262
295
  }
263
- const lines = [`${describeFunction(value.function)}`]
264
- if (value.parents.length > 0) {
265
- lines.push(`parents: ${value.parents.map(describeFunction).join(' < ')}`)
296
+ const result = await provider.functionSource({
297
+ root,
298
+ ...(input.function_id !== undefined ? { id: input.function_id } : {}),
299
+ ...(input.name ? { name: input.name, path: target } : {}),
300
+ ...(input.line !== undefined
301
+ ? {
302
+ around: {
303
+ line: input.line,
304
+ ...(input.column !== undefined ? { column: input.column } : {}),
305
+ },
306
+ }
307
+ : {}),
308
+ maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
309
+ ...pageArgs(input),
310
+ ...signalOf(context),
311
+ })
312
+ return renderPage(result, (item) =>
313
+ [
314
+ describeFunction(item.function, root),
315
+ item.parents.length > 0
316
+ ? `parents: ${item.parents.map(shortFunction).join(' < ')}`
317
+ : undefined,
318
+ ...sourceLines(item.source),
319
+ ]
320
+ .filter((l): l is string => l !== undefined)
321
+ .join('\n'),
322
+ )
323
+ }
324
+ case 'callers_of': {
325
+ const scope = input.scope ?? 'file'
326
+ if (scope === 'file' && input.path === undefined) {
327
+ return fail(
328
+ 'callers_of looks in one file by default, so it needs `path`. Pass `scope: "workspace"` to search the whole index; it matches by name only and will mix unrelated functions that share one.',
329
+ )
266
330
  }
267
- if (value.source) lines.push(...sourceLines(value.source))
268
- return lines
269
- })
270
- }
271
- case 'function_source': {
272
- if (input.function_id === undefined && !(input.path !== undefined && input.name)) {
273
- return fail('function_source needs `function_id`, or `path` together with `name`.')
331
+ const result = await provider.callersOf({
332
+ root,
333
+ name: input.name,
334
+ scope,
335
+ ...(input.path !== undefined ? { path: target } : {}),
336
+ ...(input.match ? { match: input.match } : {}),
337
+ ...pageArgs(input),
338
+ ...signalOf(context),
339
+ })
340
+ return renderPage(
341
+ result,
342
+ (site) => {
343
+ const inside = site.caller ? ` in ${shortFunction(site.caller)}` : ' at top level'
344
+ const how = site.kind === 'new' ? 'new ' : ''
345
+ const tag =
346
+ site.kind === 'call-via'
347
+ ? ' [call/apply/bind]'
348
+ : site.kind === 'tagged'
349
+ ? ' [tagged template]'
350
+ : ''
351
+ const label = site.resolution === 'text-only' ? ' [text-only]' : ''
352
+ return `${site.line}:${site.column} ${how}${site.callee}(${site.argumentCount} arg${site.argumentCount === 1 ? '' : 's'})${tag}${label}${inside}${staleMark(site.stale)}`
353
+ },
354
+ (site) => relativeTo(root, site.path),
355
+ )
274
356
  }
275
- const result = await provider.functionSource({
276
- root,
277
- ...(input.function_id !== undefined ? { id: input.function_id } : {}),
278
- ...(input.name ? { name: input.name, path: target } : {}),
279
- maxChars: input.max_chars ?? DEFAULT_MAX_CHARS,
280
- ...pageArgs(input),
281
- ...signalOf(context),
282
- })
283
- return renderPage(result, (item) =>
284
- [
285
- describeFunction(item.function),
286
- item.parents.length > 0
287
- ? `parents: ${item.parents.map(describeFunction).join(' < ')}`
288
- : undefined,
289
- ...sourceLines(item.source),
290
- ]
291
- .filter((l): l is string => l !== undefined)
292
- .join('\n'),
293
- )
294
- }
295
- case 'callers_of': {
296
- const scope = input.scope ?? 'file'
297
- if (scope === 'file' && input.path === undefined) {
298
- return fail(
299
- 'callers_of looks in one file by default, so it needs `path`. Pass `scope: "workspace"` to search the whole index; it matches by name only and will mix unrelated functions that share one.',
357
+ case 'imports': {
358
+ const result = await provider.imports({
359
+ root,
360
+ path: target,
361
+ ...pageArgs(input),
362
+ ...signalOf(context),
363
+ })
364
+ return render(
365
+ result,
366
+ (page) =>
367
+ page.items.length === 0
368
+ ? [page.total === 0 ? 'No results.' : 'No results on this page.']
369
+ : [
370
+ relativeTo(root, target),
371
+ ...page.items.map((edge) => ` ${describeEdge(edge)}`),
372
+ page.nextCursor !== undefined
373
+ ? `-- ${page.items.length} of ${page.total} shown. next cursor: ${page.nextCursor}`
374
+ : `-- ${page.items.length} of ${page.total} shown. last page.`,
375
+ ],
376
+ (page) => ({
377
+ total: page.total,
378
+ count: page.items.length,
379
+ items: page.items,
380
+ ...(page.nextCursor !== undefined ? { nextCursor: page.nextCursor } : {}),
381
+ }),
300
382
  )
301
383
  }
302
- const result = await provider.callersOf({
303
- root,
304
- name: input.name,
305
- scope,
306
- ...(input.path !== undefined ? { path: target } : {}),
307
- ...(input.match ? { match: input.match } : {}),
308
- ...pageArgs(input),
309
- ...signalOf(context),
310
- })
311
- return renderPage(result, (site) => {
312
- const inside = site.caller ? ` in ${describeFunction(site.caller)}` : ' at top level'
313
- return `${site.path}:${site.line}:${site.column} ${site.kind === 'new' ? 'new ' : ''}${site.callee}(${site.argumentCount} arg${site.argumentCount === 1 ? '' : 's'})${inside}${staleMark(site.stale)}`
314
- })
315
- }
316
- case 'imports': {
317
- const result = await provider.imports({
318
- root,
319
- path: target,
320
- ...pageArgs(input),
321
- ...signalOf(context),
322
- })
323
- return renderPage(result, (edge) => describeEdge(edge, target))
324
384
  }
325
- }
385
+ })()
386
+ // With one root, a relative path is unambiguous. With several, the same
387
+ // `src/a.ts` could be in any of them, so the reader is told which this is.
388
+ return roots.length > 1 && outcome.success
389
+ ? { ...outcome, output: `paths are relative to ${root}\n${outcome.output}` }
390
+ : outcome
326
391
  },
327
392
  })
328
393
 
@@ -381,13 +446,27 @@ async function owningRoot(roots: readonly string[], target: string): Promise<Own
381
446
  return sawFile ? { kind: 'file' } : { kind: 'none' }
382
447
  }
383
448
 
384
- function describeFunction(fn: AnalysisFunction): string {
385
- const lines = fn.endLine === fn.line ? `line ${fn.line}` : `lines ${fn.line}-${fn.endLine}`
386
- return `${fn.name ?? '(anonymous)'} [${fn.kind}, id ${fn.id}] ${fn.path}:${fn.line}:${fn.column} (${lines})`
449
+ /** `path` relative to the root (POSIX separators), or `path` itself when it is not under it. */
450
+ function relativeTo(root: string, path: string): string {
451
+ const rel = relative(root, path)
452
+ // `..` as a whole segment is outside the root; `..x` is a directory named so.
453
+ if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return path
454
+ return sep === '/' ? rel : rel.split(sep).join('/')
387
455
  }
388
456
 
389
- function describeEdge(edge: AnalysisModuleEdge, path: string): string {
390
- const where = `${path}:${edge.line}:${edge.column}`
457
+ /** `Svc.init#12 L10-40`: enough to cite and to ask for by id, no repeated path or kind. */
458
+ function shortFunction(fn: AnalysisFunction): string {
459
+ const lines = fn.endLine === fn.line ? `L${fn.line}` : `L${fn.line}-${fn.endLine}`
460
+ return `${fn.qualifiedName ?? fn.name ?? '(anonymous)'}#${fn.id} ${lines}`
461
+ }
462
+
463
+ /** The full line a single function gets: kind and place included. */
464
+ function describeFunction(fn: AnalysisFunction, root: string): string {
465
+ return `${shortFunction(fn)} [${fn.kind}] ${relativeTo(root, fn.path)}:${fn.line}:${fn.column}`
466
+ }
467
+
468
+ function describeEdge(edge: AnalysisModuleEdge): string {
469
+ const where = `${edge.line}:${edge.column}`
391
470
  if (edge.direction === 'import') {
392
471
  const names = edge.names.length > 0 ? ` { ${edge.names.join(', ')} }` : ''
393
472
  return `${where} import (${edge.kind})${names} from ${JSON.stringify(edge.source)}`
@@ -400,7 +479,9 @@ function sourceLines(slice: AnalysisSourceSlice): string[] {
400
479
  const lines = ['--- source ---', slice.text]
401
480
  if (slice.truncated) {
402
481
  lines.push(
403
- `--- cut: showing ${slice.text.length} of ${slice.chars} characters; raise max_chars to see more ---`,
482
+ slice.window
483
+ ? `--- cut: showing ${slice.text.length} of ${slice.chars} characters, the function's first ${slice.window.headerChars} and the part around the position; raise max_chars to see more ---`
484
+ : `--- cut: showing ${slice.text.length} of ${slice.chars} characters; raise max_chars to see more ---`,
404
485
  )
405
486
  }
406
487
  return lines
@@ -449,6 +530,8 @@ function render<T>(
449
530
  function renderPage<T>(
450
531
  result: CodeAnalysisResult<AnalysisPage<T>>,
451
532
  line: (item: T) => string,
533
+ /** Groups consecutive items of one file under a single heading. */
534
+ fileOf?: (item: T) => string,
452
535
  ): ToolResult {
453
536
  return render(
454
537
  result,
@@ -456,7 +539,16 @@ function renderPage<T>(
456
539
  if (page.items.length === 0) {
457
540
  return [page.total === 0 ? 'No results.' : 'No results on this page.']
458
541
  }
459
- const lines = page.items.map(line)
542
+ const lines: string[] = []
543
+ let current: string | undefined
544
+ for (const item of page.items) {
545
+ const file = fileOf?.(item)
546
+ if (file !== undefined && file !== current) {
547
+ lines.push(file)
548
+ current = file
549
+ }
550
+ lines.push(file === undefined ? line(item) : ` ${line(item)}`)
551
+ }
460
552
  lines.push(
461
553
  page.nextCursor !== undefined
462
554
  ? `-- ${page.items.length} of ${page.total} shown. next cursor: ${page.nextCursor}`
@@ -5,6 +5,7 @@ import type { ToolContext, ToolResult } from '../../types/tool/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
  import { resolveWithinAnyReal, toolRoots } from '../paths.js'
7
7
  import { atomicWriteFile } from './atomic-write-file.js'
8
+ import { clip, describeChange } from './classifier-input.js'
8
9
  import { fingerprintContent, staleFileError, unreadFileError } from './content-fingerprint.js'
9
10
  import { type NormalizedEditInput, applyEdit, normalizeEditInput } from './edit-apply.js'
10
11
  import { withFileMutationLock } from './file-mutation-lock.js'
@@ -393,6 +394,18 @@ export const EditTool = defineTool({
393
394
  // Declared so a path outside the turn's roots can be reviewed before the
394
395
  // call runs, rather than refused after. See `ToolDefinition.pathArgument`.
395
396
  pathArgument: 'path',
397
+ // The classifier judges which file changes and in what way, in a line.
398
+ classifierInput: (input: EditInput) => {
399
+ if (input.edits !== undefined) {
400
+ const shown = input.edits.slice(0, 5).map((e) => describeChange(e.old_string, e.new_string))
401
+ const more =
402
+ input.edits.length > shown.length ? `; ${input.edits.length - shown.length} more` : ''
403
+ return clip(`${input.path}: ${input.edits.length} edits: ${shown.join('; ')}${more}`, 1500)
404
+ }
405
+ const removed = input.old_string ?? input.oldStr
406
+ const added = input.new_string ?? input.newStr
407
+ return clip(`${input.path}: ${describeChange(removed, added)}`, 1500)
408
+ },
396
409
  permissions: ['file_write'],
397
410
  readOnly: false,
398
411
  destructive: false,
@@ -178,6 +178,10 @@ export function createMonitorTool(options: MonitorToolOptions = {}) {
178
178
  // has no `command`, so a rule written for commands has nothing to match
179
179
  // and cannot allow it; `requiresApproval` below is what asks.
180
180
  commandArgument: 'command',
181
+ classifierInput: (input: MonitorInput) =>
182
+ input.command !== undefined
183
+ ? `[long-running watch] ${input.command}`
184
+ : `[long-running WebSocket watch] ${JSON.stringify(input.ws)}`,
181
185
  commandDialect: bashToolDialect,
182
186
  // `network_access` too: a WebSocket call reaches the network. The gate
183
187
  // reads a tool's `category`, not this list, to route a call, so declaring
@@ -11,6 +11,7 @@ import {
11
11
  import { WebFetchRefusedError } from '../../connector/web/types.js'
12
12
  import type { ToolContext } from '../../types/tool/index.js'
13
13
  import { defineTool } from '../defineTool.js'
14
+ import { clip } from './classifier-input.js'
14
15
 
15
16
  /**
16
17
  * The two web tools, and the reason they are one file.
@@ -151,6 +152,8 @@ export const WebFetchTool = defineTool({
151
152
  // preset — handing outbound reach to runs whose sandbox was never asked
152
153
  // to confine the network.
153
154
  category: 'network',
155
+ // The classifier judges the address and what is asked of it.
156
+ classifierInput: (input) => `${clip(input.url, 1000)}: ${clip(input.prompt, 300)}`,
154
157
  permissions: ['network_access'],
155
158
  // Reads a page and changes nothing HERE. That is not the same as
156
159
  // harmless — a GET can still act on a remote system — which is why the
@@ -6,6 +6,7 @@ import type { ToolFacts } from '../../types/tool/presentation.js'
6
6
  import { defineTool } from '../defineTool.js'
7
7
  import { resolveWithinAnyReal, toolRoots } from '../paths.js'
8
8
  import { atomicWriteFile } from './atomic-write-file.js'
9
+ import { clip } from './classifier-input.js'
9
10
  import { fingerprintContent, staleFileError } from './content-fingerprint.js'
10
11
  import { withFileMutationLock } from './file-mutation-lock.js'
11
12
  import { fileWriteResult } from './file-write-result.js'
@@ -93,6 +94,11 @@ export const WriteFileTool = defineTool({
93
94
  // Declared so a path outside the turn's roots can be reviewed before the
94
95
  // call runs, rather than refused after. See `ToolDefinition.pathArgument`.
95
96
  pathArgument: 'path',
97
+ // The classifier judges where a file is written and how much, not the body.
98
+ classifierInput: (input: WriteInput) => {
99
+ const body = input.content ?? input.newStr ?? ''
100
+ return `${input.path}: writes ${body.length} characters, starting ${JSON.stringify(clip(body, 300))}`
101
+ },
96
102
  permissions: ['file_write'],
97
103
  readOnly: false,
98
104
  destructive: true,
@@ -33,6 +33,8 @@ export interface DefineToolOptions<S extends z.ZodType> {
33
33
  capturesScreen?: boolean | ((input: z.infer<S>) => boolean)
34
34
  /** Whether this exact call always needs a person's approval; see {@link ToolDefinition.requiresApproval}. */
35
35
  requiresApproval?: boolean | ((input: z.infer<S>) => boolean)
36
+ /** What the auto-mode classifier is shown of this call; see {@link ToolDefinition.classifierInput}. */
37
+ classifierInput?: (input: z.infer<S>) => string
36
38
  /** Free-form filtering data, never sent to the model; see {@link ToolDefinition.metadata}. */
37
39
  metadata?: Readonly<Record<string, unknown>>
38
40
  /** Batch ordering boundary; see {@link ToolDefinition.executionBarrier}. */
@@ -186,6 +188,7 @@ export function defineTool<S extends z.ZodType>(
186
188
  : () => options.requiresApproval as boolean,
187
189
  }
188
190
  : {}),
191
+ ...(options.classifierInput !== undefined ? { classifierInput: options.classifierInput } : {}),
189
192
  ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
190
193
 
191
194
  async execute(input: TInput, context: ToolContext): Promise<ToolResult> {
@@ -43,6 +43,13 @@ export interface AcpInitializeResult {
43
43
  readonly promptAttachments?: boolean
44
44
  /** True only when this host applies captured effort and review settings per prompt. */
45
45
  readonly promptOptions?: boolean
46
+ /**
47
+ * The review modes beyond the original five (`prompt`, `plan`,
48
+ * `accept-edits`, `auto`, `strict`) that this host applies per prompt:
49
+ * `bypass` and `classify`, when it supports them. Absent: it accepts only
50
+ * the original five, and refuses the others before a turn starts.
51
+ */
52
+ readonly promptPermissionModes?: readonly ('bypass' | 'classify')[]
46
53
  /** Optional, explicitly installed Namzu host methods; core ACP peers can ignore them. */
47
54
  readonly extensions?: readonly string[]
48
55
  readonly protocolVersion: number
@@ -119,6 +119,14 @@ export interface AnalysisFunction {
119
119
  readonly id: number
120
120
  /** `null` for an anonymous function with nothing to infer a name from. */
121
121
  readonly name: string | null
122
+ /**
123
+ * `name` qualified by what holds it, when it is a member: `Svc.init` for a
124
+ * method of the class `Svc` (a class expression assigned to a variable takes
125
+ * the variable's name), `obj.run` for a method of an object literal assigned
126
+ * to `obj`. Absent for a function that is not a member. `functionSource`
127
+ * accepts either spelling.
128
+ */
129
+ readonly qualifiedName?: string
122
130
  readonly kind: AnalysisFunctionKind
123
131
  /** Absolute path. */
124
132
  readonly path: string
@@ -136,6 +144,17 @@ export interface AnalysisSourceSlice {
136
144
  readonly chars: number
137
145
  /** `true` when `text` is shorter than `chars` because of the size cap. */
138
146
  readonly truncated: boolean
147
+ /**
148
+ * Present when the cut was made around a position rather than at the end:
149
+ * `text` is the function's first `headerChars` characters, a marker for
150
+ * what was left out, then the file's own characters `from` to `to`, which
151
+ * contain the position asked about. `from` and `to` are offsets into the file.
152
+ */
153
+ readonly window?: {
154
+ readonly headerChars: number
155
+ readonly from: number
156
+ readonly to: number
157
+ }
139
158
  }
140
159
 
141
160
  export interface IndexRequest extends AnalysisCallOptions {
@@ -222,6 +241,11 @@ export interface EnclosingFunctionRequest extends AnalysisCallOptions {
222
241
  export interface AnalysisEnclosingFunction {
223
242
  /** `null` when the position is not inside any function — a real answer. */
224
243
  readonly function: AnalysisFunction | null
244
+ /**
245
+ * When `function` is `null` because the position is in a class body outside
246
+ * every method (a field initializer, a `static {}` block): that class.
247
+ */
248
+ readonly inClass?: string
225
249
  /** The function's parents, innermost first. */
226
250
  readonly parents: readonly AnalysisFunction[]
227
251
  readonly source?: AnalysisSourceSlice
@@ -235,6 +259,11 @@ export interface FunctionSourceRequest extends AnalysisPaging {
235
259
  readonly path?: string
236
260
  readonly name?: string
237
261
  readonly maxChars?: number
262
+ /**
263
+ * A position in the function. When the source is longer than `maxChars`, the
264
+ * slice is cut around it instead of at the end, and always contains it.
265
+ */
266
+ readonly around?: { readonly line: number; readonly column?: number }
238
267
  }
239
268
 
240
269
  export interface AnalysisFunctionSource {
@@ -245,7 +274,11 @@ export interface AnalysisFunctionSource {
245
274
 
246
275
  export interface CallersOfRequest extends AnalysisPaging {
247
276
  readonly root: string
248
- /** The callee text: `foo`, `obj.foo`, `this.foo`. */
277
+ /**
278
+ * The callee text: `foo`, `obj.foo`, `this.foo`. A method's qualified name
279
+ * (`Svc.init`, as `functionSource` shows it) finds `this.init()` inside
280
+ * `Svc` as a resolved call, and every other `<expr>.init()` as `text-only`.
281
+ */
249
282
  readonly name: string
250
283
  /**
251
284
  * `file` looks in `path` only and is the default, because a callee matched
@@ -266,8 +299,19 @@ export interface AnalysisCallSite {
266
299
  readonly line: number
267
300
  readonly column: number
268
301
  readonly callee: string
269
- readonly kind: 'call' | 'new'
302
+ /**
303
+ * `call-via` is `f.call(…)`, `f.apply(…)` or `f.bind(…)` of the callee (a
304
+ * use, not necessarily a call); `tagged` is a tagged template.
305
+ */
306
+ readonly kind: 'call' | 'new' | 'call-via' | 'tagged'
270
307
  readonly argumentCount: number
308
+ /**
309
+ * Only when the request named a method by its qualified name: `resolved`
310
+ * for `this.m()` inside the class that declares it and for the qualified
311
+ * text itself, `text-only` for any other receiver, which may be an
312
+ * unrelated method of the same name.
313
+ */
314
+ readonly resolution?: 'resolved' | 'text-only'
271
315
  /** The function the call sits in; absent at the top level of a file. */
272
316
  readonly caller?: AnalysisFunction
273
317
  /** See {@link AnalysisLiteralHit.stale}. */
@@ -290,7 +334,11 @@ export type AnalysisModuleEdge =
290
334
  }
291
335
  | {
292
336
  readonly direction: 'export'
293
- readonly kind: 'named' | 'default' | 'reexport' | 'star'
337
+ /**
338
+ * `type` is a type-only export, `commonjs` is `module.exports` or
339
+ * `exports.x`, `assign` is `export =`.
340
+ */
341
+ readonly kind: 'named' | 'default' | 'reexport' | 'star' | 'type' | 'commonjs' | 'assign'
294
342
  readonly name: string
295
343
  readonly source?: string
296
344
  readonly line: number