@namzu/sdk 57.1.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 (240) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -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/connector/mcp/client.d.ts +8 -0
  14. package/dist/connector/mcp/client.d.ts.map +1 -1
  15. package/dist/connector/mcp/client.js +75 -2
  16. package/dist/connector/mcp/client.js.map +1 -1
  17. package/dist/constants/mcp/index.d.ts +2 -0
  18. package/dist/constants/mcp/index.d.ts.map +1 -1
  19. package/dist/constants/mcp/index.js +2 -0
  20. package/dist/constants/mcp/index.js.map +1 -1
  21. package/dist/constants/plugin/index.d.ts +2 -0
  22. package/dist/constants/plugin/index.d.ts.map +1 -1
  23. package/dist/constants/plugin/index.js +2 -0
  24. package/dist/constants/plugin/index.js.map +1 -1
  25. package/dist/manager/agent/lifecycle.d.ts +1 -1
  26. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  27. package/dist/manager/agent/lifecycle.js +25 -4
  28. package/dist/manager/agent/lifecycle.js.map +1 -1
  29. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  30. package/dist/manager/session/turn-recorder.js +2 -1
  31. package/dist/manager/session/turn-recorder.js.map +1 -1
  32. package/dist/persona/assembler.d.ts.map +1 -1
  33. package/dist/persona/assembler.js +2 -2
  34. package/dist/persona/assembler.js.map +1 -1
  35. package/dist/plugin/index.d.ts +1 -1
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/lifecycle.d.ts +8 -1
  38. package/dist/plugin/lifecycle.d.ts.map +1 -1
  39. package/dist/plugin/lifecycle.js +9 -2
  40. package/dist/plugin/lifecycle.js.map +1 -1
  41. package/dist/plugin/shell-hook-http.d.ts +39 -0
  42. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  43. package/dist/plugin/shell-hook-http.js +163 -0
  44. package/dist/plugin/shell-hook-http.js.map +1 -0
  45. package/dist/plugin/shell-hook.d.ts +93 -11
  46. package/dist/plugin/shell-hook.d.ts.map +1 -1
  47. package/dist/plugin/shell-hook.js +223 -39
  48. package/dist/plugin/shell-hook.js.map +1 -1
  49. package/dist/public-runtime.d.ts +2 -2
  50. package/dist/public-runtime.d.ts.map +1 -1
  51. package/dist/public-runtime.js +2 -2
  52. package/dist/public-runtime.js.map +1 -1
  53. package/dist/public-tools.d.ts +1 -0
  54. package/dist/public-tools.d.ts.map +1 -1
  55. package/dist/public-tools.js +2 -0
  56. package/dist/public-tools.js.map +1 -1
  57. package/dist/public-types.d.ts +1 -1
  58. package/dist/public-types.d.ts.map +1 -1
  59. package/dist/runtime/query/executor.d.ts +1 -0
  60. package/dist/runtime/query/executor.d.ts.map +1 -1
  61. package/dist/runtime/query/executor.js +34 -9
  62. package/dist/runtime/query/executor.js.map +1 -1
  63. package/dist/runtime/query/index.d.ts +2 -0
  64. package/dist/runtime/query/index.d.ts.map +1 -1
  65. package/dist/runtime/query/index.js +25 -1
  66. package/dist/runtime/query/index.js.map +1 -1
  67. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  68. package/dist/runtime/query/iteration/index.js +6 -2
  69. package/dist/runtime/query/iteration/index.js.map +1 -1
  70. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  71. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  72. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  73. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  74. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  75. package/dist/runtime/query/iteration/phases/compaction.js +6 -4
  76. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  77. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  78. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  80. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  82. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  83. package/dist/runtime/query/path-escalation.d.ts +2 -0
  84. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  85. package/dist/runtime/query/path-escalation.js +7 -4
  86. package/dist/runtime/query/path-escalation.js.map +1 -1
  87. package/dist/runtime/query/plugin-hooks.js +2 -2
  88. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  89. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  90. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  91. package/dist/runtime/query/prepare-turn.js +25 -0
  92. package/dist/runtime/query/prepare-turn.js.map +1 -1
  93. package/dist/runtime/query/review-policy.d.ts +3 -2
  94. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  95. package/dist/runtime/query/review-policy.js +32 -13
  96. package/dist/runtime/query/review-policy.js.map +1 -1
  97. package/dist/session/retention/archive.js +1 -1
  98. package/dist/session/retention/archive.js.map +1 -1
  99. package/dist/session/workspace/driver.d.ts +6 -0
  100. package/dist/session/workspace/driver.d.ts.map +1 -1
  101. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  102. package/dist/session/workspace/git-worktree.js +28 -0
  103. package/dist/session/workspace/git-worktree.js.map +1 -1
  104. package/dist/skills/index.d.ts +1 -1
  105. package/dist/skills/index.d.ts.map +1 -1
  106. package/dist/skills/index.js +1 -1
  107. package/dist/skills/index.js.map +1 -1
  108. package/dist/skills/loader.d.ts +7 -0
  109. package/dist/skills/loader.d.ts.map +1 -1
  110. package/dist/skills/loader.js +53 -0
  111. package/dist/skills/loader.js.map +1 -1
  112. package/dist/skills/registry.d.ts +1 -1
  113. package/dist/skills/registry.d.ts.map +1 -1
  114. package/dist/skills/registry.js +2 -1
  115. package/dist/skills/registry.js.map +1 -1
  116. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  117. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  118. package/dist/tools/builtins/bash-cwd.js +127 -0
  119. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  120. package/dist/tools/builtins/bash.d.ts.map +1 -1
  121. package/dist/tools/builtins/bash.js +41 -6
  122. package/dist/tools/builtins/bash.js.map +1 -1
  123. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  124. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  125. package/dist/tools/builtins/bounded-file-read.js +67 -0
  126. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  127. package/dist/tools/builtins/grep.d.ts +6 -1
  128. package/dist/tools/builtins/grep.d.ts.map +1 -1
  129. package/dist/tools/builtins/grep.js +187 -24
  130. package/dist/tools/builtins/grep.js.map +1 -1
  131. package/dist/tools/builtins/read-file.d.ts +1 -0
  132. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  133. package/dist/tools/builtins/read-file.js +113 -28
  134. package/dist/tools/builtins/read-file.js.map +1 -1
  135. package/dist/tools/builtins/read-media.d.ts +68 -0
  136. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  137. package/dist/tools/builtins/read-media.js +397 -0
  138. package/dist/tools/builtins/read-media.js.map +1 -0
  139. package/dist/tools/builtins/search-tools.js +1 -1
  140. package/dist/tools/builtins/search-tools.js.map +1 -1
  141. package/dist/tools/builtins/skill.d.ts +4 -2
  142. package/dist/tools/builtins/skill.d.ts.map +1 -1
  143. package/dist/tools/builtins/skill.js +61 -2
  144. package/dist/tools/builtins/skill.js.map +1 -1
  145. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  146. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  147. package/dist/tools/builtins/sleep-guard.js +33 -0
  148. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  149. package/dist/tools/builtins/view-image.d.ts +23 -1
  150. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  151. package/dist/tools/builtins/view-image.js +66 -58
  152. package/dist/tools/builtins/view-image.js.map +1 -1
  153. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  154. package/dist/tools/file-read-tracker.js +31 -0
  155. package/dist/tools/file-read-tracker.js.map +1 -1
  156. package/dist/toolsets/manager.js +3 -3
  157. package/dist/toolsets/manager.js.map +1 -1
  158. package/dist/types/agent/query.d.ts +2 -0
  159. package/dist/types/agent/query.d.ts.map +1 -1
  160. package/dist/types/agent/task.d.ts +6 -2
  161. package/dist/types/agent/task.d.ts.map +1 -1
  162. package/dist/types/agent/task.js.map +1 -1
  163. package/dist/types/connector/mcp.d.ts +46 -0
  164. package/dist/types/connector/mcp.d.ts.map +1 -1
  165. package/dist/types/hitl/index.d.ts +1 -1
  166. package/dist/types/plugin/index.d.ts +72 -5
  167. package/dist/types/plugin/index.d.ts.map +1 -1
  168. package/dist/types/plugin/index.js +12 -1
  169. package/dist/types/plugin/index.js.map +1 -1
  170. package/dist/types/session/sub-session.d.ts +6 -2
  171. package/dist/types/session/sub-session.d.ts.map +1 -1
  172. package/dist/types/skills/index.d.ts +36 -0
  173. package/dist/types/skills/index.d.ts.map +1 -1
  174. package/dist/types/skills/index.js +9 -0
  175. package/dist/types/skills/index.js.map +1 -1
  176. package/dist/types/tool/index.d.ts +22 -0
  177. package/dist/types/tool/index.d.ts.map +1 -1
  178. package/dist/types/tool/index.js.map +1 -1
  179. package/dist/types/workspace/ref.d.ts +2 -0
  180. package/dist/types/workspace/ref.d.ts.map +1 -1
  181. package/dist/utils/frontmatter.d.ts +12 -0
  182. package/dist/utils/frontmatter.d.ts.map +1 -1
  183. package/dist/utils/frontmatter.js +6 -0
  184. package/dist/utils/frontmatter.js.map +1 -1
  185. package/package.json +1 -1
  186. package/src/agents/QueryAgent.ts +2 -0
  187. package/src/agents/file-definitions.ts +126 -6
  188. package/src/authorization/protected-paths.ts +30 -2
  189. package/src/connector/mcp/client.ts +87 -1
  190. package/src/constants/mcp/index.ts +3 -0
  191. package/src/constants/plugin/index.ts +2 -0
  192. package/src/manager/agent/lifecycle.ts +24 -5
  193. package/src/manager/session/turn-recorder.ts +2 -1
  194. package/src/persona/assembler.ts +2 -2
  195. package/src/plugin/index.ts +5 -0
  196. package/src/plugin/lifecycle.ts +16 -2
  197. package/src/plugin/shell-hook-http.ts +181 -0
  198. package/src/plugin/shell-hook.ts +386 -89
  199. package/src/public-runtime.ts +2 -0
  200. package/src/public-tools.ts +2 -0
  201. package/src/public-types.ts +5 -0
  202. package/src/runtime/query/executor.ts +40 -10
  203. package/src/runtime/query/index.ts +33 -0
  204. package/src/runtime/query/iteration/index.ts +11 -3
  205. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  206. package/src/runtime/query/iteration/phases/compaction.ts +21 -4
  207. package/src/runtime/query/iteration/phases/context.ts +2 -0
  208. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  209. package/src/runtime/query/path-escalation.ts +11 -5
  210. package/src/runtime/query/plugin-hooks.ts +2 -2
  211. package/src/runtime/query/prepare-turn.ts +33 -0
  212. package/src/runtime/query/review-policy.ts +45 -16
  213. package/src/session/retention/archive.ts +1 -1
  214. package/src/session/workspace/driver.ts +6 -0
  215. package/src/session/workspace/git-worktree.ts +31 -0
  216. package/src/skills/index.ts +6 -1
  217. package/src/skills/loader.ts +54 -0
  218. package/src/skills/registry.ts +7 -6
  219. package/src/tools/builtins/bash-cwd.ts +156 -0
  220. package/src/tools/builtins/bash.ts +79 -34
  221. package/src/tools/builtins/bounded-file-read.ts +80 -0
  222. package/src/tools/builtins/grep.ts +198 -28
  223. package/src/tools/builtins/read-file.ts +151 -30
  224. package/src/tools/builtins/read-media.ts +483 -0
  225. package/src/tools/builtins/search-tools.ts +1 -1
  226. package/src/tools/builtins/skill.ts +91 -2
  227. package/src/tools/builtins/sleep-guard.ts +31 -0
  228. package/src/tools/builtins/view-image.ts +74 -62
  229. package/src/tools/file-read-tracker.ts +30 -0
  230. package/src/toolsets/manager.ts +3 -3
  231. package/src/types/agent/query.ts +2 -0
  232. package/src/types/agent/task.ts +6 -2
  233. package/src/types/connector/mcp.ts +48 -0
  234. package/src/types/hitl/index.ts +1 -1
  235. package/src/types/plugin/index.ts +102 -16
  236. package/src/types/session/sub-session.ts +6 -2
  237. package/src/types/skills/index.ts +48 -0
  238. package/src/types/tool/index.ts +22 -0
  239. package/src/types/workspace/ref.ts +2 -0
  240. package/src/utils/frontmatter.ts +21 -0
