@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -4,6 +4,30 @@ import { type FunctionHookRegistration, validateFunctionHookTarget } from "./fun
4
4
 
5
5
  export type TaggedFunctionHookHandler = (...args: unknown[]) => Promise<unknown>;
6
6
 
7
+ // Host SDK lifecycle observers publish the accepted run's bookkeeping. They do
8
+ // not execute user work and must not consume provider-attempt replay authority.
9
+ const sdkLifecycleObservers = new WeakSet<(...args: never[]) => unknown>();
10
+ const providerRequestHandlers = new WeakMap<(...args: never[]) => unknown, string>();
11
+
12
+ /** Host-owned provider adapters must not consume replay authority for other providers. */
13
+ export function tagProviderRequestHandler<T extends (...args: never[]) => unknown>(provider: string, handler: T): T {
14
+ providerRequestHandlers.set(handler, provider);
15
+ return handler;
16
+ }
17
+
18
+ export function getProviderRequestHandlerProvider(handler: (...args: never[]) => unknown): string | undefined {
19
+ return providerRequestHandlers.get(handler);
20
+ }
21
+
22
+ export function tagSdkLifecycleObserver<T extends (...args: never[]) => unknown>(handler: T): T {
23
+ sdkLifecycleObservers.add(handler);
24
+ return handler;
25
+ }
26
+
27
+ export function isSdkLifecycleObserver(handler: (...args: never[]) => unknown): boolean {
28
+ return sdkLifecycleObservers.has(handler);
29
+ }
30
+
7
31
  const registrations = new WeakMap<TaggedFunctionHookHandler, FunctionHookRegistration>();
8
32
  const registrationOrders = new WeakMap<(...args: never[]) => unknown, number>();
9
33
 
@@ -21,6 +45,9 @@ export function wrapExtensionHandlerRegistration<T extends (...args: never[]) =>
21
45
  ): T {
22
46
  const registered = ((...args: never[]) => handler(...args)) as T;
23
47
  registrationOrders.set(registered, registrationOrder);
48
+ if (sdkLifecycleObservers.has(handler)) sdkLifecycleObservers.add(registered);
49
+ const provider = providerRequestHandlers.get(handler);
50
+ if (provider !== undefined) providerRequestHandlers.set(registered, provider);
24
51
  return registered;
25
52
  }
26
53
 
