@namzu/sdk 52.4.0 → 52.6.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 (152) hide show
  1. package/CHANGELOG.md +33 -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-apply.d.ts.map +1 -1
  86. package/dist/tools/builtins/edit-apply.js +30 -12
  87. package/dist/tools/builtins/edit-apply.js.map +1 -1
  88. package/dist/tools/builtins/edit-match.d.ts +47 -0
  89. package/dist/tools/builtins/edit-match.d.ts.map +1 -0
  90. package/dist/tools/builtins/edit-match.js +262 -0
  91. package/dist/tools/builtins/edit-match.js.map +1 -0
  92. package/dist/tools/builtins/edit.d.ts.map +1 -1
  93. package/dist/tools/builtins/edit.js +12 -0
  94. package/dist/tools/builtins/edit.js.map +1 -1
  95. package/dist/tools/builtins/monitor.d.ts.map +1 -1
  96. package/dist/tools/builtins/monitor.js +3 -0
  97. package/dist/tools/builtins/monitor.js.map +1 -1
  98. package/dist/tools/builtins/web.d.ts.map +1 -1
  99. package/dist/tools/builtins/web.js +3 -0
  100. package/dist/tools/builtins/web.js.map +1 -1
  101. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  102. package/dist/tools/builtins/write-file.js +6 -0
  103. package/dist/tools/builtins/write-file.js.map +1 -1
  104. package/dist/tools/defineTool.d.ts +2 -0
  105. package/dist/tools/defineTool.d.ts.map +1 -1
  106. package/dist/tools/defineTool.js +1 -0
  107. package/dist/tools/defineTool.js.map +1 -1
  108. package/dist/types/acp/index.d.ts +7 -0
  109. package/dist/types/acp/index.d.ts.map +1 -1
  110. package/dist/types/code-analysis/index.d.ts +54 -3
  111. package/dist/types/code-analysis/index.d.ts.map +1 -1
  112. package/dist/types/hitl/index.d.ts +45 -1
  113. package/dist/types/hitl/index.d.ts.map +1 -1
  114. package/dist/types/hitl/index.js.map +1 -1
  115. package/dist/types/tool/index.d.ts +16 -0
  116. package/dist/types/tool/index.d.ts.map +1 -1
  117. package/dist/types/tool/index.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/authorization/gate.ts +35 -0
  120. package/src/authorization/removal.ts +12 -6
  121. package/src/authorization/rules.ts +27 -2
  122. package/src/authorization/shell-paths.ts +264 -51
  123. package/src/bridge/acp/server.ts +20 -0
  124. package/src/public-runtime.ts +18 -0
  125. package/src/public-types.ts +11 -0
  126. package/src/runtime/query/auto-review/classifier.ts +404 -0
  127. package/src/runtime/query/auto-review/index.ts +23 -0
  128. package/src/runtime/query/auto-review/prompt.ts +97 -0
  129. package/src/runtime/query/auto-review/rules.ts +106 -0
  130. package/src/runtime/query/auto-review/transcript.ts +159 -0
  131. package/src/runtime/query/auto-review/verdict.ts +50 -0
  132. package/src/runtime/query/executor.ts +29 -2
  133. package/src/runtime/query/index.ts +2 -2
  134. package/src/runtime/query/iteration/phases/context.ts +17 -3
  135. package/src/runtime/query/iteration/phases/tool-review.ts +41 -0
  136. package/src/runtime/query/path-escalation.ts +44 -9
  137. package/src/runtime/query/resume-pending.ts +14 -0
  138. package/src/runtime/query/review-policy.ts +591 -14
  139. package/src/tools/builtins/bash.ts +3 -0
  140. package/src/tools/builtins/classifier-input.ts +19 -0
  141. package/src/tools/builtins/code-analysis.ts +211 -119
  142. package/src/tools/builtins/edit-apply.ts +31 -12
  143. package/src/tools/builtins/edit-match.ts +274 -0
  144. package/src/tools/builtins/edit.ts +13 -0
  145. package/src/tools/builtins/monitor.ts +4 -0
  146. package/src/tools/builtins/web.ts +3 -0
  147. package/src/tools/builtins/write-file.ts +6 -0
  148. package/src/tools/defineTool.ts +3 -0
  149. package/src/types/acp/index.ts +7 -0
  150. package/src/types/code-analysis/index.ts +51 -3
  151. package/src/types/hitl/index.ts +49 -1
  152. package/src/types/tool/index.ts +17 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Flexible `old_string` matching for the edit tool.
