@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
@@ -1,9 +1,34 @@
1
1
  /**
2
2
  * Generated bundled GJC workflow skill catalog.
3
3
  *
4
- * Keep this module metadata-only: skill bodies are loaded through literal
5
- * dynamic imports only when a caller asks for their content.
4
+ * Keep this module metadata-only: skill bodies are read from their embedded
5
+ * `type: "file"` paths only when a caller asks for their content. Do not also
6
+ * import a body with `type: "text"`: Bun caches a module per specifier, so the
7
+ * second import of the same file would return the first one's value.
6
8
  */
9
+ import { readFile } from "node:fs/promises";
10
+ import autoresearchAutoCriticPath from "./gjc/skills/autoresearch/auto-critic.md" with { type: "file" };
11
+ import autoresearchAutoIteratePath from "./gjc/skills/autoresearch/auto-iterate.md" with { type: "file" };
12
+ import autoresearchSkillPath from "./gjc/skills/autoresearch/SKILL.md" with { type: "file" };
13
+ import deepInterviewAutoAnswerUncertainPath from "./gjc/skills/deep-interview/auto-answer-uncertain.md" with {
14
+ type: "file",
15
+ };
16
+ import deepInterviewLateralReviewPanelPath from "./gjc/skills/deep-interview/lateral-review-panel.md" with {
17
+ type: "file",
18
+ };
19
+ import deepInterviewSkillPath from "./gjc/skills/deep-interview/SKILL.md" with { type: "file" };
20
+ import ralplanSkillPath from "./gjc/skills/ralplan/SKILL.md" with { type: "file" };
21
+ import ultragoalAiSlopCleanerPath from "./gjc/skills/ultragoal/ai-slop-cleaner.md" with { type: "file" };
22
+ import ultragoalBoundaryCohortGatePath from "./gjc/skills/ultragoal/boundary-cohort-gate.md" with { type: "file" };
23
+ import ultragoalCrossRepositorySuccessionPath from "./gjc/skills/ultragoal/cross-repository-succession.md" with {
24
+ type: "file",
25
+ };
26
+ import ultragoalSkillPath from "./gjc/skills/ultragoal/SKILL.md" with { type: "file" };
27
+ import ultragoalTerminalCriticGatePath from "./gjc/skills/ultragoal/terminal-critic-gate.md" with { type: "file" };
28
+ import ultragoalValidationBatchContractsPath from "./gjc/skills/ultragoal/validation-batch-contracts.md" with {
29
+ type: "file",
30
+ };
31
+
7
32
  export type BundledGjcSkillName = "autoresearch" | "deep-interview" | "ralplan" | "ultragoal";
8
33
 