@@ -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,
@@ -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
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The directory a `bash` call starts in, carried from one call to the next.
3
+ *
4
+ * A model that runs `cd packages/app` expects the next call to be there; shell
5
+ * state (variables, functions, aliases) does not carry over, but the directory
6
+ * does. After a foreground command the shell writes its physical directory to a
7
+ * private file, and the next call of the same session starts there.
8
+ *
9
+ * Two limits keep this from widening what a command can reach. The new
10
+ * directory has to lie inside the turn's roots (the working directory, the
11
+ * directories added for the session, any path this call was approved for); a
12
+ * command that leaves them is put back at the session's working directory and
13
+ * the result says so. And a directory that no longer exists is dropped without
14
+ * ceremony. Nothing here applies to a sandboxed command or to Windows shells,
15
+ * whose paths a host process cannot use as they are printed.
16
+ */
17
+
18
+ import { randomUUID } from 'node:crypto'
19
+ import { statSync } from 'node:fs'
20
+ import { rm } from 'node:fs/promises'
21
+ import { tmpdir } from 'node:os'
22
+ import { join } from 'node:path'
23
+
24
+ import { type CommandShell, hostCommandShell } from '../command-shell.js'
25
+ import { pathOutsideRoots, toolRoots } from '../paths.js'
26
+ import { readBoundedFile } from './bounded-file-read.js'
27
+
28
+ interface Carried {
29
+ /** The working directory the session started with; the fallback. */
30
+ readonly base: string
31
+ cwd: string
32
+ }
33
+
34
+ /** Sessions remembered; the oldest is forgotten first. */
35
+ const MAX_SESSIONS = 256
36
+ const carried = new Map<string, Carried>()
37
+
38
+ /** The notice appended to a result when the command left the allowed directories. */
39
+ export function cwdResetNotice(base: string): string {
40
+ return `Shell cwd was reset to ${base}`
41
+ }
42
+
43
+ /** Single-quote `text` for a POSIX shell. */
44
+ function shellQuote(text: string): string {
45
+ return `'${text.replace(/'/g, `'\\''`)}'`
46
+ }
47
+
48
+ /**
49
+ * Runs `command` unchanged and records the directory it ended in. The exit
50
+ * status is the command's own. A command that ends the shell itself (`exit`)
51
+ * records nothing, so the directory stays as it was.
52
+ */
53
+ export function withCwdCapture(command: string, file: string): string {
54
+ return `eval ${shellQuote(command)}\n__namzu_status=$?\npwd -P >| ${shellQuote(file)} 2>/dev/null\nexit $__namzu_status`
55
+ }
56
+
57
+ export function newCwdFile(): string {
58
+ return join(tmpdir(), `namzu-cwd-${randomUUID()}`)
59
+ }
60
+
61
+ /** Where this session's next foreground call starts. Synchronous: the spawn must not wait on it. */
62
+ export function startingDirectory(sessionId: string, base: string): string {
63
+ const entry = carried.get(sessionId)
64
+ if (entry === undefined) return base
65
+ if (entry.base !== base) {
66
+ carried.delete(sessionId)
67
+ return base
68
+ }
69
+ try {
70
+ if (statSync(entry.cwd).isDirectory()) return entry.cwd
71
+ } catch {
72
+ // Removed or unreadable: start again from the base.
73
+ }
74
+ entry.cwd = base
75
+ return base
76
+ }
77
+
78
+ /**
79
+ * The cwd used by bash execution and host authorization. Hosts must resolve it
80
+ * for each call, not cache the session's initial directory. Sandboxed commands
81
+ * (including sandbox escapes) and Windows never use the carried POSIX cwd.
82
+ */
83
+ export function effectiveBashWorkingDirectory(
84
+ sessionId: string | undefined,
85
+ workingDirectory: string,
86
+ options: {
87
+ sandboxed?: boolean
88
+ shell?: Pick<CommandShell, 'path' | 'windows'>
89
+ } = {},
90
+ ): string {
91
+ const shell = options.shell ?? hostCommandShell()
92
+ if (options.sandboxed || sessionId === undefined || shell.windows || shell.path === undefined)
93
+ return workingDirectory
94
+ return startingDirectory(sessionId, workingDirectory)
95
+ }
96
+
97
+ /**
98
+ * Reads what the finished command recorded and keeps it for the next call.
99
+ * Returns the notice to append when the directory was outside the roots.
100
+ */
101
+ export async function recordDirectory(
102
+ sessionId: string,
103
+ file: string,
104
+ context: Parameters<typeof toolRoots>[0],
105
+ ): Promise<string | undefined> {
106
+ let reported: string
107
+ try {
108
+ reported = (await readBoundedFile(file, 64 * 1024)).toString('utf8').trim()
109
+ } catch {
110
+ return undefined
111
+ } finally {
112
+ await rm(file, { force: true })
113
+ }
114
+ if (reported === '') return undefined
115
+ const base = context.workingDirectory
116
+ const outside = await pathOutsideRoots(toolRoots(context), reported)
117
+ const next = outside === undefined ? reported : base
118
+ carried.delete(sessionId)
119
+ if (next !== base) {
120
+ carried.set(sessionId, { base, cwd: next })
121
+ if (carried.size > MAX_SESSIONS) {
122
+ const oldest = carried.keys().next().value
123
+ if (oldest !== undefined) carried.delete(oldest)
124
+ }
125
+ }
126
+ return outside === undefined ? undefined : cwdResetNotice(base)
127
+ }
128
+
129
+ /** For tests: forget every session. */
130
+ export function forgetCarriedDirectories(): void {
131
+ carried.clear()
132
+ }
133
+
134
+ // Enumerable symbol crosses the executor's context copies, never the model's
135
+ // JSON input. No public ToolContext field or caller-authored cwd is trusted.
136
+ const REVIEWED_CWD = Symbol('namzu.bash.reviewed-cwd')
137
+
138
+ export function pinBashWorkingDirectory(context: object, cwd: string): void {
139
+ Object.defineProperty(context, REVIEWED_CWD, { value: cwd, enumerable: true, configurable: true })
140
+ }
141
+
142
+ export function bashWorkingDirectoryForExecution(context: {
143
+ sessionId?: string
144
+ workingDirectory: string
145
+ sandbox?: unknown
146
+ }): string {
147
+ const current = effectiveBashWorkingDirectory(context.sessionId, context.workingDirectory, {
148
+ sandboxed: context.sandbox !== undefined,
149
+ })
150
+ const reviewed = (context as { [REVIEWED_CWD]?: string })[REVIEWED_CWD]
151
+ if (reviewed !== undefined && current !== reviewed)
152
+ throw new Error(
153
+ 'Shell working directory changed after this call was reviewed. Nothing ran. Retry the call so its paths can be checked in the current directory.',
154
+ )
155
+ return reviewed ?? current
156
+ }