3
+ *
4
+ * When an exact match of `old_string` against the file fails, a handful of
5
+ * near-misses are not the model being wrong about the text, only about one
6
+ * invisible or interchangeable detail: a curly quote where the file has a
7
+ * straight one, or a `\uXXXX` escape where the file has the character itself
8
+ * (or the reverse). This module recovers the real span the model meant, and
9
+ * carries the same transformation across to `new_string` so the replacement
10
+ * stays consistent with what it replaced.
11
+ *
12
+ * The strategies and their order are a faithful port of the reference client's
13
+ * file-edit resolver (the `lje`/`b`/`_`/`L`/`k`/`R`/`C`/`cDs`/`ipt`/`H`/`Q`/`N`
14
+ * functions in its shipped bundle), so an edit that lands there lands here:
15
+ *
16
+ * 1. exact
17
+ * 2. curly vs straight quotes, both directions
18
+ * 3. `\uXXXX` escape in `old_string`, character in the file
19
+ * 4. character in `old_string`, `\uXXXX` escape in the file
20
+ *
21
+ * The reference also expands a map of its own prompt delimiters (`<fnr>` →
22
+ * `<function_results>`, `\n\nH:` → `\n\nHuman:`, …). That is deliberately NOT
23
+ * ported: those tokens are artifacts of that client's own transcript format,
24
+ * not a mismatch a Namzu model produces against a real source file.
25
+ *
26
+ * Pure by construction, like `edit-apply`: every function here is from strings
27
+ * to strings, with no filesystem access.
28
+ */
29
+
30
+ const LEFT_SINGLE_QUOTE = '‘'
31
+ const RIGHT_SINGLE_QUOTE = '’'
32
+ const LEFT_DOUBLE_QUOTE = '“'
33
+ const RIGHT_DOUBLE_QUOTE = '”'
34
+
35
+ /** `old_string` carries a `\uXXXX` escape the file may hold as a character. */
36
+ const HAS_UNICODE_ESCAPE = /\\u[0-9a-fA-F]{4}/
37
+ /** `old_string` carries a non-ASCII character the file may hold as an escape. */
38
+ const HAS_NON_ASCII = /[\u0080-￿]/
39
+
40
+ /** Curly quotes to straight. Length-preserving: one character for one. */
41
+ function straightenQuotes(text: string): string {
42
+ return text
43
+ .replaceAll(LEFT_SINGLE_QUOTE, "'")
44
+ .replaceAll(RIGHT_SINGLE_QUOTE, "'")
45
+ .replaceAll(LEFT_DOUBLE_QUOTE, '"')
46
+ .replaceAll(RIGHT_DOUBLE_QUOTE, '"')
47
+ }
48
+
49
+ /**
50
+ * Replace each `\uXXXX` escape with its character, leaving an escaped
51
+ * backslash (`\\`) untouched so `\\u0041` stays the five characters it is
52
+ * rather than becoming a stray `A`.
53
+ */
54
+ function decodeUnicodeEscapes(text: string): string {
55
+ return text.replace(/(\\\\)|\\u([0-9a-fA-F]{4})/g, (whole, backslashes, hex) =>
56
+ backslashes !== undefined ? whole : String.fromCharCode(Number.parseInt(hex, 16)),
57
+ )
58
+ }
59
+
60
+ /** The length `text` would have if every non-ASCII character were a `\uXXXX`. */
61
+ function escapedLength(text: string): number {
62
+ let nonAscii = 0
63
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) >= 128) nonAscii++
64
+ return text.length + 5 * nonAscii
65
+ }
66
+
67
+ /** Consecutive backslashes immediately before index `at`. */
68
+ function trailingBackslashes(text: string, at: number): number {
69
+ let count = 0
70
+ while (text[at - 1 - count] === '\\') count++
71
+ return count
72
+ }
73
+
74
+ /** Escape a literal string so it matches itself inside a `RegExp`. */
75
+ function escapeRegExp(text: string): string {
76
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
77
+ }
78
+
79
+ /**
80
+ * A pattern that matches `model` where every non-ASCII character appears as its
81
+ * `\uXXXX` escape (case-insensitive in the hex letters) and every ASCII
82
+ * character appears literally. Used to find, in a file written with escapes,
83
+ * the span a model wrote with the characters themselves.
84
+ */
85
+ function buildEscapeTolerantPattern(model: string): string {
86
+ let pattern = ''
87
+ for (let i = 0; i < model.length; i++) {
88
+ const code = model.charCodeAt(i)
89
+ if (code >= 128) {
90
+ pattern += '\\\\u'
91
+ for (const digit of code.toString(16).padStart(4, '0'))
92
+ pattern += digit >= 'a' ? `[${digit}${digit.toUpperCase()}]` : digit
93
+ } else {
94
+ // biome-ignore lint/style/noNonNullAssertion: index < length
95
+ pattern += escapeRegExp(model[i]!)
96
+ }
97
+ }
98
+ return pattern
99
+ }
100
+
101
+ /**
102
+ * Find, in `file`, the escaped form of `model` — where `model` is written with
103
+ * real non-ASCII characters and `file` holds them as `\uXXXX`. Returns the
104
+ * matched (escaped) substring, or null when there is no such span, when the
105
+ * pattern would be ambiguous, or when a matched `\u` is itself escaped by an
106
+ * odd run of backslashes (a literal `\u`, not an escape).
107
+ */
108
+ function findEscapedForm(model: string, file: string): string | null {
109
+ // A model ending in an odd number of backslashes cannot be the clean prefix
110
+ // of an escape sequence; bail rather than build a pattern that misreads it.
111
+ if (trailingBackslashes(model, model.length) % 2 === 1) return null
112
+
113
+ // Offsets, within the escaped match, whose backslash parity must be checked
114
+ // so a literal `\u` in the file is not mistaken for an escape.
115
+ const sensitiveOffsets: number[] = model[0] === '\\' ? [0] : []
116
+ let offset = 0
117
+ for (let i = 0; i < model.length; i++) {
118
+ if (model.charCodeAt(i) >= 128) {
119
+ sensitiveOffsets.push(offset)
120
+ offset += 6
121
+ } else {
122
+ offset += 1
123
+ }
124
+ }
125
+
126
+ let regex: RegExp
127
+ try {
128
+ regex = new RegExp(buildEscapeTolerantPattern(model), 'g')
129
+ } catch {
130
+ return null
131
+ }
132
+ for (let match = regex.exec(file); match !== null; match = regex.exec(file)) {
133
+ const at = match.index
134
+ if (sensitiveOffsets.every((o) => trailingBackslashes(file, at + o) % 2 === 0)) {
135
+ return file.indexOf(match[0]) === at ? match[0] : null
136
+ }
137
+ regex.lastIndex = at + 1
138
+ }
139
+ return null
140
+ }
141
+
142
+ /**
143
+ * The span of `content` that `oldString` refers to, recovering it across a
144
+ * quote- or escape-only difference, or null when nothing matches even then.
145
+ *
146
+ * The returned string is a real substring of `content` (or its escaped form),
147
+ * so the caller can run the ordinary uniqueness check and splice against it.
148
+ */
149
+ export function resolveOldString(content: string, oldString: string): string | null {
150
+ if (content.includes(oldString)) return oldString
151
+
152
+ const straight = straightenQuotes(oldString)
153
+ const quoteIndex = straightenQuotes(content).indexOf(straight)
154
+ if (quoteIndex !== -1) return content.substring(quoteIndex, quoteIndex + oldString.length)
155
+
156
+ if (HAS_UNICODE_ESCAPE.test(oldString)) {
157
+ const decoded = decodeUnicodeEscapes(oldString)
158
+ if (decoded !== oldString && content.includes(decoded)) return decoded
159
+ }
160
+
161
+ if (HAS_NON_ASCII.test(oldString)) {
162
+ if (escapedLength(oldString) > content.length || !content.includes('\\u')) return null
163
+ return findEscapedForm(oldString, content)
164
+ }
165
+
166
+ return null
167
+ }
168
+
169
+ /** Is `char` at `index` opening a quote (preceded by a boundary) rather than closing one? */
170
+ function isOpeningPosition(chars: readonly string[], index: number): boolean {
171
+ if (index === 0) return true
172
+ const before = chars[index - 1]
173
+ return (
174
+ before === ' ' ||
175
+ before === '\t' ||
176
+ before === '\n' ||
177
+ before === '\r' ||
178
+ before === '(' ||
179
+ before === '[' ||
180
+ before === '{' ||
181
+ before === '—' ||
182
+ before === '–'
183
+ )
184
+ }
185
+
186
+ /** Apply curly double quotes to `text`, choosing opening vs closing by context. */
187
+ function curlifyDoubleQuotes(text: string): string {
188
+ const chars = [...text]
189
+ return chars
190
+ .map((char, index) =>
191
+ char === '"'
192
+ ? isOpeningPosition(chars, index)
193
+ ? LEFT_DOUBLE_QUOTE
194
+ : RIGHT_DOUBLE_QUOTE
195
+ : char,
196
+ )
197
+ .join('')
198
+ }
199
+
200
+ /** Apply curly single quotes to `text`; an apostrophe between letters is always closing. */
201
+ function curlifySingleQuotes(text: string): string {
202
+ const chars = [...text]
203
+ return chars
204
+ .map((char, index) => {
205
+ if (char !== "'") return char
206
+ const before = index > 0 ? chars[index - 1] : undefined
207
+ const after = index < chars.length - 1 ? chars[index + 1] : undefined
208
+ const betweenLetters =
209
+ before !== undefined && /\p{L}/u.test(before) && after !== undefined && /\p{L}/u.test(after)
210
+ if (betweenLetters) return RIGHT_SINGLE_QUOTE
211
+ return isOpeningPosition(chars, index) ? LEFT_SINGLE_QUOTE : RIGHT_SINGLE_QUOTE
212
+ })
213
+ .join('')
214
+ }
215
+
216
+ /**
217
+ * Carry the transformation that matched `oldString` across to `newString`, so
218
+ * the replacement matches the span it lands in rather than the literal the
219
+ * model typed.
220
+ *
221
+ * `resolved` is what {@link resolveOldString} returned — equal to `oldString`
222
+ * for an exact match (then `newString` is returned unchanged), and otherwise
223
+ * the real span, which tells us which strategy fired.
224
+ */
225
+ export function reconcileNewString(oldString: string, resolved: string, newString: string): string {
226
+ if (resolved === oldString) return newString
227
+
228
+ // character in old, escape in file: re-escape new's non-ASCII to the file's
229
+ // hex spelling, matching the letter-case the file used.
230
+ if (
231
+ HAS_NON_ASCII.test(oldString) &&
232
+ escapedLength(oldString) === resolved.length &&
233
+ findEscapedForm(oldString, resolved) !== null
234
+ ) {
235
+ const perChar = new Map<number, string>()
236
+ let upper = 0
237
+ let lower = 0
238
+ for (let oldIndex = 0, resolvedIndex = 0; oldIndex < oldString.length; oldIndex++) {
239
+ const code = oldString.charCodeAt(oldIndex)
240
+ if (code >= 128) {
241
+ const hex = resolved.slice(resolvedIndex + 2, resolvedIndex + 6)
242
+ perChar.set(code, hex)
243
+ for (const digit of hex)
244
+ if (digit >= 'a' && digit <= 'f') lower++
245
+ else if (digit >= 'A' && digit <= 'F') upper++
246
+ resolvedIndex += 6
247
+ } else {
248
+ resolvedIndex += 1
249
+ }
250
+ }
251
+ return newString.replace(/[\u0080-￿]/g, (char) => {
252
+ const code = char.charCodeAt(0)
253
+ const known = perChar.get(code)
254
+ if (known !== undefined) return `\\u${known}`
255
+ const hex = code.toString(16).padStart(4, '0')
256
+ return `\\u${upper > lower ? hex.toUpperCase() : hex}`
257
+ })
258
+ }
259
+
260
+ // escape in old, character in file: decode new's escapes to match.
261
+ if (HAS_UNICODE_ESCAPE.test(oldString) && decodeUnicodeEscapes(oldString) === resolved) {
262
+ return decodeUnicodeEscapes(newString)
263
+ }
264
+
265
+ // quote difference: re-apply the file's curly quotes to new, by kind.
266
+ const hasCurlyDouble =
267
+ resolved.includes(LEFT_DOUBLE_QUOTE) || resolved.includes(RIGHT_DOUBLE_QUOTE)
268
+ const hasCurlySingle =
269
+ resolved.includes(LEFT_SINGLE_QUOTE) || resolved.includes(RIGHT_SINGLE_QUOTE)
270
+ let reconciled = newString
271
+ if (hasCurlyDouble) reconciled = curlifyDoubleQuotes(reconciled)
272
+ if (hasCurlySingle) reconciled = curlifySingleQuotes(reconciled)
273
+ return reconciled
274
+ }
@@ -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
@@ -1,5 +1,6 @@
1
1
  import type { CostInfo, TokenUsage } from '../common/index.js'