@@ -51,6 +51,8 @@ import {
51
51
  import {
52
52
  getExtensionHandlerRegistrationOrder,
53
53
  getFunctionHookRegistration,
54
+ getProviderRequestHandlerProvider,
55
+ isSdkLifecycleObserver,
54
56
  readConstrainedFunctionHookFile,
55
57
  } from "./function-hooks-internal";
56
58
  import type {
@@ -325,6 +327,7 @@ export class ExtensionRunner {
325
327
  #getArtifactRangeFn: ExtensionContextActions["getArtifactRange"] = undefined;
326
328
 
327
329
  #getJobsFn: ExtensionContextActions["getJobs"] = undefined;
330
+ #getProjectProgressFn: ExtensionContextActions["getProjectProgress"] = undefined;
328
331
  #onJobFoldFn: ExtensionContextActions["onJobFold"] = undefined;
329
332
  #onSessionEventFn: ExtensionContextActions["onSessionEvent"] = undefined;
330
333
  #sdkControlFn: ExtensionContextActions["sdkControl"] = undefined;
@@ -482,6 +485,7 @@ export class ExtensionRunner {
482
485
  this.#getArtifactRangeFn = contextActions.getArtifactRange;
483
486
 
484
487
  this.#getJobsFn = contextActions.getJobs;
488
+ this.#getProjectProgressFn = contextActions.getProjectProgress;
485
489
  this.#onJobFoldFn = contextActions.onJobFold;
486
490
  this.#onSessionEventFn = contextActions.onSessionEvent;
487
491
  this.#sdkControlFn = contextActions.sdkControl;
@@ -848,6 +852,7 @@ export class ExtensionRunner {
848
852
  getArtifactRange: (id, offset, length) => this.#getArtifactRangeFn?.(id, offset, length),
849
853
 
850
854
  getJobs: () => this.#getJobsFn?.(),
855
+ ...(this.#getProjectProgressFn ? { getProjectProgress: this.#getProjectProgressFn } : {}),
851
856
  onJobFold: listener => this.#onJobFoldFn?.(listener) ?? (() => {}),
852
857
  onSessionEvent: listener => this.#onSessionEventFn?.(listener) ?? (() => {}),
853
858
  sdkControl: (operation, input) => this.#sdkControlFn?.(operation, input),
@@ -867,6 +872,7 @@ export class ExtensionRunner {
867
872
  ...(this.#getExtensionsFn ? ["getExtensions"] : []),
868
873
  ...(this.#getArtifactRangeFn ? ["getArtifactRange"] : []),
869
874
  ...(this.#getJobsFn ? ["getJobs"] : []),
875
+ ...(this.#getProjectProgressFn ? ["getProjectProgress"] : []),
870
876
  ...(this.#onJobFoldFn ? ["onJobFold"] : []),
871
877
  ...(this.#sdkControlFn ? ["sdkControl"] : []),
872
878
  ...(this.#invokeSkillFn ? ["invokeSkill"] : []),
@@ -914,6 +920,13 @@ export class ExtensionRunner {
914
920
  const wildcard = this.#handlersByEvent.get("*") ?? [];
915
921
  const matches: IndexedHandler[] = [];
916
922
  for (const indexed of [...exact, ...wildcard]) {
923
+ const provider = getProviderRequestHandlerProvider(indexed.handler);
924
+ if (
925
+ event.type === "before_provider_request" &&
926
+ provider !== undefined &&
927
+ this.createContext().model?.provider !== provider
928
+ )
929
+ continue;
917
930
  const registration = getFunctionHookRegistration(indexed.handler);
918
931
  if (!registration) {
919
932
  if (includeLegacyToolHandlers && indexed.registrationOrder >= 0 && exact.includes(indexed))
@@ -1072,7 +1085,8 @@ export class ExtensionRunner {
1072
1085
  if (handlers.length === 0) return { action: "continue", event };
1073
1086
  const includesLegacy = handlers.some(indexed => getFunctionHookRegistration(indexed.handler) === undefined);
1074
1087
  if (includesLegacy) this.#requireScopeOrFailClosed(options.scope, event.type);
1075
- this.#markAttemptExecuted(options.scope);
1088
+ if (handlers.some(indexed => event.type !== "agent_start" || !isSdkLifecycleObserver(indexed.handler)))
1089
+ this.#markAttemptExecuted(options.scope);
1076
1090
  const functionHookDepth = this.#functionHookDepth.getStore() ?? 0;
1077
1091
  if (functionHookDepth >= 16) {
1078
1092
  const reason = "Function hook re-entry depth exceeded";
@@ -49,6 +49,7 @@ import type { ExecOptions, ExecResult } from "../../exec/exec";
49
49
  import type { CustomEditor } from "../../modes/components/custom-editor";
50
50
  import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow-gate-broker";
51
51
  import type { Theme } from "../../modes/theme/theme";
52
+ import type { ProjectProgressSnapshot } from "../../progress/progress-contract";
52
53
  import type { AgentSessionEventListener } from "../../session/agent-session";
53
54
  import type {
54
55
  ClientBridge,
@@ -538,6 +539,8 @@ export interface ExtensionContext {
538
539
  | Promise<{ bytes: Uint8Array; totalBytes: number } | undefined>;
539
540
 
540
541
  getJobs(): unknown;
542
+ /** Read-only project progress snapshot (`session.progress`), derived from durable session state. */
543
+ getProjectProgress?(): Promise<ProjectProgressSnapshot>;
541
544
  /** Subscribe to foreground-wait folds (`bash_folded`). Returns an unsubscribe function. */
542
545
  onJobFold?(listener: (event: JobFoldEvent) => void): () => void;
543
546
  /** Observe session events synchronously in producer order. Returns an unsubscribe function. */
@@ -764,6 +767,8 @@ export interface AgentStartEvent extends SharedAgentStartEvent {
764
767
  sdkRunToken?: string;
765
768
  /** Complete set of SDK queue owners consumed together by this run. */
766
769
  sdkRunTokens?: string[];
770
+ /** Internal public lifecycle boundary; accepted retries with a suppressed predecessor end retain it. */
771
+ lifecycleScope?: AttemptScope;
767
772
  }
768
773
 
769
774
  /** Fired when an agent run fails before emitting agent_end. The error is the
@@ -1723,6 +1728,8 @@ export interface ExtensionContextActions {
1723
1728
  | undefined
1724
1729
  | Promise<{ bytes: Uint8Array; totalBytes: number } | undefined>;
1725
1730
  getJobs?: () => unknown;
1731
+ /** Read-only project progress snapshot backing the `session.progress` SDK query. */
1732
+ getProjectProgress?: () => Promise<ProjectProgressSnapshot>;
1726
1733
  /** Subscribe to foreground-wait folds so the SDK host can publish `bash_folded`. Returns an unsubscribe. */
1727
1734
  onJobFold?: (listener: (event: JobFoldEvent) => void) => () => void;
1728
1735
  /** Ordered, synchronous AgentSession subscription, independent of async extension hooks. */
@@ -6,7 +6,7 @@ import { GjcPluginLoadError, type GjcPluginMcpManifestEntry } from "./types";
6
6
  /**
7
7
  * Shared MCP security policy applied at BOTH install validation and runtime
8
8
  * connect for third-party plugin-bundle MCP servers. Defaults are deny-first:
9
- * HTTPS only, no private/loopback/link-local/metadata endpoints, stdio confined
9
+ * HTTPS only, no private/loopback/link-local/CGNAT/metadata endpoints, stdio confined
10
10
  * to the plugin root.
11
11
  */
12
12
 
@@ -34,6 +34,7 @@ export function isDeniedIpv4(host: string): boolean {
34
34
  if (a === 172 && b >= 16 && b <= 31) return true; // private 172.16.0.0/12
35
35
  if (a === 192 && b === 168) return true; // private 192.168.0.0/16
36
36
  if (a === 169 && b === 254) return true; // link-local 169.254.0.0/16 (incl 169.254.169.254 metadata)
37
+ if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT 100.64.0.0/10
37
38
  if (a === 0) return true; // 0.0.0.0/8 unspecified/this-network
38
39
  if (a >= 224) return true; // multicast/reserved 224.0.0.0/4 and 240.0.0.0/4
39
40
  return false;
@@ -149,22 +149,82 @@ const initialTemporaryRoots = initialProcessEnvironment.then(async environment =
149
149
  }
150
150
  return [...roots];
151
151
  });
152
- const initialNodeAuthorities = initialProcessEnvironment.then(async environment => {
153
- const authorities = new Map<string, string>();
152
+
153
+ /** Startup identity of a file: the object `hashStableFile` must still be reading. */
154
+ export interface StableFileIdentity {
155
+ dev: number;
156
+ ino: number;
157
+ size: number;
158
+ mtimeMs: number;
159
+ ctimeMs: number;
160
+ }
161
+
162
+ /**
163
+ * Startup identity of every canonical `node` on PATH, keyed by realpath. Only
164
+ * `stat` runs at startup; the digest is computed on first use from a descriptor
165
+ * whose identity must still match this snapshot, so a later replacement cannot
166
+ * become launch authority.
167
+ */
168
+ const initialNodeIdentities = initialProcessEnvironment.then(async environment => {
169
+ const identities = new Map<string, StableFileIdentity>();
154
170
  const temporaryRoots = await initialTemporaryRoots;
155
171
  for (const pathEntry of (environment.get("PATH") ?? "").split(path.delimiter).filter(path.isAbsolute)) {
156
172
  const lexical = path.join(pathEntry, process.platform === "win32" ? "node.exe" : "node");
157
173
  try {
158
174
  const real = await fs.realpath(lexical);
175
+ // A temporary root is writable by other local users and by checkouts the user
176
+ // never vetted, so a `node` resolved there never becomes launch authority.
159
177
  if (temporaryRoots.some(root => isWithin(root, real))) continue;
160
- authorities.set(real, await hashStableFile(real, "Initial Node executable", MCP_LAUNCHER_MAX_BYTES));
178
+ if (identities.has(real)) continue;
179
+ const stat = await fs.stat(real);
180
+ identities.set(real, {
181
+ dev: stat.dev,
182
+ ino: stat.ino,
183
+ size: stat.size,
184
+ mtimeMs: stat.mtimeMs,
185
+ ctimeMs: stat.ctimeMs,
186
+ });
161
187
  } catch {
162
- // Missing or unstable startup candidates do not become authority.
188
+ // Missing startup candidates do not become authority.
163
189
  }
164
190
  }
165
- return authorities;
191
+ return identities;
166
192
  });
167
193
 
194
+ /**
195
+ * First-use digests of startup Node candidates, keyed by realpath. Successful and
196
+ * in-flight digests stay memoized; a failed attempt is evicted so a transient error
197
+ * (e.g. EMFILE) can be retried against the same startup identity.
198
+ */
199
+ const initialNodeHashes = new Map<string, Promise<string | undefined>>();
200
+
201
+ /**
202
+ * Digest of a `node` that was on PATH at startup, or `undefined` when the path
203
+ * was not a startup candidate or no longer is the startup file object.
204
+ */
205
+ export function getInitialNodeHash(realPath: string): Promise<string | undefined> {
206
+ const cached = initialNodeHashes.get(realPath);
207
+ if (cached) return cached;
208
+
209
+ const attempt = initialNodeIdentities.then(async identities => {
210
+ const identity = identities.get(realPath);
211
+ if (!identity) return undefined;
212
+ try {
213
+ return await hashStableFile(realPath, "Initial Node executable", MCP_LAUNCHER_MAX_BYTES, identity);
214
+ } catch {
215
+ // Do not memoize failed attempts to allow retries on transient errors
216
+ // (e.g., EMFILE on file open/read). Remove the pending entry so the next
217
+ // call can retry the operation with the same startup identity.
218
+ initialNodeHashes.delete(realPath);
219
+ return undefined;
220
+ }
221
+ });
222
+ // Memoize the in-flight attempt so only one concurrent operation runs, but
223
+ // the entry may be deleted if the operation fails (to allow retry).
224
+ initialNodeHashes.set(realPath, attempt);
225
+ return attempt;
226
+ }
227
+
168
228
  async function snapshotExistingFile(filePath: string): Promise<FileSnapshot | null> {
169
229
  try {
170
230
  const stat = await fs.stat(filePath);
@@ -416,11 +476,28 @@ const STABLE_HASH_CHUNK_BYTES = 1024 * 1024;
416
476
  * on PATH; buffering each interpreter (~120 MiB) held its full bytes in the
417
477
  * session process until exit.
418
478
  */
419
- export async function hashStableFile(filePath: string, label: string, maxBytes: number): Promise<string> {
479
+ export async function hashStableFile(
480
+ filePath: string,
481
+ label: string,
482
+ maxBytes: number,
483
+ expected?: StableFileIdentity,
484
+ ): Promise<string> {
420
485
  const handle = await fs.open(filePath, fs.constants.O_RDONLY);
421
486
  try {
422
487
  const before = await handle.stat();
423
488
  if (!before.isFile()) throw new Error(`${label} is not a regular file`);
489
+ // Checked on the opened descriptor, so the digest belongs to the expected
490
+ // file object even if the path is swapped around the open.
491
+ if (
492
+ expected &&
493
+ (before.dev !== expected.dev ||
494
+ before.ino !== expected.ino ||
495
+ before.size !== expected.size ||
496
+ before.mtimeMs !== expected.mtimeMs ||
497
+ before.ctimeMs !== expected.ctimeMs)
498
+ ) {
499
+ throw new Error(`${label} is not the expected file`);
500
+ }
424
501
  if (before.size > maxBytes) throw new Error(`${label} exceeds its byte limit`);
425
502
  const hash = createHash("sha256");
426
503
  const chunk = Buffer.allocUnsafe(STABLE_HASH_CHUNK_BYTES);
@@ -515,7 +592,7 @@ async function isInitialManagedNodeLauncherPath(
515
592
  const real = await fs.realpath(executablePath);
516
593
  if (untrustedRoots.some(root => isWithin(root, real))) return false;
517
594
  if ((await initialTemporaryRoots).some(root => isWithin(root, real))) return false;
518
- const expected = (await initialNodeAuthorities).get(real);
595
+ const expected = await getInitialNodeHash(real);
519
596
  if (!expected) return false;
520
597
  return (await hashStableFile(real, "Initial Node executable", MCP_LAUNCHER_MAX_BYTES)) === expected;
521
598
  }
@@ -671,7 +748,7 @@ async function prepareVerifiedStdioLaunch(input: {
671
748
  true,
672
749
  );
673
750
  const launcherReal = await fs.realpath(input.launcherPath);
674
- const expectedLauncherHash = (await initialNodeAuthorities).get(launcherReal);
751
+ const expectedLauncherHash = await getInitialNodeHash(launcherReal);
675
752
  if (!expectedLauncherHash || sha256(launcherBytes) !== expectedLauncherHash) {
676
753
  throw new Error("Plugin MCP Node interpreter drifted from startup authority");
677
754
  }
@@ -12,6 +12,7 @@ import {
12
12
  } from "@gajae-code/utils";
13
13
  import { withFileLock } from "../../config/file-lock";
14
14
  import { extractPackageName, parsePluginSpec } from "./parser";
15
+ import { resolvePluginLinkPath } from "./plugin-link-path";
15
16
  import type {
16
17
  DoctorCheck,
17
18
  DoctorOptions,
@@ -535,11 +536,11 @@ export class PluginManager {
535
536
 
536
537
  await this.#ensurePluginsDir();
537
538
 
538
- const linkPath = path.join(getPluginsNodeModules(), pkg.name);
539
+ const linkPath = resolvePluginLinkPath(getPluginsNodeModules(), pkg.name);
539
540
 
540
541
  // Handle scoped packages
541
542
  if (pkg.name.startsWith("@")) {
542
- const scopeDir = path.join(getPluginsNodeModules(), pkg.name.split("/")[0]);
543
+ const scopeDir = path.dirname(linkPath);
543
544
  await fs.promises.mkdir(scopeDir, { recursive: true });
544
545
  }
545
546
 
@@ -87,6 +87,21 @@ export async function cachePlugin(
87
87
  return targetPath;
88
88
  }
89
89
 
90
+ /** Write a regular file. A symlink at the destination is removed, not followed. */
91
+ export async function writeCachedRegularFile(targetPath: string, content: string): Promise<void> {
92
+ try {
93
+ const stat = await fs.lstat(targetPath);
94
+ if (stat.isSymbolicLink()) {
95
+ await fs.unlink(targetPath);
96
+ } else if (!stat.isFile()) {
97
+ throw new Error("unsafe cache artifact");
98
+ }
99
+ } catch (error) {
100
+ if (!isEnoent(error)) throw error;
101
+ }
102
+ await Bun.write(targetPath, content);
103
+ }
104
+
90
105
  export async function stageCachedPlugin(sourcePath: string, stagingPath: string): Promise<string> {
91
106
  await fs.rm(stagingPath, { recursive: true, force: true });
92
107
  await fs.cp(sourcePath, stagingPath, { recursive: true, errorOnExist: true, verbatimSymlinks: true });
@@ -19,7 +19,13 @@ import {
19
19
  } from "@gajae-code/natives";
20
20
  import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
21
21
  import { withFileLock } from "../../../config/file-lock";
22
- import { cachePlugin, getCachedPluginPath, inspectCachedPlugin, stageCachedPlugin } from "./cache";
22
+ import {
23
+ cachePlugin,
24
+ getCachedPluginPath,
25
+ inspectCachedPlugin,
26
+ stageCachedPlugin,
27
+ writeCachedRegularFile,
28
+ } from "./cache";
23
29
  import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
24
30
  import {
25
31
  addInstalledPlugin,
@@ -1430,12 +1436,14 @@ export class MarketplaceManager {
1430
1436
  if (!pathIsWithin(cachePath, sourcePath)) {
1431
1437
  throw new Error(`Plugin "${entry.name}" lspServers path escapes the plugin directory`);
1432
1438
  }
1439
+ const sourceStat = await fs.lstat(sourcePath);
1440
+ if (sourceStat.isSymbolicLink()) throw new Error(`Plugin "${entry.name}" lspServers path is a symlink`);
1433
1441
  const content = await Bun.file(sourcePath).text();
1434
- await Bun.write(targetPath, content);
1442
+ await writeCachedRegularFile(targetPath, content);
1435
1443
  return;
1436
1444
  }
1437
1445
 
1438
- await Bun.write(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\n`);
1446
+ await writeCachedRegularFile(targetPath, `${JSON.stringify({ servers: lspServers }, null, 2)}\n`);
1439
1447
  }
1440
1448
 
1441
1449
  /**
@@ -0,0 +1,12 @@
1
+ import path from "node:path";
2
+
3
+ /** Resolve a package.json name under the plugins node_modules root, or throw. */
4
+ export function resolvePluginLinkPath(root: string, name: string): string {
5
+ const base = path.resolve(root);
6
+ const destination = path.resolve(base, name);
7
+ const relative = path.relative(base, destination);
8
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
9
+ throw new Error(`package.json name is not a safe plugin link path: ${name}`);
10
+ }
11
+ return destination;
12
+ }
@@ -9,7 +9,7 @@ import {
9
9
  import { findRepoRoot } from "../capability/fs";
10
10
  import type { Skill as CapabilitySkill } from "../capability/skill";
11
11
  import type { SkillsSettings } from "../config/settings-schema";
12
- import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
12
+ import { resolveGlobalUserSkillLinkTrust, resolveSkillScopeTrust } from "../config/skill-settings-defaults";
13
13
  import { BUNDLED_GJC_SKILL_CATALOG } from "../defaults/gjc-skills.generated";
14
14
  import { scanClaudeProjectSkills, scanClaudeUserSkills } from "../discovery/claude";
15
15
  import { loadMarketplaceSkills } from "../discovery/claude-plugins";
@@ -79,6 +79,8 @@ export interface DiscoverRuntimeSkillsOptions {
79
79
  offset?: number;
80
80
  source?: RuntimeSkillDiscoveryScope | "all";
81
81
  policy?: SkillsSettings;
82
+ /** User-owned permission to read external user skill symlinks; defaults to the direct policy value. */
83
+ allowExternalUserSkillSymlinks?: boolean;
82
84
  }
83
85
 
84
86
  function getRuntimeHome(): string {
@@ -296,6 +298,7 @@ async function scanProjectOrUserDir(
296
298
  source: RuntimeSkillDiscoveryScope,
297
299
  providerPriority: number,
298
300
  authorityRoot?: string,
301
+ allowExternalUserSkillSymlinks?: boolean,
299
302
  ): Promise<ScanJobResult> {
300
303
  const result = await scanSkillsFromDir(ctx, {
301
304
  dir,
@@ -304,6 +307,7 @@ async function scanProjectOrUserDir(
304
307
  providerId: "runtime",
305
308
  level,
306
309
  requireDescription: true,
310
+ allowExternalUserSkillSymlinks,
307
311
  });
308
312
  return {
309
313
  items: result.items.map(skill => ({ skill, source, providerPriority })),
@@ -449,6 +453,8 @@ export async function discoverRuntimeSkills(
449
453
  const home = options.home ?? getRuntimeHome();
450
454
  const source = options.source ?? "all";
451
455
  const policy = options.policy;
456
+ const allowExternalUserSkillSymlinks =
457
+ options.allowExternalUserSkillSymlinks ?? resolveGlobalUserSkillLinkTrust(policy ?? {});
452
458
  const diagnostics: string[] = [];
453
459
  const agentDir = resolveRuntimeAgentDir(home, options.agentDir, hasExplicitHome);
454
460
  const profileAuthority =
@@ -478,6 +484,7 @@ export async function discoverRuntimeSkills(
478
484
  "user",
479
485
  100,
480
486
  path.resolve(dir) === path.resolve(agentDir, "skills") ? agentDir : undefined,
487
+ allowExternalUserSkillSymlinks,
481
488
  ),
482
489
  );
483
490
  }
@@ -485,7 +492,16 @@ export async function discoverRuntimeSkills(
485
492
  if ((source === "all" || source === "user") && policy?.enabled === true) {
486
493
  for (const dir of getCustomSkillDirs(policy, home)) {
487
494
  scanJobs.push(
488
- scanProjectOrUserDir({ cwd: options.cwd, home, repoRoot: home }, dir, "user", `custom ${dir}`, "user", 0),
495
+ scanProjectOrUserDir(
496
+ { cwd: options.cwd, home, repoRoot: home },
497
+ dir,
498
+ "user",
499
+ `custom ${dir}`,
500
+ "user",
501
+ 0,
502
+ undefined,
503
+ allowExternalUserSkillSymlinks,
504
+ ),
489
505
  );
490
506
  }
491
507
  }
@@ -630,6 +646,7 @@ export async function findRuntimeSkillByName(
630
646
  home?: string,
631
647
  agentDir?: string,
632
648
  profileAuthority?: "default" | "custom",
649
+ allowExternalUserSkillSymlinks?: boolean,
633
650
  ): Promise<Skill | undefined> {
634
651
  const normalized = name.trim();
635
652
  if (!normalized) return undefined;
@@ -650,6 +667,7 @@ export async function findRuntimeSkillByName(
650
667
  home: resolvedHome,
651
668
  agentDir: resolvedAgentDir,
652
669
  profileAuthority: resolvedProfileAuthority,
670
+ allowExternalUserSkillSymlinks: allowExternalUserSkillSymlinks ?? resolveGlobalUserSkillLinkTrust(policy ?? {}),
653
671
  });
654
672
  return loaded.skills.find(skill => skill.name === normalized);
655
673
  }
@@ -56,6 +56,7 @@ export type SkillDisabledReason = "protected" | "scope-trust" | "ignored" | "inc
56
56
  export interface ManagedSkillRecord {
57
57
  name: string;
58
58
  description: string;
59
+ importedFrom?: string;
59
60
  path: string;
60
61
  scope: SkillScope;
61
62
  /** Canonical source label, e.g. "project .gjc/skills" or "user ~/.gjc/agent/skills". */
@@ -299,8 +300,10 @@ export async function listNativeSkillsForManagement(options: {
299
300
  cwd: string;
300
301
  home?: string;
301
302
  agentDir?: string;
302
- /** Resolver-owned profile classification; unlike path comparison, this survives HOME refreshes. */
303
+ /** Resolver-owned profile classification; unlike path comparison, this survives HOME/config refreshes. */
303
304
  profileAuthority?: "default" | "custom";
305
+ /** User-owned permission to follow symlinks outside user skill scan roots. */
306
+ allowExternalUserSkillSymlinks?: boolean;
304
307
  policy?: SkillManagementPolicy;
305
308
  }): Promise<ManagedSkillRecord[]> {
306
309
  const homeWasInjected = options.home !== undefined;
@@ -321,6 +324,7 @@ export async function listNativeSkillsForManagement(options: {
321
324
  const policy = options.policy;
322
325
  const projectTrusted = resolveSkillScopeTrust(policy ?? {}, "project");
323
326
  const userTrusted = resolveSkillScopeTrust(policy ?? {}, "user");
327
+ const allowExternalUserSkillSymlinks = options.allowExternalUserSkillSymlinks ?? userTrusted;
324
328
 
325
329
  const scanJobs: Array<Promise<{ dir: string; scope: SkillScope; items: CapabilitySkill[] }>> = [];
326
330
  const projectDirs = await getProjectSkillDirs(options.cwd, home);
@@ -351,6 +355,7 @@ export async function listNativeSkillsForManagement(options: {
351
355
  providerId: "runtime",
352
356
  level: "user",
353
357
  requireDescription: true,
358
+ allowExternalUserSkillSymlinks,
354
359
  },
355
360
  ).then(result => ({ dir, scope: "user" as const, items: result.items })),
356
361
  );
@@ -388,10 +393,12 @@ export async function listNativeSkillsForManagement(options: {
388
393
 
389
394
  if (!seenNames.has(skill.name)) {
390
395
  seenNames.add(skill.name);
396
+ const importedFrom = skill.frontmatter?.xGjcImportedFrom ?? skill.frontmatter?.["x-gjc-imported-from"];
391
397
  records.push({
392
398
  name: skill.name,
393
399
  description: typeof skill.frontmatter?.description === "string" ? skill.frontmatter.description : "",
394
- path: skill.path,
400
+ ...(typeof importedFrom === "string" ? { importedFrom } : {}),
401
+ path: skill.discoveryPath ?? skill.path,
395
402
  scope,
396
403
  source,
397
404
  hidden: skill.frontmatter?.hide === true,
@@ -125,6 +125,8 @@ export interface LoadSkillsOptions extends SkillsSettings {
125
125
  agentDir?: string;
126
126
  /** Resolver-owned classification for the selected agent directory. */
127
127
  profileAuthority?: "default" | "custom";
128
+ /** User-owned permission to follow symlinks outside user skill scan roots. */
129
+ allowExternalUserSkillSymlinks?: boolean;
128
130
  }
129
131
 
130
132
  /**
@@ -167,6 +169,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
167
169
 
168
170
  const projectTrusted = resolveSkillScopeTrust(options, "project");
169
171
  const userTrusted = resolveSkillScopeTrust(options, "user");
172
+ const allowExternalUserSkillSymlinks = options.allowExternalUserSkillSymlinks ?? userTrusted;
170
173
 
171
174
  // Skill scope trust decides which canonical locations are loaded: project
172
175
  // scope covers `.gjc/skills` (walk-up), user scope covers `~/.gjc/agent/skills`
@@ -197,6 +200,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
197
200
  userAgentDir: agentDir,
198
201
  profileAuthority,
199
202
  repoRoot: await findRepoRoot(cwd),
203
+ allowExternalUserSkillSymlinks,
200
204
  };
201
205
  const providerResults = await Promise.all(
202
206
  providers.map(async provider => {
@@ -311,12 +315,13 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
311
315
  customDirectories.map(async dir => {
312
316
  const expandedDir = expandTilde(dir, home);
313
317
  const scanResult = await scanSkillsFromDir(
314
- { cwd, home, repoRoot: null },
318
+ { cwd, home, repoRoot: null, allowExternalUserSkillSymlinks },
315
319
  {
316
320
  dir: expandedDir,
317
321
  providerId: "custom",
318
322
  level: "user",
319
323
  requireDescription: true,
324
+ allowExternalUserSkillSymlinks,
320
325
  },
321
326
  );
322
327
  return { expandedDir, scanResult };