9
34
  export interface BundledGjcSkillCatalogEntry {
@@ -12,49 +37,34 @@ export interface BundledGjcSkillCatalogEntry {
12
37
  readonly parentSkillName?: BundledGjcSkillName;
13
38
  readonly relativePath: string;
14
39
  readonly description?: string;
40
+ /**
41
+ * Path of the bundled body. A `type: "file"` import, so `bun build --compile`
42
+ * embeds the file and this resolves inside `/$bunfs` in compiled binaries.
43
+ */
44
+ readonly sourcePath?: string;
15
45
  readonly loadContent: () => Promise<string>;
16
46
  }
17
47
 
18
- const deepInterview = () =>
19
- import("./gjc/skills/deep-interview/SKILL.md", { with: { type: "text" } }).then(module => module.default);
20
- const ralplan = () =>
21
- import("./gjc/skills/ralplan/SKILL.md", { with: { type: "text" } }).then(module => module.default);
22
- const autoresearch = () =>
23
- import("./gjc/skills/autoresearch/SKILL.md", { with: { type: "text" } }).then(module => module.default);
24
- const ultragoal = () =>
25
- import("./gjc/skills/ultragoal/SKILL.md", { with: { type: "text" } }).then(module => module.default);
26
- const autoAnswerUncertain = () =>
27
- import("./gjc/skills/deep-interview/auto-answer-uncertain.md", { with: { type: "text" } }).then(
28
- module => module.default,
29
- );
30
- const lateralReviewPanel = () =>
31
- import("./gjc/skills/deep-interview/lateral-review-panel.md", { with: { type: "text" } }).then(
32
- module => module.default,
33
- );
34
- const aiSlopCleaner = () =>
35
- import("./gjc/skills/ultragoal/ai-slop-cleaner.md", { with: { type: "text" } }).then(module => module.default);
36
- const validationBatchContracts = () =>
37
- import("./gjc/skills/ultragoal/validation-batch-contracts.md", { with: { type: "text" } }).then(
38
- module => module.default,
39
- );
40
- const boundaryCohortGate = () =>
41
- import("./gjc/skills/ultragoal/boundary-cohort-gate.md", { with: { type: "text" } }).then(module => module.default);
42
- const terminalCriticGate = () =>
43
- import("./gjc/skills/ultragoal/terminal-critic-gate.md", { with: { type: "text" } }).then(module => module.default);
44
- const crossRepositorySuccession = () =>
45
- import("./gjc/skills/ultragoal/cross-repository-succession.md", { with: { type: "text" } }).then(
46
- module => module.default,
47
- );
48
- const autoresearchIterate = () =>
49
- import("./gjc/skills/autoresearch/auto-iterate.md", { with: { type: "text" } }).then(module => module.default);
50
- const autoresearchCritic = () =>
51
- import("./gjc/skills/autoresearch/auto-critic.md", { with: { type: "text" } }).then(module => module.default);
48
+ const deepInterview = () => readFile(deepInterviewSkillPath, "utf8");
49
+ const ralplan = () => readFile(ralplanSkillPath, "utf8");
50
+ const autoresearch = () => readFile(autoresearchSkillPath, "utf8");
51
+ const ultragoal = () => readFile(ultragoalSkillPath, "utf8");
52
+ const autoAnswerUncertain = () => readFile(deepInterviewAutoAnswerUncertainPath, "utf8");
53
+ const lateralReviewPanel = () => readFile(deepInterviewLateralReviewPanelPath, "utf8");
54
+ const aiSlopCleaner = () => readFile(ultragoalAiSlopCleanerPath, "utf8");
55
+ const validationBatchContracts = () => readFile(ultragoalValidationBatchContractsPath, "utf8");
56
+ const boundaryCohortGate = () => readFile(ultragoalBoundaryCohortGatePath, "utf8");
57
+ const terminalCriticGate = () => readFile(ultragoalTerminalCriticGatePath, "utf8");
58
+ const crossRepositorySuccession = () => readFile(ultragoalCrossRepositorySuccessionPath, "utf8");
59
+ const autoresearchIterate = () => readFile(autoresearchAutoIteratePath, "utf8");
60
+ const autoresearchCritic = () => readFile(autoresearchAutoCriticPath, "utf8");
52
61
 
53
62
  export const BUNDLED_GJC_SKILL_CATALOG: readonly BundledGjcSkillCatalogEntry[] = [
54
63
  {
55
64
  kind: "skill",
56
65
  name: "deep-interview",
57
66
  relativePath: "skills/deep-interview/SKILL.md",
67
+ sourcePath: deepInterviewSkillPath,
58
68
  description: "Socratic deep interview with mathematical ambiguity gating before explicit execution approval",
59
69
  loadContent: deepInterview,
60
70
  },
@@ -62,6 +72,7 @@ export const BUNDLED_GJC_SKILL_CATALOG: readonly BundledGjcSkillCatalogEntry[] =
62
72
  kind: "skill",
63
73
  name: "ralplan",
64
74
  relativePath: "skills/ralplan/SKILL.md",
75
+ sourcePath: ralplanSkillPath,
65
76
  description: "Consensus planning entrypoint that auto-gates vague ultragoal requests before execution",
66
77
  loadContent: ralplan,
67
78
  },
@@ -69,6 +80,7 @@ export const BUNDLED_GJC_SKILL_CATALOG: readonly BundledGjcSkillCatalogEntry[] =
69
80
  kind: "skill",
70
81
  name: "autoresearch",
71
82
  relativePath: "skills/autoresearch/SKILL.md",
83
+ sourcePath: autoresearchSkillPath,
72
84
  description: "Goal-directed research missions interleaving web and data evidence into a structured verdict",
73
85
  loadContent: autoresearch,
74
86
  },
@@ -76,6 +88,7 @@ export const BUNDLED_GJC_SKILL_CATALOG: readonly BundledGjcSkillCatalogEntry[] =
76
88
  kind: "skill",
77
89
  name: "ultragoal",
78
90
  relativePath: "skills/ultragoal/SKILL.md",
91
+ sourcePath: ultragoalSkillPath,
79
92
  description: "Create and execute durable repo-native multi-goal plans over GJC goal mode artifacts.",
80
93
  loadContent: ultragoal,
81
94
  },
@@ -83,54 +96,63 @@ export const BUNDLED_GJC_SKILL_CATALOG: readonly BundledGjcSkillCatalogEntry[] =
83
96
  kind: "skill-fragment",
84
97
  parentSkillName: "deep-interview",
85
98
  relativePath: "skill-fragments/deep-interview/auto-answer-uncertain.md",
99
+ sourcePath: deepInterviewAutoAnswerUncertainPath,
86
100
  loadContent: autoAnswerUncertain,
87
101
  },