2
2
  import type { CheckpointId, PlanId, SessionId, TurnId } from '../ids/index.js'
3
+ import type { Message } from '../message/index.js'
3
4
 
4
5
  export type { CheckpointId }
5
6
 
@@ -139,7 +140,28 @@ export type HITLDecisionRequest =
139
140
  question: UserQuestionData
140
141
  }
141
142
 
142
- export type ResumeHandler = (request: HITLDecisionRequest) => Promise<HITLResumeDecision>
143
+ /**
144
+ * What the turn offers a resume handler besides the request, in memory only.
145
+ *
146
+ * Kept out of {@link HITLDecisionRequest} on purpose: the request is written
147
+ * to the session log when a decision takes long, and a transcript does not
148
+ * belong there. A handler that does not need it ignores the argument; one that
149
+ * wraps another passes it on.
150
+ */
151
+ export interface ResumeContext {
152
+ /**
153
+ * The conversation as it stands now, the batch under review included.
154
+ * Read on demand; the array is the turn's own, so a caller must not change it.
155
+ */
156
+ readonly transcript: () => readonly Message[]
157
+ /** The turn's cancellation signal, when it has one. */
158
+ readonly signal?: AbortSignal
159
+ }
160
+
161
+ export type ResumeHandler = (
162
+ request: HITLDecisionRequest,
163
+ context?: ResumeContext,
164
+ ) => Promise<HITLResumeDecision>
143
165
 
