@namzu/sdk 57.0.0 → 58.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 (278) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +3 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/file-definitions.d.ts +35 -4
  6. package/dist/agents/file-definitions.d.ts.map +1 -1
  7. package/dist/agents/file-definitions.js +98 -8
  8. package/dist/agents/file-definitions.js.map +1 -1
  9. package/dist/authorization/protected-paths.d.ts +9 -0
  10. package/dist/authorization/protected-paths.d.ts.map +1 -1
  11. package/dist/authorization/protected-paths.js +29 -3
  12. package/dist/authorization/protected-paths.js.map +1 -1
  13. package/dist/compaction/index.d.ts +1 -0
  14. package/dist/compaction/index.d.ts.map +1 -1
  15. package/dist/compaction/index.js.map +1 -1
  16. package/dist/compaction/manual.d.ts +7 -0
  17. package/dist/compaction/manual.d.ts.map +1 -1
  18. package/dist/compaction/manual.js +31 -12
  19. package/dist/compaction/manual.js.map +1 -1
  20. package/dist/compaction/model-summary.d.ts +101 -0
  21. package/dist/compaction/model-summary.d.ts.map +1 -0
  22. package/dist/compaction/model-summary.js +333 -0
  23. package/dist/compaction/model-summary.js.map +1 -0
  24. package/dist/compaction/verifier.d.ts +6 -1
  25. package/dist/compaction/verifier.d.ts.map +1 -1
  26. package/dist/compaction/verifier.js +3 -3
  27. package/dist/compaction/verifier.js.map +1 -1
  28. package/dist/config/runtime.d.ts +62 -0
  29. package/dist/config/runtime.d.ts.map +1 -1
  30. package/dist/config/runtime.js +16 -0
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/connector/mcp/client.d.ts +8 -0
  33. package/dist/connector/mcp/client.d.ts.map +1 -1
  34. package/dist/connector/mcp/client.js +75 -2
  35. package/dist/connector/mcp/client.js.map +1 -1
  36. package/dist/constants/mcp/index.d.ts +2 -0
  37. package/dist/constants/mcp/index.d.ts.map +1 -1
  38. package/dist/constants/mcp/index.js +2 -0
  39. package/dist/constants/mcp/index.js.map +1 -1
  40. package/dist/constants/plugin/index.d.ts +2 -0
  41. package/dist/constants/plugin/index.d.ts.map +1 -1
  42. package/dist/constants/plugin/index.js +2 -0
  43. package/dist/constants/plugin/index.js.map +1 -1
  44. package/dist/manager/agent/lifecycle.d.ts +1 -1
  45. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  46. package/dist/manager/agent/lifecycle.js +25 -4
  47. package/dist/manager/agent/lifecycle.js.map +1 -1
  48. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  49. package/dist/manager/session/turn-recorder.js +2 -1
  50. package/dist/manager/session/turn-recorder.js.map +1 -1
  51. package/dist/persona/assembler.d.ts.map +1 -1
  52. package/dist/persona/assembler.js +2 -2
  53. package/dist/persona/assembler.js.map +1 -1
  54. package/dist/plugin/index.d.ts +1 -1
  55. package/dist/plugin/index.d.ts.map +1 -1
  56. package/dist/plugin/lifecycle.d.ts +8 -1
  57. package/dist/plugin/lifecycle.d.ts.map +1 -1
  58. package/dist/plugin/lifecycle.js +9 -2
  59. package/dist/plugin/lifecycle.js.map +1 -1
  60. package/dist/plugin/shell-hook-http.d.ts +39 -0
  61. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  62. package/dist/plugin/shell-hook-http.js +163 -0
  63. package/dist/plugin/shell-hook-http.js.map +1 -0
  64. package/dist/plugin/shell-hook.d.ts +93 -11
  65. package/dist/plugin/shell-hook.d.ts.map +1 -1
  66. package/dist/plugin/shell-hook.js +223 -39
  67. package/dist/plugin/shell-hook.js.map +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  69. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  70. package/dist/prompt/coding-agent-doctrine.js +2 -1
  71. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -2
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +2 -2
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +1 -0
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +2 -0
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +1 -1
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/executor.d.ts +4 -0
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +47 -9
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +29 -1
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +6 -2
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  94. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  96. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/phases/compaction.js +52 -5
  99. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  101. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  105. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  106. package/dist/runtime/query/path-escalation.d.ts +2 -0
  107. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  108. package/dist/runtime/query/path-escalation.js +7 -4
  109. package/dist/runtime/query/path-escalation.js.map +1 -1
  110. package/dist/runtime/query/plugin-hooks.js +2 -2
  111. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  112. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  113. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  114. package/dist/runtime/query/prepare-turn.js +25 -0
  115. package/dist/runtime/query/prepare-turn.js.map +1 -1
  116. package/dist/runtime/query/review-policy.d.ts +3 -2
  117. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  118. package/dist/runtime/query/review-policy.js +32 -13
  119. package/dist/runtime/query/review-policy.js.map +1 -1
  120. package/dist/scheduler/local.js +1 -1
  121. package/dist/scheduler/local.js.map +1 -1
  122. package/dist/session/retention/archive.js +1 -1
  123. package/dist/session/retention/archive.js.map +1 -1
  124. package/dist/session/workspace/driver.d.ts +6 -0
  125. package/dist/session/workspace/driver.d.ts.map +1 -1
  126. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  127. package/dist/session/workspace/git-worktree.js +28 -0
  128. package/dist/session/workspace/git-worktree.js.map +1 -1
  129. package/dist/skills/index.d.ts +1 -1
  130. package/dist/skills/index.d.ts.map +1 -1
  131. package/dist/skills/index.js +1 -1
  132. package/dist/skills/index.js.map +1 -1
  133. package/dist/skills/loader.d.ts +7 -0
  134. package/dist/skills/loader.d.ts.map +1 -1
  135. package/dist/skills/loader.js +53 -0
  136. package/dist/skills/loader.js.map +1 -1
  137. package/dist/skills/registry.d.ts +1 -1
  138. package/dist/skills/registry.d.ts.map +1 -1
  139. package/dist/skills/registry.js +2 -1
  140. package/dist/skills/registry.js.map +1 -1
  141. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  142. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  143. package/dist/tools/builtins/bash-cwd.js +127 -0
  144. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  145. package/dist/tools/builtins/bash.d.ts.map +1 -1
  146. package/dist/tools/builtins/bash.js +41 -6
  147. package/dist/tools/builtins/bash.js.map +1 -1
  148. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  149. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  150. package/dist/tools/builtins/bounded-file-read.js +67 -0
  151. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  152. package/dist/tools/builtins/grep.d.ts +6 -1
  153. package/dist/tools/builtins/grep.d.ts.map +1 -1
  154. package/dist/tools/builtins/grep.js +187 -24
  155. package/dist/tools/builtins/grep.js.map +1 -1
  156. package/dist/tools/builtins/read-file.d.ts +1 -0
  157. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  158. package/dist/tools/builtins/read-file.js +113 -28
  159. package/dist/tools/builtins/read-file.js.map +1 -1
  160. package/dist/tools/builtins/read-media.d.ts +68 -0
  161. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  162. package/dist/tools/builtins/read-media.js +397 -0
  163. package/dist/tools/builtins/read-media.js.map +1 -0
  164. package/dist/tools/builtins/search-tools.js +1 -1
  165. package/dist/tools/builtins/search-tools.js.map +1 -1
  166. package/dist/tools/builtins/skill.d.ts +4 -2
  167. package/dist/tools/builtins/skill.d.ts.map +1 -1
  168. package/dist/tools/builtins/skill.js +61 -2
  169. package/dist/tools/builtins/skill.js.map +1 -1
  170. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  171. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  172. package/dist/tools/builtins/sleep-guard.js +33 -0
  173. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  174. package/dist/tools/builtins/view-image.d.ts +23 -1
  175. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  176. package/dist/tools/builtins/view-image.js +66 -58
  177. package/dist/tools/builtins/view-image.js.map +1 -1
  178. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  179. package/dist/tools/file-read-tracker.js +31 -0
  180. package/dist/tools/file-read-tracker.js.map +1 -1
  181. package/dist/toolsets/manager.js +3 -3
  182. package/dist/toolsets/manager.js.map +1 -1
  183. package/dist/types/agent/base.d.ts +8 -0
  184. package/dist/types/agent/base.d.ts.map +1 -1
  185. package/dist/types/agent/query.d.ts +2 -0
  186. package/dist/types/agent/query.d.ts.map +1 -1
  187. package/dist/types/agent/scheduler.d.ts +8 -0
  188. package/dist/types/agent/scheduler.d.ts.map +1 -1
  189. package/dist/types/agent/task.d.ts +6 -2
  190. package/dist/types/agent/task.d.ts.map +1 -1
  191. package/dist/types/agent/task.js.map +1 -1
  192. package/dist/types/connector/mcp.d.ts +46 -0
  193. package/dist/types/connector/mcp.d.ts.map +1 -1
  194. package/dist/types/hitl/index.d.ts +1 -1
  195. package/dist/types/plugin/index.d.ts +74 -7
  196. package/dist/types/plugin/index.d.ts.map +1 -1
  197. package/dist/types/plugin/index.js +12 -1
  198. package/dist/types/plugin/index.js.map +1 -1
  199. package/dist/types/session/sub-session.d.ts +6 -2
  200. package/dist/types/session/sub-session.d.ts.map +1 -1
  201. package/dist/types/skills/index.d.ts +36 -0
  202. package/dist/types/skills/index.d.ts.map +1 -1
  203. package/dist/types/skills/index.js +9 -0
  204. package/dist/types/skills/index.js.map +1 -1
  205. package/dist/types/tool/index.d.ts +37 -0
  206. package/dist/types/tool/index.d.ts.map +1 -1
  207. package/dist/types/tool/index.js.map +1 -1
  208. package/dist/types/workspace/ref.d.ts +2 -0
  209. package/dist/types/workspace/ref.d.ts.map +1 -1
  210. package/dist/utils/frontmatter.d.ts +12 -0
  211. package/dist/utils/frontmatter.d.ts.map +1 -1
  212. package/dist/utils/frontmatter.js +6 -0
  213. package/dist/utils/frontmatter.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/agents/QueryAgent.ts +3 -0
  216. package/src/agents/file-definitions.ts +126 -6
  217. package/src/authorization/protected-paths.ts +30 -2
  218. package/src/compaction/index.ts +1 -0
  219. package/src/compaction/manual.ts +68 -30
  220. package/src/compaction/model-summary.ts +412 -0
  221. package/src/compaction/verifier.ts +3 -3
  222. package/src/config/runtime.ts +16 -0
  223. package/src/connector/mcp/client.ts +87 -1
  224. package/src/constants/mcp/index.ts +3 -0
  225. package/src/constants/plugin/index.ts +2 -0
  226. package/src/manager/agent/lifecycle.ts +24 -5
  227. package/src/manager/session/turn-recorder.ts +2 -1
  228. package/src/persona/assembler.ts +2 -2
  229. package/src/plugin/index.ts +5 -0
  230. package/src/plugin/lifecycle.ts +16 -2
  231. package/src/plugin/shell-hook-http.ts +181 -0
  232. package/src/plugin/shell-hook.ts +386 -89
  233. package/src/prompt/coding-agent-doctrine.ts +2 -1
  234. package/src/public-runtime.ts +6 -0
  235. package/src/public-tools.ts +2 -0
  236. package/src/public-types.ts +5 -0
  237. package/src/runtime/query/executor.ts +54 -10
  238. package/src/runtime/query/index.ts +44 -0
  239. package/src/runtime/query/iteration/index.ts +11 -3
  240. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  241. package/src/runtime/query/iteration/phases/compaction.ts +67 -5
  242. package/src/runtime/query/iteration/phases/context.ts +6 -0
  243. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  244. package/src/runtime/query/path-escalation.ts +11 -5
  245. package/src/runtime/query/plugin-hooks.ts +2 -2
  246. package/src/runtime/query/prepare-turn.ts +33 -0
  247. package/src/runtime/query/review-policy.ts +45 -16
  248. package/src/scheduler/local.ts +1 -1
  249. package/src/session/retention/archive.ts +1 -1
  250. package/src/session/workspace/driver.ts +6 -0
  251. package/src/session/workspace/git-worktree.ts +31 -0
  252. package/src/skills/index.ts +6 -1
  253. package/src/skills/loader.ts +54 -0
  254. package/src/skills/registry.ts +7 -6
  255. package/src/tools/builtins/bash-cwd.ts +156 -0
  256. package/src/tools/builtins/bash.ts +79 -34
  257. package/src/tools/builtins/bounded-file-read.ts +80 -0
  258. package/src/tools/builtins/grep.ts +198 -28
  259. package/src/tools/builtins/read-file.ts +151 -30
  260. package/src/tools/builtins/read-media.ts +483 -0
  261. package/src/tools/builtins/search-tools.ts +1 -1
  262. package/src/tools/builtins/skill.ts +91 -2
  263. package/src/tools/builtins/sleep-guard.ts +31 -0
  264. package/src/tools/builtins/view-image.ts +74 -62
  265. package/src/tools/file-read-tracker.ts +30 -0
  266. package/src/toolsets/manager.ts +3 -3
  267. package/src/types/agent/base.ts +9 -0
  268. package/src/types/agent/query.ts +2 -0
  269. package/src/types/agent/scheduler.ts +9 -0
  270. package/src/types/agent/task.ts +6 -2
  271. package/src/types/connector/mcp.ts +48 -0
  272. package/src/types/hitl/index.ts +1 -1
  273. package/src/types/plugin/index.ts +102 -16
  274. package/src/types/session/sub-session.ts +6 -2
  275. package/src/types/skills/index.ts +48 -0
  276. package/src/types/tool/index.ts +38 -0
  277. package/src/types/workspace/ref.ts +2 -0
  278. package/src/utils/frontmatter.ts +21 -0