88
102
  {
89
103
  kind: "skill-fragment",
90
104
  parentSkillName: "deep-interview",
91
105
  relativePath: "skill-fragments/deep-interview/lateral-review-panel.md",
106
+ sourcePath: deepInterviewLateralReviewPanelPath,
92
107
  loadContent: lateralReviewPanel,
93
108
  },
94
109
  {
95
110
  kind: "skill-fragment",
96
111
  parentSkillName: "ultragoal",
97
112
  relativePath: "skill-fragments/ultragoal/ai-slop-cleaner.md",
113
+ sourcePath: ultragoalAiSlopCleanerPath,
98
114
  loadContent: aiSlopCleaner,
99
115
  },
100
116
  {
101
117
  kind: "skill-fragment",
102
118
  parentSkillName: "ultragoal",
103
119
  relativePath: "skill-fragments/ultragoal/validation-batch-contracts.md",
120
+ sourcePath: ultragoalValidationBatchContractsPath,
104
121
  loadContent: validationBatchContracts,
105
122
  },
106
123
  {
107
124
  kind: "skill-fragment",
108
125
  parentSkillName: "ultragoal",
109
126
  relativePath: "skill-fragments/ultragoal/boundary-cohort-gate.md",
127
+ sourcePath: ultragoalBoundaryCohortGatePath,
110
128
  loadContent: boundaryCohortGate,
111
129
  },
112
130
  {
113
131
  kind: "skill-fragment",
114
132
  parentSkillName: "ultragoal",
115
133
  relativePath: "skill-fragments/ultragoal/terminal-critic-gate.md",
134
+ sourcePath: ultragoalTerminalCriticGatePath,
116
135
  loadContent: terminalCriticGate,
117
136
  },
118
137
  {
119
138
  kind: "skill-fragment",
120
139
  parentSkillName: "ultragoal",
121
140
  relativePath: "skill-fragments/ultragoal/cross-repository-succession.md",
141
+ sourcePath: ultragoalCrossRepositorySuccessionPath,
122
142
  loadContent: crossRepositorySuccession,
123
143
  },
124
144
  {
125
145
  kind: "skill-fragment",
126
146
  parentSkillName: "autoresearch",
127
147
  relativePath: "skill-fragments/autoresearch/auto-iterate.md",
148
+ sourcePath: autoresearchAutoIteratePath,
128
149
  loadContent: autoresearchIterate,
129
150
  },
130
151
  {
131
152
  kind: "skill-fragment",
132
153
  parentSkillName: "autoresearch",
133
154
  relativePath: "skill-fragments/autoresearch/auto-critic.md",
155
+ sourcePath: autoresearchAutoCriticPath,
134
156
  loadContent: autoresearchCritic,
135
157
  },
136
158
  ];
@@ -79,7 +79,12 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
79
79
  scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "project" }),
80
80
  );
81
81
  const userScans = getUserPathCandidates(ctx, "skills").map(dir =>
82
- scanSkillsFromDir(ctx, { dir, providerId: PROVIDER_ID, level: "user" }),
82
+ scanSkillsFromDir(ctx, {
83
+ dir,
84
+ providerId: PROVIDER_ID,
85
+ level: "user",
86
+ allowExternalUserSkillSymlinks: ctx.allowExternalUserSkillSymlinks,
87
+ }),
83
88
  );
84
89
 
85
90
  const results = await Promise.all([...projectScans, ...userScans]);
@@ -22,6 +22,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
22
22
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
23
23
  import { type CustomTool, toolCapability } from "../capability/tool";
24
24
  import type { LoadContext, LoadResult } from "../capability/types";
25
+ import { resolveGlobalUserSkillLinkTrust } from "../config/skill-settings-defaults";
25
26
  import { expandTilde } from "../tools/path-utils";
26
27
  import {
27
28
  buildRuleFromMarkdown,
@@ -137,7 +138,9 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
137
138
  const data = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
138
139
  if (!data?.mcpServers) return result;
139
140
 
140
- const expanded = expandEnvVarsDeep(data.mcpServers);
141
+ // Project mcp.json is repo content. Skip secret-named ${VAR} the same way
142
+ // project ssh.json does; user scope still expands those names.
143
+ const expanded = expandEnvVarsDeep(data.mcpServers, undefined, level === "project");
141
144
  for (const [serverName, config] of Object.entries(expanded)) {
142
145
  const serverConfig = config as Record<string, unknown>;
143
146
 
@@ -220,6 +223,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
220
223
  cwd: serverConfig.cwd as string | undefined,
221
224
  url: serverConfig.url as string | undefined,
222
225
  headers: serverConfig.headers as Record<string, string> | undefined,
226
+ publicNetwork: serverConfig.publicNetwork === true ? true : undefined,
223
227
  auth: serverConfig.auth as
224
228
  | {
225
229
  type: "oauth" | "apikey";
@@ -359,6 +363,14 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
359
363
  level: "user",
360
364
  scope: "native",
361
365
  requireDescription: true,
366
+ allowExternalUserSkillSymlinks:
367
+ ctx.allowExternalUserSkillSymlinks ??
368
+ (typeof ctx.settings?.getGlobal === "function"
369
+ ? resolveGlobalUserSkillLinkTrust({
370
+ trustUserSkills: ctx.settings.getGlobal("skills.trustUserSkills"),
371
+ enablePiUser: ctx.settings.getGlobal("skills.enablePiUser"),
372
+ })
373
+ : true),
362
374
  }),
363
375
  );