144
166
  export interface ToolCallSummary {
145
167
  id: string
@@ -158,6 +180,24 @@ export interface ToolCallSummary {
158
180
  reason?: string
159
181
  /** A matching rule requested review; read-only exemptions must not bypass it. */
160
182
  explicitReview?: true
183
+ /**
184
+ * A rule written to ask a person (a `review` rule), or a hook that asked
185
+ * for one, requested this review. Unlike {@link explicitReview} it is not
186
+ * set when an `allow` rule was demoted to review because the call reaches
187
+ * past its boundary. `bypass` and `classify` keep this question for a
188
+ * person; they answer the demoted kind themselves.
189
+ */
190
+ askRule?: true
191
+ /**
192
+ * How wide the `allow` rule that settled this call is. `tool` means the
193
+ * rule names the whole tool, a category, a tier or a source, or its
194
+ * pattern matches every input; `pattern` means it matches some inputs
195
+ * only. Present only when `decision` is `allow` and a rule (not the
196
+ * read-only allowance) decided it. The `classify` review mode reads it:
197
+ * a whole-tool allow for a shell tool would let the classifier never
198
+ * see a command.
199
+ */
200
+ allowScope?: 'tool' | 'pattern'
161
201
  }
162
202
  /**
163
203
  * What this call reaches beyond the turn's ordinary boundary, as the
@@ -278,6 +318,14 @@ export interface ToolCallEscalation {
278
318
  * tool that declares `commandArgument`.
279
319
  */
280
320
  dangerousRemoval?: string
321
+ /**
322
+ * Why the kernel could not finish looking at this call: the check that
323
+ * finds the escalations above threw, so none of them is known to be
324
+ * absent. Treated as the most serious escalation there is, because the
325
+ * safety checks did not run: a person is asked in every mode, `bypass`
326
+ * and `classify` included, and it is refused when nobody can be asked.
327
+ */
328
+ unfinishedCheck?: string
281
329
  }