@@ -16,7 +16,9 @@ import type {
16
16
  ReviewedExecution,
17
17
  ToolCallDeclines,
18
18
  ToolCallDenials,
19
+ ToolCallOutcome,
19
20
  } from '../../executor.js'
21
+ import { applyLifecycleHookResults } from '../../plugin-hooks.js'
20
22
  import {
21
23
  awaitProjectInstructionCallback,
22
24
  replaceProjectInstructionSnapshot,
@@ -212,11 +214,81 @@ export async function* runToolReview(
212
214
  /** Set only where a person's No is known, so a policy refusal never lands in it. */
213
215
  let declines: ToolCallDeclines | undefined
214
216
 
217
+ /**
218
+ * Tell `permission_denied` hooks about each call refused without a person's
219
+ * No: a rule, the mode's policy, a classifier or a hook. Observational, so
220
+ * a hook's answer changes nothing; one that throws is logged, not allowed to
221
+ * fail the step.
222
+ */
223
+ const announceRefusals = async (denials?: ToolCallDenials): Promise<void> => {
224
+ if (!ctx.pluginManager || !denials || denials.size === 0) return
225
+ for (const summary of toolCallSummaries) {
226
+ const reason = denials.get(summary.id)
227
+ if (reason === undefined || declines?.has(summary.id)) continue
228
+ if (ctx.abortController.signal.aborted) return
229
+ try {
230
+ await ctx.pluginManager.executeHooks(
231
+ 'permission_denied',
232
+ {
233
+ sessionId: ctx.recorder.sessionId,
234
+ turnId: ctx.recorder.turnId,
235
+ toolName: summary.name,
236
+ toolInput: summary.input,
237
+ reason,
238
+ signal: ctx.abortController.signal,
239
+ },
240
+ ctx.emitEvent,
241
+ )
242
+ } catch (error) {
243
+ if (ctx.abortController.signal.aborted) return
244
+ ctx.log.warn('A permission_denied hook failed', {
245
+ 'exception.message': error instanceof Error ? error.message : String(error),
246
+ })
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Tell `post_tool_batch` hooks that every call of this step has settled.
253
+ * Fires once per step, before the next model request. A hook may ask for the
254
+ * turn to stop once this step is done; an `error` fails the turn like any
255
+ * other lifecycle hook.
256
+ */
257
+ const announceBatch = async (outcomes: readonly ToolCallOutcome[]): Promise<void> => {
258
+ if (!ctx.pluginManager || ctx.abortController.signal.aborted) return
259
+ const inputs = new Map(toolCallSummaries.map((summary) => [summary.id, summary.input]))
260
+ const toolCalls = outcomes.map((outcome) => ({
261
+ toolCallId: outcome.toolCallId,
262
+ toolName: outcome.toolName,
263
+ toolInput: inputs.get(outcome.toolCallId),
264
+ output: outcome.output,
265
+ isError: outcome.isError === true,
266
+ }))
267
+ if (toolCalls.length === 0) return
268
+ const results = await ctx.pluginManager.executeHooks(
269
+ 'post_tool_batch',
270
+ {
271
+ sessionId: ctx.recorder.sessionId,
272
+ turnId: ctx.recorder.turnId,
273
+ toolCalls,
274
+ signal: ctx.abortController.signal,
275
+ },
276
+ ctx.emitEvent,
277
+ )
278
+ applyLifecycleHookResults('post_tool_batch', results)
279
+ for (const result of results) {
280
+ if (result.action === 'continue' && result.halt) {
281
+ ctx.toolExecutor.requestHalt?.(result.halt.reason)
282
+ }
283
+ }
284
+ }
285
+
215
286
  /** Executes the batch, answering every call, and appends the results. */
216
287
  const settleBatch = async (
217
288
  denials?: ToolCallDenials,
218
289
  review?: ReviewedExecution,
219
290
  ): Promise<void> => {
291
+ await announceRefusals(denials)
220
292
  const startedAt = Date.now()
221
293
  const batch = await ctx.toolExecutor.executeBatch(
222
294
  response,
@@ -228,6 +300,7 @@ export async function* runToolReview(
228
300
  )
229
301
  toolMs += Date.now() - startedAt
230
302
  executed = batch.results
303
+ await announceBatch(batch.results)
231
304
  // Registry receipts are ordered by actual completion, include nested
232
305
  // dispatches, and exclude denials/recovered results. A successful possible
233
306
  // mutation permits another experiment; unrelated reads cannot do so.
@@ -533,9 +606,7 @@ export async function* runToolReview(
533
606
  toolCall.authorization = {
534
607
  decision: gateResult.decision,
535
608
  ...(gateResult.reason ? { reason: gateResult.reason } : {}),
536
- ...(gateResult.decision === 'review' && (gateResult.matchedRule || hookAsked)
537
- ? { explicitReview: true as const }
538
- : {}),
609
+ ...(ruleAsked || hookAsked ? { explicitReview: true as const } : {}),
539
610
  ...(ruleAsked || hookAsked ? { askRule: true as const } : {}),
540
611
  ...(gateResult.decision === 'allow' && allowRuleScope(gateResult.matchedRule)
541
612
  ? { allowScope: allowRuleScope(gateResult.matchedRule) }
@@ -34,6 +34,8 @@ export interface PathEscalations {
34
34
  export interface PathEscalationContext {
35
35
  /** The working directory first, then the added directories. */
36
36
  readonly roots: readonly string[]
37
+ /** Actual shell start directory; roots remain the original authorization boundary. */
38
+ readonly cwd?: string
37
39
  /** Whether to report paths outside the roots (`outsideRootAccess: 'review'`, no sandbox). */
38
40
  readonly reviewOutside: boolean
39
41
  }
@@ -150,7 +152,7 @@ export async function shellEscalations(
150
152
  ): Promise<PathEscalations> {
151
153
  const reading = readShellLine(line, dialect)
152
154
  const access = shellPathAccess(reading)
153
- const [cwd, ...added] = context.roots
155
+ const cwd = context.cwd ?? context.roots[0]
154
156
  const outside = new Set<string>()
155
157
  const protectedPaths = new Set<string>()
156
158
  const home = homedir()
@@ -222,14 +224,18 @@ export async function shellEscalations(
222
224
  }
223
225
  }
224
226
  }
225
- const removal =
226
- cwd === undefined
227
+ const removalAt = (directory: string | undefined) =>
228
+ directory === undefined
227
229
  ? undefined
228
230
  : dangerousRemoval(reading, access.changesDirectory, {
229
- cwd,
230
- additionalDirectories: added,
231
+ cwd: directory,
232
+ additionalDirectories: context.roots,
231
233
  home,
232
234
  })
235
+ // Moving into an added directory must not make removal of the original
236
+ // working directory cease to be catastrophic. Keep both conservative floors.
237
+ const removal =
238
+ removalAt(cwd) ?? (cwd !== context.roots[0] ? removalAt(context.roots[0]) : undefined)
233
239
  return {
234
240
  outsidePaths: [...outside],
235
241
  protectedPaths: [...protectedPaths],
@@ -19,9 +19,9 @@ export function applyLifecycleHookResults(
19
19
  case 'continue':
20
20
  continue
21
21
  case 'annotate':
22
- if (event !== 'user_prompt_submit') {
22
+ if (event !== 'user_prompt_submit' && event !== 'subagent_start') {
23
23
  throw new Error(
24
- `Plugin hook ${event} returned 'annotate', which only user_prompt_submit accepts`,
24
+ `Plugin hook ${event} returned 'annotate', which only user_prompt_submit and subagent_start accept`,
25
25
  )
26
26
  }
27
27
  if (result.text.trim().length > 0) annotations.push(result.text.trim())
@@ -89,6 +89,8 @@ export interface PreparedTurn {
89
89
  readonly approvalPolicy: SessionApprovalPolicy
90
90
  readonly eventTranslator: EventTranslator
91
91
  readonly executeUserInterruptHooks: (terminalError: unknown) => Promise<void>
92
+ /** Tell `turn_failure` hooks the root turn failed; never throws, never fires for a cancellation. */
93
+ readonly executeTurnFailureHooks: (terminalError: unknown) => Promise<void>
92
94
  /**
93
95
  * Repair reports this turn owes its log.
94
96
  *
@@ -823,6 +825,36 @@ export async function prepareTurn(params: QueryParams): Promise<PreparedTurn> {
823
825
  }
824
826
  }
825
827
 
828
+ const executeTurnFailureHooks = async (terminalError: unknown): Promise<void> => {
829
+ // A cancellation is the operator's own act, a child's failure reaches its
830
+ // parent as a tool result, and the turn is already failed: this is a
831
+ // notice, so nothing it does may change how the turn settles.
832
+ if (
833
+ !params.pluginManager ||
834
+ params.parentSessionId !== undefined ||
835
+ ctx.abortController.signal.aborted ||
836
+ isCallerAbortError(terminalError, ctx.abortController.signal)
837
+ ) {
838
+ return
839
+ }
840
+ try {
841
+ await params.pluginManager.executeHooks(
842
+ 'turn_failure',
843
+ {
844
+ sessionId: ctx.sessionId,
845
+ turnId: ctx.turnId,
846
+ error: toErrorMessage(terminalError),
847
+ },
848
+ eventTranslator.emitEvent,
849
+ )
850
+ } catch (error) {
851
+ ctx.log.error('Turn failure hooks did not settle cleanly', {
852
+ [NAMZU.TURN_ID]: ctx.turnId,
853
+ ...errorAttributes(error),
854
+ })
855
+ }
856
+ }
857
+
826
858
  return {
827
859
  turnConfig,
828
860
  budget,
@@ -836,6 +868,7 @@ export async function prepareTurn(params: QueryParams): Promise<PreparedTurn> {
836
868
  approvalPolicy,
837
869
  eventTranslator,
838
870
  executeUserInterruptHooks,
871
+ executeTurnFailureHooks,
839
872
  pendingHistoryRepairs,
840
873
  initialMessages,
841
874
  historyIds,
@@ -28,6 +28,8 @@ import {
28
28
  readShellLine,
29
29
  shellPathAccess,
30
30
  } from '../../authorization/shell-paths.js'
31
+ import { effectiveBashWorkingDirectory } from '../../tools/builtins/bash-cwd.js'
32
+ import { BashTool } from '../../tools/builtins/bash.js'
31
33
  import { isTrustedReadOnly } from '../../tools/trusted-read-only.js'
32
34
  import type { ToolManager } from '../../toolsets/manager.js'
33
35
  import type {
@@ -83,10 +85,11 @@ export type ReviewMode =
83
85
  *
84
86
  * Where `auto` approves what remains after the questions a person must
85
87
  * answer (a path outside the roots, a program nobody can name, a protected
86
- * write), `bypass` also approves those. It still asks, or refuses when
88
+ * write), `bypass` approves outside paths and unknown programs but not protected
89
+ * writes. It still asks, or refuses when
87
90
  * nobody can be asked: a `deny` rule (the gate refuses before this is
88
91
  * reached), an `ask` rule or a hook that asked for a person, a catastrophic
89
- * removal, a tool's own `requiresApproval`, a sandbox escape, and a call
92
+ * removal, a protected write, a tool's own `requiresApproval`, a sandbox escape, and a call
90
93
  * whose safety checks did not finish. The dangerous-pattern floor refuses in
91
94
  * the gate, unchanged. With nobody to ask, whatever would have been asked is
92
95
  * refused.
@@ -571,14 +574,15 @@ function isPermissive(mode: ReviewMode): boolean {
571
574
 
572
575
  /**
573
576
  * Whether the call is one of the few no mode may settle in advance: a rule or
574
- * hook asked for a person, a sandbox escape, a catastrophic removal, a check
575
- * that did not finish, or a tool that always needs approval. The dangerous
577
+ * hook asked for a person, a sandbox escape, a protected write, a catastrophic
578
+ * removal, a check that did not finish, or a tool that always needs approval. The dangerous
576
579
  * pattern floor and `deny` rules are not here: the gate refuses those first.
577
580
  */
578
581
  function isBypassImmune(tc: ToolCallSummary): boolean {
579
582
  return (
580
583
  tc.authorization?.askRule === true ||
581
584
  tc.escalation?.sandboxEscape === true ||
585
+ (tc.escalation?.protectedPaths?.length ?? 0) > 0 ||
582
586
  tc.escalation?.dangerousRemoval !== undefined ||
583
587
  tc.escalation?.unfinishedCheck !== undefined ||
584
588
  tc.requiresApproval === true
@@ -593,6 +597,9 @@ function immuneRefusal(
593
597
  if (!escapeConfirmed && toolCalls.some((tc) => tc.escalation?.sandboxEscape === true)) {
594
598
  return SANDBOX_ESCAPE_UNATTENDED_REFUSAL
595
599
  }
600
+ if (toolCalls.some((tc) => (tc.escalation?.protectedPaths?.length ?? 0) > 0)) {
601
+ return PROTECTED_PATH_UNATTENDED_REFUSAL
602
+ }
596
603
  if (toolCalls.some((tc) => tc.escalation?.dangerousRemoval !== undefined)) {
597
604
  return DANGEROUS_REMOVAL_UNATTENDED_REFUSAL
598
605
  }
@@ -879,7 +886,10 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
879
886
  * path, a path in a variable, a program nobody can name or a `sed` script
880
887
  * that can run something is not that.
881
888
  */
882
- const shellFileCommandsInRoots = async (tc: ToolCallSummary): Promise<boolean> => {
889
+ const shellFileCommandsInRoots = async (
890
+ tc: ToolCallSummary,
891
+ sessionId?: string,
892
+ ): Promise<boolean> => {
883
893
  const rootList = rootsNow()
884
894
  if (rootList.length === 0) return false
885
895
  if (
@@ -906,23 +916,39 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
906
916
  if (invocation.program === 'sed' && !sedOnlyEdits(invocation.args)) return false
907
917
  }
908
918
  if (shellPathAccess(reading).uses.some((use) => use.unresolved)) return false
909
- const found = await shellEscalations(line, dialect, { roots: rootList, reviewOutside: true })
910
- return (
911
- found.outsidePaths.length === 0 &&
912
- found.protectedPaths.length === 0 &&
913
- found.dangerousRemoval === undefined
919
+ const cwd =
920
+ tool.execute === BashTool.execute && rootList[0] !== undefined
921
+ ? effectiveBashWorkingDirectory(sessionId, rootList[0])
922
+ : undefined
923
+ const found = await shellEscalations(line, dialect, {
924
+ roots: rootList,
925
+ ...(cwd !== undefined ? { cwd } : {}),
926
+ reviewOutside: true,
927
+ })
928
+ // A standalone review request carries no sandbox attribution. Retain the
929
+ // original-root reading too: missing execution metadata can only ask more,
930
+ // never approve a call the old reading would have sent to a person.
931
+ const baseline =
932
+ cwd !== undefined && cwd !== rootList[0]
933
+ ? await shellEscalations(line, dialect, { roots: rootList, reviewOutside: true })
934
+ : found
935
+ return [found, baseline].every(
936
+ (reading) =>
937
+ reading.outsidePaths.length === 0 &&
938
+ reading.protectedPaths.length === 0 &&
939
+ reading.dangerousRemoval === undefined,
914
940
  )
915
941
  }
916
942
 
917
943
  /** What `accept-edits` approves without asking: edits, exempt reads and file commands inside the roots. */
918
- const acceptEditsAllows = async (tc: ToolCallSummary): Promise<boolean> =>
944
+ const acceptEditsAllows = async (tc: ToolCallSummary, sessionId?: string): Promise<boolean> =>
919
945
  !tc.authorization?.explicitReview &&
920
946
  tc.escalation === undefined &&
921
947
  !tc.isDestructive &&
922
948
  tc.requiresApproval !== true &&
923
949
  (ACCEPT_EDITS_TOOLS.has(tc.name) ||
924
950
  exempt(tc.name, tc.input) ||
925
- (await shellFileCommandsInRoots(tc)))
951
+ (await shellFileCommandsInRoots(tc, sessionId)))
926
952
 
927
953
  /**
928
954
  * Whether a rule's `allow` is one the classifier should not defer to: the
@@ -944,7 +970,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
944
970
  }
945
971
 
946
972
  /** Whether `classify` decides this call without the classifier: it would not be asked, or is plainly safe. */
947
- const neverSent = async (tc: ToolCallSummary): Promise<boolean> => {
973
+ const neverSent = async (tc: ToolCallSummary, sessionId?: string): Promise<boolean> => {
948
974
  if (tc.authorization?.decision === 'allow' && !allowRuleTooWide(tc)) return true
949
975
  // A name on the safe list is safe for what it does in the session; a read of
950
976
  // /etc/shadow is still a read of /etc/shadow, and a person's own ask is
@@ -965,7 +991,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
965
991
  ) {
966
992
  return true
967
993
  }
968
- return acceptEditsAllows(tc)
994
+ return acceptEditsAllows(tc, sessionId)
969
995
  }
970
996
 
971
997
  return async (request, context?: ResumeContext): Promise<HITLResumeDecision> => {
@@ -1043,7 +1069,10 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1043
1069
  request.toolCalls.map((tc) =>
1044
1070
  // The mode covers a write although the flag is set; the classifier
1045
1071
  // still judges one, so only this branch lifts the flag.
1046
- acceptEditsAllows(isFileWrite(tc.name) ? { ...tc, isDestructive: false } : tc),
1072
+ acceptEditsAllows(
1073
+ isFileWrite(tc.name) ? { ...tc, isDestructive: false } : tc,
1074
+ request.sessionId,
1075
+ ),
1047
1076
  ),
1048
1077
  )
1049
1078
  ).every(Boolean)
@@ -1093,7 +1122,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1093
1122
  const transcript = context?.transcript() ?? []
1094
1123
  for (const tc of live) {
1095
1124
  // The questions a person must answer are asked of a person, never of a model.
1096
- if (isBypassImmune(tc) || (await neverSent(tc))) continue
1125
+ if (isBypassImmune(tc) || (await neverSent(tc, request.sessionId))) continue
1097
1126
  const decision = await classifier.review({
1098
1127
  call: tc,
1099
1128
  transcript,
@@ -133,7 +133,7 @@ export class LocalTaskScheduler implements TaskScheduler {
133
133
  ...(options.phaseDetail ? { phaseDetail: options.phaseDetail } : {}),
134
134
  ...(options.phaseOrder !== undefined ? { phaseOrder: options.phaseOrder } : {}),
135
135
  input: {
136
- messages: [createUserMessage(options.prompt)],
136
+ messages: [...(options.inheritedMessages ?? []), createUserMessage(options.prompt)],
137
137
  workingDirectory: options.workingDirectory,
138
138
  taskStore: this.parentInput?.taskStore,
139
139
  runtimeToolOverrides: this.parentInput?.runtimeToolOverrides,
@@ -223,7 +223,7 @@ export class ArchivalManager {
223
223
 
224
224
  // 6. Dispose the workspace (idempotent — driver contract tolerates
225
225
  // already-disposed refs; a missing ref is a no-op).
226
- if (workspace && sub.workspaceRetention !== 'retain') {
226
+ if (workspace && sub.workspaceRetention === undefined) {
227
227
  try {
228
228
  const driver = this.deps.workspaceRegistry.get(workspace.meta.backend)
229
229
  await driver.dispose(workspace)
@@ -47,6 +47,12 @@ export interface WorkspaceInspection {
47
47
  exists: boolean
48
48
  currentRef: string
49
49
  isDirty: boolean
50
+ /**
51
+ * Whether the workspace moved off the commit it started on. Absent when the
52
+ * backend cannot tell; a caller deciding whether to keep it treats absent
53
+ * as `true`.
54
+ */
55
+ hasNewCommits?: boolean
50
56
  }
51
57
 
52
58
  /**
@@ -147,11 +147,19 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
147
147
  })
148
148
  }
149
149
 
150
+ // The commit the checkout starts on, so a later look can tell a child that
151
+ // committed from one that did not. Left out when it cannot be read: a ref
152
+ // without it is always treated as changed.
153
+ const baseCommit = await this.exec('git', ['-C', worktreePath, 'rev-parse', 'HEAD'])
154
+ .then((r) => r.stdout.trim() || undefined)
155
+ .catch(() => undefined)
156
+
150
157
  const meta: GitWorktreeBackendMeta = {
151
158
  backend: 'git-worktree',
152
159
  repoRoot,
153
160
  branch,
154
161
  worktreePath,
162
+ ...(baseCommit ? { baseCommit } : {}),
155
163
  }
156
164
 
157
165
  this.log.info('git-worktree created', {
@@ -202,6 +210,25 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
202
210
  [NAMZU.SESSION_ID]: ref.id,
203
211
  'namzu.session.path': owned.worktreePath,
204
212
  })
213
+ // A branch still on its starting commit holds nothing, so it goes with
214
+ // its checkout. Compare and delete under Git's ref lock: a branch that
215
+ // advanced after the worktree listing must keep its new commits.
216
+ if (owned.baseCommit !== undefined && registered.head === owned.baseCommit) {
217
+ await this.exec('git', [
218
+ '-C',
219
+ owned.repoRoot,
220
+ 'update-ref',
221
+ '--no-deref',
222
+ '-d',
223
+ `refs/heads/${owned.branch}`,
224
+ owned.baseCommit,
225
+ ]).catch((cause) =>
226
+ this.log.warn('git-worktree branch left behind', {
227
+ 'namzu.session.branch': owned.branch,
228
+ 'namzu.session.cause': cause instanceof Error ? cause.message : String(cause),
229
+ }),
230
+ )
231
+ }
205
232
  } catch (cause) {
206
233
  // Two concurrent disposals can both observe the entry before one wins.
207
234
  // Re-read after failure and accept only repository evidence that the
@@ -282,10 +309,13 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
282
309
  throw new WorkspaceBackendError({ op: 'inspect:status', kind: this.kind, cause })
283
310
  }
284
311
 
312
+ const baseCommit = owned.baseCommit
285
313
  return {
286
314
  exists: true,
287
315
  currentRef: entry.branch ?? entry.head ?? ref.meta.branch,
288
316
  isDirty: statusStdout.trim().length > 0,
317
+ // Unknown (no recorded start, or HEAD unreadable) reads as "has commits".
318
+ hasNewCommits: baseCommit === undefined || entry.head !== baseCommit,
289
319
  }
290
320
  }
291
321
 
@@ -329,6 +359,7 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
329
359
  repoRoot,
330
360
  branch,
331
361
  worktreePath,
362
+ ...(ref.meta.baseCommit ? { baseCommit: ref.meta.baseCommit } : {}),
332
363
  }
333
364
  } catch (cause) {
334
365
  throw new WorkspaceBackendError({ op: `${op}:ownership`, kind: this.kind, cause })
@@ -1,2 +1,7 @@
1
- export { SKILL_FRONTMATTER_KEYS, loadSkill, discoverSkills } from './loader.js'
1
+ export {
2
+ SKILL_FRONTMATTER_KEYS,
3
+ SKILL_FRONTMATTER_TOLERATED_KEYS,
4
+ loadSkill,
5
+ discoverSkills,
6
+ } from './loader.js'
2
7
  export { SkillCollisionError, SkillRegistry, resolveSkillChain } from './registry.js'
@@ -1,5 +1,6 @@
1
1
  import { readFile, readdir, stat } from 'node:fs/promises'
2
2
  import { basename, join } from 'node:path'
3
+ import type { ReasoningEffort } from '../types/provider/chat.js'
3
4
  import type {
4
5
  Skill,
5
6
  SkillDisclosureLevel,
@@ -31,8 +32,28 @@ export const SKILL_FRONTMATTER_KEYS: readonly string[] = Object.freeze([
31
32
  'invocation',
32
33
  'disable-model-invocation',
33
34
  'metadata',
35
+ 'when_to_use',
36
+ 'context',
37
+ 'agent',
38
+ 'model',
39
+ 'effort',
40
+ 'background',
34
41
  ])
35
42
 
43
+ /**
44
+ * The keys added after other tools' skills already used them. A value in one
45
+ * that this reader cannot take (`when_to_use: >-`, `model: [a, b]`) leaves the
46
+ * key absent instead of refusing the skill.
47
+ */
48
+ export const SKILL_FRONTMATTER_TOLERATED_KEYS: readonly string[] = [
49
+ 'when_to_use',
50
+ 'context',
51
+ 'agent',
52
+ 'model',
53
+ 'effort',
54
+ 'background',
55
+ ]
56
+
36
57
  const READS_SKILL_KEY = (key: string): boolean => SKILL_FRONTMATTER_KEYS.includes(key)
37
58
 
38
59
  /**
@@ -44,6 +65,7 @@ const READS_SKILL_KEY = (key: string): boolean => SKILL_FRONTMATTER_KEYS.include
44
65
  export const SKILL_FRONTMATTER_OPTIONS = {
45
66
  lists: ['allowed-tools'],
46
67
  readsKey: READS_SKILL_KEY,
68
+ tolerates: (key: string): boolean => SKILL_FRONTMATTER_TOLERATED_KEYS.includes(key),
47
69
  } as const
48
70
 
49
71
  /**
@@ -161,6 +183,27 @@ function toSkillMetadata(parsed: ParsedFrontmatter, dirPath: string): SkillMetad
161
183
  }
162
184
  }
163
185
 
186
+ // The fields that shape how the skill RUNS are read leniently, the other
187
+ // way round from the access fields above: a value this loader does not
188
+ // understand (`effort: 5000`, `context: sideways`) leaves the skill at its
189
+ // plain behaviour rather than refusing a skill written for another agent.
190
+ const whenToUse = scalarAt(values, 'when_to_use')?.trim()
191
+ if (whenToUse) skillMetadata.whenToUse = whenToUse
192
+
193
+ if (scalarAt(values, 'context') === 'fork') skillMetadata.context = 'fork'
194
+ const agent = scalarAt(values, 'agent')?.trim()
195
+ if (agent) skillMetadata.agent = agent
196
+ const model = scalarAt(values, 'model')?.trim()
197
+ if (model && model.toLowerCase() !== 'inherit') skillMetadata.model = model
198
+ const effort = scalarAt(values, 'effort')?.trim().toLowerCase()
199
+ if (effort && (REASONING_EFFORTS as readonly string[]).includes(effort)) {
200
+ skillMetadata.effort = effort as ReasoningEffort
201
+ }
202
+ const background = scalarAt(values, 'background')
203
+ if (background === 'true' || background === 'false') {
204
+ skillMetadata.background = background === 'true'
205
+ }
206
+
164
207
  const extra = mappingAt(values, 'metadata')
165
208
  if (extra && Object.keys(extra).length > 0) {
166
209
  skillMetadata.metadata = { ...extra }
@@ -169,6 +212,17 @@ function toSkillMetadata(parsed: ParsedFrontmatter, dirPath: string): SkillMetad
169
212
  return skillMetadata
170
213
  }
171
214
 
215
+ const REASONING_EFFORTS: readonly ReasoningEffort[] = [
216
+ 'none',
217
+ 'minimal',
218
+ 'low',
219
+ 'medium',
220
+ 'high',
221
+ 'xhigh',
222
+ 'max',
223
+ 'ultra',
224
+ ]
225
+
172
226
  const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
173
227
 
174
228
  function validateSkillName(name: string, dirPath: string): void {
@@ -1,11 +1,12 @@
1
1
  import { stat } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { RegistryCollisionError } from '../registry/collision.js'
4
- import type {
5
- Skill,
6
- SkillChain,
7
- SkillDisclosureLevel,
8
- SkillLoadResult,
4
+ import {
5
+ type Skill,
6
+ type SkillChain,
7
+ type SkillDisclosureLevel,
8
+ type SkillLoadResult,
9
+ skillModelDescription,
9
10
  } from '../types/skills/index.js'
10
11
  import { SCOPE_ATTRIBUTE } from '../utils/log/types.js'
11
12
  import { type Logger, resolveLogger } from '../utils/logger.js'
@@ -273,7 +274,7 @@ export class SkillRegistry {
273
274
  const skill = loaded.skill
274
275
  entries.push({
275
276
  registeredName,
276
- description: skill.metadata.description,
277
+ description: skillModelDescription(skill.metadata),
277
278
  location: join(skill.dirPath, SKILL_FILENAME),
278
279
  directory: skill.dirPath,
279
280
  ...(skill.metadata.allowedTools === undefined