364
376
 
@@ -479,6 +479,8 @@ export interface ScanSkillsFromDirOptions {
479
479
  linkContainmentRoot?: string;
480
480
  /** Filesystem authority for explicit-home reads. */
481
481
  scope?: ReadScope;
482
+ /** User-owned permission to follow symlinks outside a user-scope scan root. */
483
+ allowExternalUserSkillSymlinks?: boolean;
482
484
  }
483
485
 
484
486
  // Stable ordering used for skill lists in prompts: name (case-insensitive), then name, then path.
@@ -498,6 +500,8 @@ export const SKILL_FRONTMATTER_SCAN_TOTAL_BYTES = 64 * 1024;
498
500
 
499
501
  export const SkillDiscoveryTestHooks: {
500
502
  afterCandidateValidated?: (candidatePath: string) => void | Promise<void>;
503
+ afterSkillLinksCaptured?: (candidatePath: string) => void | Promise<void>;
504
+ afterSkillPathResolved?: (candidatePath: string) => void | Promise<void>;
501
505
  afterAuthorityRootValidated?: (root: string) => void | Promise<void>;
502
506
  afterAuthorityRootMissing?: (root: string) => void | Promise<void>;
503
507
  afterContainedRootValidated?: (root: string) => void | Promise<void>;
@@ -517,6 +521,25 @@ interface ScanRootLinkIdentity {
517
521
  target: DirectoryIdentity;
518
522
  }
519
523
 
524
+ interface SkillFileIdentity {
525
+ dev: bigint;
526
+ ino: bigint;
527
+ realPath: string;
528
+ }
529
+
530
+ interface SkillLinkIdentity {
531
+ path: string;
532
+ dev: bigint;
533
+ ino: bigint;
534
+ linkText: string;
535
+ target: {
536
+ dev: bigint;
537
+ ino: bigint;
538
+ realPath: string;
539
+ kind: "directory" | "file";
540
+ };
541
+ }
542
+
520
543
  async function captureScanRootLinkIdentity(root: string): Promise<ScanRootLinkIdentity | null> {
521
544
  const observed = await fs.promises.lstat(root, { bigint: true });
522
545
  if (!observed.isSymbolicLink()) return null;
@@ -577,6 +600,38 @@ async function directoryIdentityIsCurrent(
577
600
  }
578
601
  }
579
602
 
603
+ async function captureSkillLinkIdentity(linkPath: string): Promise<SkillLinkIdentity | null> {
604
+ const observed = await fs.promises.lstat(linkPath, { bigint: true });
605
+ if (!observed.isSymbolicLink()) return null;
606
+ const linkText = await fs.promises.readlink(linkPath);
607
+ const realPath = await fs.promises.realpath(linkPath);
608
+ const target = await fs.promises.stat(realPath, { bigint: true });
609
+ const kind = target.isDirectory() ? "directory" : target.isFile() ? "file" : undefined;
610
+ if (!kind) return null;
611
+ return {
612
+ path: linkPath,
613
+ dev: observed.dev,
614
+ ino: observed.ino,
615
+ linkText,
616
+ target: { dev: target.dev, ino: target.ino, realPath, kind },
617
+ };
618
+ }
619
+
620
+ async function skillLinkIdentityIsCurrent(expected: SkillLinkIdentity): Promise<boolean> {
621
+ try {
622
+ const observed = await fs.promises.lstat(expected.path, { bigint: true });
623
+ if (!observed.isSymbolicLink() || observed.dev !== expected.dev || observed.ino !== expected.ino) return false;
624
+ if ((await fs.promises.readlink(expected.path)) !== expected.linkText) return false;
625
+ const realPath = await fs.promises.realpath(expected.path);
626
+ if (normalizePathForComparison(realPath) !== normalizePathForComparison(expected.target.realPath)) return false;
627
+ const target = await fs.promises.stat(realPath, { bigint: true });
628
+ const kind = target.isDirectory() ? "directory" : target.isFile() ? "file" : undefined;
629
+ return kind === expected.target.kind && target.dev === expected.target.dev && target.ino === expected.target.ino;
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+
580
635
  export async function scanSkillsFromDir(
581
636
  _ctx: LoadContext,
582
637
  options: ScanSkillsFromDirOptions,
@@ -693,7 +748,36 @@ export async function scanSkillsFromDir(
693
748
  const relative = path.relative(realRoot, candidate);
694
749
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
695
750
  };
696
- const openSkillFileSafely = async (skillPath: string): Promise<FileHandle> => {
751
+ const skillPathAndLinksAreCurrent = async (
752
+ skillPath: string,
753
+ expectedFile: SkillFileIdentity | undefined,
754
+ linkIdentities: readonly SkillLinkIdentity[],
755
+ ): Promise<boolean> => {
756
+ try {
757
+ const [realPath, currentFile, currentLinks] = await Promise.all([
758
+ fs.promises.realpath(skillPath),
759
+ expectedFile ? fs.promises.stat(expectedFile.realPath, { bigint: true }) : Promise.resolve(undefined),
760
+ Promise.all(linkIdentities.map(skillLinkIdentityIsCurrent)),
761
+ ]);
762
+ if (!expectedFile) return isWithinRoot(realPath) && currentLinks.every(Boolean);
763
+ if (!currentFile) return false;
764
+ return (
765
+ normalizePathForComparison(realPath) === normalizePathForComparison(expectedFile.realPath) &&
766
+ currentFile.isFile() &&
767
+ currentFile.nlink === 1n &&
768
+ currentFile.dev === expectedFile.dev &&
769
+ currentFile.ino === expectedFile.ino &&
770
+ currentLinks.every(Boolean)
771
+ );
772
+ } catch {
773
+ return false;
774
+ }
775
+ };
776
+ const openSkillFileSafely = async (
777
+ skillPath: string,
778
+ expectedFile: SkillFileIdentity | undefined,
779
+ linkIdentities: readonly SkillLinkIdentity[],
780
+ ): Promise<FileHandle> => {
697
781
  if (
698
782
  !(await scanRootIsCurrent()) ||
699
783
  (options.authorityRoot &&
@@ -707,14 +791,14 @@ export async function scanSkillsFromDir(
707
791
  (process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
708
792
  const handle = await fs.promises.open(skillPath, flags);
709
793
  try {
710
- const [opened, observed, currentPath, currentScanRoot, currentAuthorityRoot] = await Promise.all([
794
+ const [opened, observed, currentScanRoot, currentAuthorityRoot, currentSkillPath] = await Promise.all([
711
795
  handle.stat({ bigint: true }),
712
796
  fs.promises.lstat(skillPath, { bigint: true }),
713
- fs.promises.realpath(skillPath),
714
797
  scanRootIsCurrent(),
715
798
  options.authorityRoot && authorityIdentity
716
799
  ? directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)
717
800
  : true,
801
+ skillPathAndLinksAreCurrent(skillPath, expectedFile, linkIdentities),
718
802
  ]);
719
803
  if (
720
804
  !opened.isFile() ||
@@ -723,9 +807,10 @@ export async function scanSkillsFromDir(
723
807
  !observed.isFile() ||
724
808
  opened.dev !== observed.dev ||
725
809
  opened.ino !== observed.ino ||
726
- !isWithinRoot(currentPath) ||
810
+ (expectedFile !== undefined && (opened.dev !== expectedFile.dev || opened.ino !== expectedFile.ino)) ||
727
811
  !currentScanRoot ||
728
- !currentAuthorityRoot
812
+ !currentAuthorityRoot ||
813
+ !currentSkillPath
729
814
  ) {
730
815
  throw new Error(`Unsafe skill file: ${skillPath}`);
731
816
  }
@@ -735,15 +820,20 @@ export async function scanSkillsFromDir(
735
820
  throw error;
736
821
  }
737
822
  };
738
- const readSkillContentSafely = async (skillPath: string): Promise<string> => {
739
- const handle = await openSkillFileSafely(skillPath);
823
+ const readSkillContentSafely = async (
824
+ skillPath: string,
825
+ expectedFile: SkillFileIdentity | undefined,
826
+ linkIdentities: readonly SkillLinkIdentity[],
827
+ ): Promise<string> => {
828
+ const handle = await openSkillFileSafely(skillPath, expectedFile, linkIdentities);
740
829
  try {
741
830
  const content = await handle.readFile({ encoding: "utf8" });
742
831
  if (
743
832
  !(await scanRootIsCurrent()) ||
744
833
  (options.authorityRoot &&
745
834
  authorityIdentity &&
746
- !(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)))
835
+ !(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity))) ||
836
+ !(await skillPathAndLinksAreCurrent(skillPath, expectedFile, linkIdentities))
747
837
  ) {
748
838
  throw new Error(`Unsafe skill authority root for: ${skillPath}`);
749
839
  }
@@ -754,8 +844,10 @@ export async function scanSkillsFromDir(
754
844
  };
755
845
  const readSkillFrontmatterSafely = async (
756
846
  skillPath: string,
847
+ expectedFile: SkillFileIdentity | undefined,
848
+ linkIdentities: readonly SkillLinkIdentity[],
757
849
  ): Promise<{ frontmatter: SkillFrontmatter | null; size: number }> => {
758
- const handle = await openSkillFileSafely(skillPath);
850
+ const handle = await openSkillFileSafely(skillPath, expectedFile, linkIdentities);
759
851
  try {
760
852
  const size = Number((await handle.stat({ bigint: true })).size);
761
853
  const scanLimit = Math.min(size, SKILL_FRONTMATTER_SCAN_TOTAL_BYTES);
@@ -786,20 +878,64 @@ export async function scanSkillsFromDir(
786
878
  await handle.close();
787
879
  }
788
880
  };
789
- const loadSkill = async (candidatePath: string) => {
881
+ const loadSkill = async (candidatePath: string, expectedCanonicalPath?: string) => {
790
882
  try {
883
+ const linkIdentities = (
884
+ await Promise.all([
885
+ captureSkillLinkIdentity(path.dirname(candidatePath)),
886
+ captureSkillLinkIdentity(candidatePath),
887
+ ])
888
+ ).filter((identity): identity is SkillLinkIdentity => identity !== null);
889
+ await SkillDiscoveryTestHooks.afterSkillLinksCaptured?.(candidatePath);
791
890
  const skillPath = await fs.promises.realpath(candidatePath);
792
- if (!isWithinRoot(skillPath)) {
793
- warnings.push(`Refusing skill path outside scan root: ${candidatePath}`);
891
+ await SkillDiscoveryTestHooks.afterSkillPathResolved?.(candidatePath);
892
+ if (
893
+ expectedCanonicalPath &&
894
+ normalizePathForComparison(skillPath) !== normalizePathForComparison(expectedCanonicalPath)
895
+ ) {
896
+ warnings.push(`Refusing changed skill path during isolated-home scan: ${candidatePath}`);
897
+ return;
898
+ }
899
+ const allowOutsideRoot = options.allowExternalUserSkillSymlinks && level === "user";
900
+ if (!isWithinRoot(skillPath) && !allowOutsideRoot) {
901
+ const remedy =
902
+ level === "user"
903
+ ? `add ${path.dirname(path.dirname(skillPath))} to skills.customDirectories to load it, or enable skills.trustUserSkills`
904
+ : "keep project skill targets inside the skills scan root";
905
+ warnings.push(
906
+ `Refusing skill path outside scan root: ${candidatePath} (resolves to ${skillPath}; ${remedy})`,
907
+ );
794
908
  return;
795
909
  }
796
- const stat = await fs.promises.stat(skillPath);
797
- if (!stat.isFile() || stat.nlink !== 1) {
910
+ const stat = await fs.promises.stat(skillPath, { bigint: true });
911
+ if (!stat.isFile() || stat.nlink !== 1n) {
798
912
  warnings.push(`Skill path is not a regular file: ${candidatePath}`);
799
913
  return;
800
914
  }
915
+ const pinExternalTarget = !isWithinRoot(skillPath) && allowOutsideRoot;
916
+ const fileLink = linkIdentities.find(identity => identity.path === candidatePath);
917
+ const parentLink = linkIdentities.find(identity => identity.path === path.dirname(candidatePath));
918
+ let capturedLinkTargetPath: string | undefined;
919
+ if (fileLink?.target.kind === "file") {
920
+ capturedLinkTargetPath = fileLink.target.realPath;
921
+ } else if (parentLink?.target.kind === "directory") {
922
+ capturedLinkTargetPath = path.join(parentLink.target.realPath, path.basename(candidatePath));
923
+ }
924
+ if (
925
+ pinExternalTarget &&
926
+ (!capturedLinkTargetPath ||
927
+ normalizePathForComparison(skillPath) !== normalizePathForComparison(capturedLinkTargetPath))
928
+ ) {
929
+ warnings.push(`Refusing skill path not bound to its captured external link target: ${candidatePath}`);
930
+ return;
931
+ }
932
+ const fileIdentity = pinExternalTarget ? { dev: stat.dev, ino: stat.ino, realPath: skillPath } : undefined;
933
+ const pinnedLinkIdentities = pinExternalTarget ? linkIdentities : [];
801
934
  await SkillDiscoveryTestHooks.afterCandidateValidated?.(candidatePath);
802
- const { frontmatter, size } = await readSkillFrontmatterSafely(skillPath);
935
+ const { frontmatter, size } = await readSkillFrontmatterSafely(skillPath, fileIdentity, pinnedLinkIdentities);
936
+ if (!(await skillPathAndLinksAreCurrent(skillPath, fileIdentity, pinnedLinkIdentities))) {
937
+ throw new Error(`Unsafe skill link for: ${candidatePath}`);
938
+ }
803
939
  if (!frontmatter) {
804
940
  if (size > SKILL_FRONTMATTER_SCAN_TOTAL_BYTES) {
805
941
  warnings.push(
@@ -823,8 +959,9 @@ export async function scanSkillsFromDir(
823
959
  items.push({
824
960
  name,
825
961
  path: skillPath,
962
+ discoveryPath: candidatePath,
826
963
  loadContent: async () => {
827
- const content = await readSkillContentSafely(skillPath);
964
+ const content = await readSkillContentSafely(skillPath, fileIdentity, pinnedLinkIdentities);
828
965
  return parseFrontmatter(content, { source: skillPath }).body;
829
966
  },
830
967
  frontmatter: frontmatter as SkillFrontmatter,
@@ -841,14 +978,10 @@ export async function scanSkillsFromDir(
841
978
  for (const entry of entries) {
842
979
  if (entry.name.startsWith(".")) continue;
843
980
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
844
- const skillPath = await canonicalizePathWithinHome(
845
- _ctx,
846
- path.join(scanDir, entry.name, "SKILL.md"),
847
- containmentRoot,
848
- scope,
849
- );
850
- if (!skillPath) continue;
851
- work.push(loadSkill(skillPath));
981
+ const candidatePath = path.join(scanDir, entry.name, "SKILL.md");
982
+ const canonicalSkillPath = await canonicalizePathWithinHome(_ctx, candidatePath, containmentRoot, scope);
983
+ if (!canonicalSkillPath) continue;
984
+ work.push(loadSkill(candidatePath, _ctx.isolatedHome ? canonicalSkillPath : undefined));
852
985
  }
853
986
  await Promise.all(work);
854
987
 
@@ -923,8 +1056,17 @@ export async function readContainedFile(
923
1056
  * Expand environment variables in a string.
924
1057
  * Supports ${VAR} and ${VAR:-default} syntax.
925
1058
  */
926
- function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
1059
+ const SENSITIVE_ENV_NAME = /key|secret|token|pass|auth|credential|cookie|dsn|url$/i;
1060
+
1061
+ export function isSensitiveEnvName(name: string): boolean {
1062
+ return SENSITIVE_ENV_NAME.test(name);
1063
+ }
1064
+
1065
+ function expandEnvVars(value: string, extraEnv?: Record<string, string>, skipSensitiveNames = false): string {
927
1066
  return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => {
1067
+ if (skipSensitiveNames && isSensitiveEnvName(varName)) {
1068
+ return defaultValue !== undefined ? defaultValue : `\${${varName}}`;
1069
+ }
928
1070
  const envValue = extraEnv?.[varName] ?? Bun.env[varName];
929
1071
  if (envValue !== undefined) return envValue;
930
1072
  if (defaultValue !== undefined) return defaultValue;
@@ -935,17 +1077,17 @@ function expandEnvVars(value: string, extraEnv?: Record<string, string>): string
935
1077
  /**
936
1078
  * Recursively expand environment variables in an object.
937
1079
  */
938
- export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>): T {
1080
+ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>, skipSensitiveNames = false): T {
939
1081
  if (typeof obj === "string") {
940
- return expandEnvVars(obj, extraEnv) as T;
1082
+ return expandEnvVars(obj, extraEnv, skipSensitiveNames) as T;
941
1083
  }
942
1084
  if (Array.isArray(obj)) {
943
- return obj.map(item => expandEnvVarsDeep(item, extraEnv)) as T;
1085
+ return obj.map(item => expandEnvVarsDeep(item, extraEnv, skipSensitiveNames)) as T;
944
1086
  }
945
1087
  if (obj !== null && typeof obj === "object") {
946
1088
  const result: Record<string, unknown> = {};
947
1089
  for (const [key, value] of Object.entries(obj)) {
948
- result[key] = expandEnvVarsDeep(value, extraEnv);
1090
+ result[key] = expandEnvVarsDeep(value, extraEnv, skipSensitiveNames);
949
1091
  }
950
1092
  return result as T;
951
1093
  }
@@ -154,7 +154,12 @@ function validateExactMCPConfig(config: unknown): {
154
154
  /**
155
155
  * Transform raw MCP config to canonical MCPServer format.
156
156
  */
157
- function transformMCPConfig(config: MCPConfigFile, source: SourceMeta, quiet = false): MCPServer[] {
157
+ function transformMCPConfig(
158
+ config: MCPConfigFile,
159
+ source: SourceMeta,
160
+ quiet = false,
161
+ skipSensitiveEnvNames = false,
162
+ ): MCPServer[] {
158
163
  const servers: MCPServer[] = [];
159
164
 
160
165
  if (config.mcpServers) {
@@ -217,21 +222,29 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta, quiet = f
217
222
  cwd: serverConfig.cwd,
218
223
  url: serverConfig.url,
219
224
  headers: serverConfig.headers,
225
+ publicNetwork:
226
+ serverConfig.type === "http" &&
227
+ "publicNetwork" in serverConfig &&
228
+ (serverConfig as { publicNetwork?: boolean }).publicNetwork === true
229
+ ? true
230
+ : undefined,
220
231
  auth: serverConfig.auth,
221
232
  oauth: serverConfig.oauth,
222
233
  transport: serverConfig.type,
223
234
  _source: source,
224
235
  };
225
236
 
226
- // Expand environment variables
227
- if (server.command) server.command = expandEnvVarsDeep(server.command);
228
- if (server.args) server.args = expandEnvVarsDeep(server.args);
229
- if (server.env) server.env = expandEnvVarsDeep(server.env);
230
- if (server.cwd) server.cwd = expandEnvVarsDeep(server.cwd);
231
- if (server.url) server.url = expandEnvVarsDeep(server.url);
232
- if (server.headers) server.headers = expandEnvVarsDeep(server.headers);
233
- if (server.auth) server.auth = expandEnvVarsDeep(server.auth);
234
- if (server.oauth) server.oauth = expandEnvVarsDeep(server.oauth);
237
+ // Repo-root mcp.json is project content. Exact `--mcp-config` passes
238
+ // skipSensitiveEnvNames false so a trusted file can still reference secrets.
239
+ const expand = <T>(value: T): T => expandEnvVarsDeep(value, undefined, skipSensitiveEnvNames);
240
+ if (server.command) server.command = expand(server.command);
241
+ if (server.args) server.args = expand(server.args);
242
+ if (server.env) server.env = expand(server.env);
243
+ if (server.cwd) server.cwd = expand(server.cwd);
244
+ if (server.url) server.url = expand(server.url);
245
+ if (server.headers) server.headers = expand(server.headers);
246
+ if (server.auth) server.auth = expand(server.auth);
247
+ if (server.oauth) server.oauth = expand(server.oauth);
235
248
  servers.push(server);
236
249
  }
237
250
  }
@@ -440,7 +453,7 @@ async function readExactMCPConfigFile(filePath: string): Promise<string | null>
440
453
  export async function loadMCPJsonFile(
441
454
  filePath: string,
442
455
  level: "user" | "project",
443
- options?: { quiet?: boolean; useCache?: boolean; readOptions?: ReadFileOptions },
456
+ options?: { quiet?: boolean; useCache?: boolean; readOptions?: ReadFileOptions; skipSensitiveEnvNames?: boolean },
444
457
  ): Promise<MCPJsonLoadResult> {
445
458
  const warnings: string[] = [];
446
459
  const items: MCPServer[] = [];
@@ -497,7 +510,7 @@ export async function loadMCPJsonFile(
497
510
  validConfig = config as MCPConfigFile;
498
511
  }
499
512
  const source = createSourceMeta(PROVIDER_ID, sourcePath, level);
500
- items.push(...transformMCPConfig(validConfig, source, options?.quiet));
513
+ items.push(...transformMCPConfig(validConfig, source, options?.quiet, options?.skipSensitiveEnvNames === true));
501
514
 
502
515
  return {
503
516
  items,
@@ -515,6 +528,7 @@ async function load(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
515
528
  filenames.map(filename =>
516
529
  loadMCPJsonFile(path.join(ctx.cwd, filename), "project", {
517
530
  readOptions: getReadOptions(ctx, "project"),
531
+ skipSensitiveEnvNames: true,
518
532
  }),
519
533
  ),
520
534
  );
@@ -119,7 +119,7 @@ async function loadSshJsonFile(
119
119
  warnings.push(`Failed to parse JSON in ${canonicalPath}`);
120
120
  return { items, warnings };
121
121
  }
122
- const config = expandEnvVarsDeep(parsed);
122
+ const config = expandEnvVarsDeep(parsed, undefined, level === "project");
123
123
  if (!config.hosts || typeof config.hosts !== "object") {
124
124
  warnings.push(`Missing hosts in ${canonicalPath}`);
125
125
  return { items, warnings };