282
330
 
283
331
  export interface ToolModification {
@@ -1178,6 +1178,23 @@ export interface ToolDefinition<TInput = unknown> extends ToolPresentation<TInpu
1178
1178
  */
1179
1179
  requiresApproval?(input: TInput): boolean
1180
1180
 
1181
+ /**
1182
+ * What the auto-mode safety classifier is shown of this call.
1183
+ *
1184
+ * The classifier (`createAutoReviewClassifier`, review mode `classify`)
1185
+ * reads a compact transcript, not raw tool arguments: a tool knows which
1186
+ * part of its own input says what the call DOES. A shell tool returns the
1187
+ * command, a file tool the path and a short summary of the change, a fetch
1188
+ * the address. The string should be short and carry no content the model
1189
+ * could not already be trusted with; the runtime caps its length.
1190
+ *
1191
+ * Absent, the classifier is shown the input as compact JSON. A tool whose
1192
+ * input is large or opaque (a file body, a binary payload) should declare
1193
+ * this so the classifier judges the action rather than the bulk. Never
1194
+ * called with a tool RESULT: results are untrusted and are not shown.
1195
+ */
1196
+ classifierInput?(input: TInput): string
1197
+
1181
1198
  /**
1182
1199
  * Free-form, tool-author-declared data for filtering and behaviour
1183
1200
  * customization — never a classification the runtime itself reads.