@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. 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,
@@ -220,6 +221,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
220
221
  cwd: serverConfig.cwd as string | undefined,
221
222
  url: serverConfig.url as string | undefined,
222
223
  headers: serverConfig.headers as Record<string, string> | undefined,
224
+ publicNetwork: serverConfig.publicNetwork === true ? true : undefined,
223
225
  auth: serverConfig.auth as
224
226
  | {
225
227
  type: "oauth" | "apikey";
@@ -359,6 +361,14 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
359
361
  level: "user",
360
362
  scope: "native",
361
363
  requireDescription: true,
364
+ allowExternalUserSkillSymlinks:
365
+ ctx.allowExternalUserSkillSymlinks ??
366
+ (typeof ctx.settings?.getGlobal === "function"
367
+ ? resolveGlobalUserSkillLinkTrust({
368
+ trustUserSkills: ctx.settings.getGlobal("skills.trustUserSkills"),
369
+ enablePiUser: ctx.settings.getGlobal("skills.enablePiUser"),
370
+ })
371
+ : true),
362
372
  }),
363
373
  );
364
374
 
@@ -203,10 +203,13 @@ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: stri
203
203
 
204
204
  /** Build the filesystem authority for a provider read. */
205
205
  export function getReadOptions(
206
- ctx: Pick<LoadContext, "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity">,
206
+ ctx: Pick<
207
+ LoadContext,
208
+ "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity" | "bypassCache"
209
+ >,
207
210
  scope: ReadScope,
208
211
  ): ReadFileOptions | undefined {
209
- if (!ctx.isolatedHome) return undefined;
212
+ if (!ctx.isolatedHome) return ctx.bypassCache ? { bypassCache: true } : undefined;
210
213
  return {
211
214
  isolatedHome: true,
212
215
  home: ctx.home,
@@ -219,7 +222,10 @@ export function getReadOptions(
219
222
  }
220
223
 
221
224
  export async function getReadOptionsForContainment(
222
- ctx: Pick<LoadContext, "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity">,
225
+ ctx: Pick<
226
+ LoadContext,
227
+ "home" | "isolatedHome" | "userAgentDir" | "homeIdentity" | "userAgentIdentity" | "bypassCache"
228
+ >,
223
229
  scope: ReadScope,
224
230
  containmentRoot?: string,
225
231
  ): Promise<ReadFileOptions | undefined> {
@@ -473,6 +479,8 @@ export interface ScanSkillsFromDirOptions {
473
479
  linkContainmentRoot?: string;
474
480
  /** Filesystem authority for explicit-home reads. */
475
481
  scope?: ReadScope;
482
+ /** User-owned permission to follow symlinks outside a user-scope scan root. */
483
+ allowExternalUserSkillSymlinks?: boolean;
476
484
  }
477
485
 
478
486
  // Stable ordering used for skill lists in prompts: name (case-insensitive), then name, then path.
@@ -492,6 +500,8 @@ export const SKILL_FRONTMATTER_SCAN_TOTAL_BYTES = 64 * 1024;
492
500
 
493
501
  export const SkillDiscoveryTestHooks: {
494
502
  afterCandidateValidated?: (candidatePath: string) => void | Promise<void>;
503
+ afterSkillLinksCaptured?: (candidatePath: string) => void | Promise<void>;
504
+ afterSkillPathResolved?: (candidatePath: string) => void | Promise<void>;
495
505
  afterAuthorityRootValidated?: (root: string) => void | Promise<void>;
496
506
  afterAuthorityRootMissing?: (root: string) => void | Promise<void>;
497
507
  afterContainedRootValidated?: (root: string) => void | Promise<void>;
@@ -511,6 +521,25 @@ interface ScanRootLinkIdentity {
511
521
  target: DirectoryIdentity;
512
522
  }
513
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
+
514
543
  async function captureScanRootLinkIdentity(root: string): Promise<ScanRootLinkIdentity | null> {
515
544
  const observed = await fs.promises.lstat(root, { bigint: true });
516
545
  if (!observed.isSymbolicLink()) return null;
@@ -571,6 +600,38 @@ async function directoryIdentityIsCurrent(
571
600
  }
572
601
  }
573
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
+
574
635
  export async function scanSkillsFromDir(
575
636
  _ctx: LoadContext,
576
637
  options: ScanSkillsFromDirOptions,
@@ -687,7 +748,36 @@ export async function scanSkillsFromDir(
687
748
  const relative = path.relative(realRoot, candidate);
688
749
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
689
750
  };
690
- 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> => {
691
781
  if (
692
782
  !(await scanRootIsCurrent()) ||
693
783
  (options.authorityRoot &&
@@ -701,14 +791,14 @@ export async function scanSkillsFromDir(
701
791
  (process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
702
792
  const handle = await fs.promises.open(skillPath, flags);
703
793
  try {
704
- const [opened, observed, currentPath, currentScanRoot, currentAuthorityRoot] = await Promise.all([
794
+ const [opened, observed, currentScanRoot, currentAuthorityRoot, currentSkillPath] = await Promise.all([
705
795
  handle.stat({ bigint: true }),
706
796
  fs.promises.lstat(skillPath, { bigint: true }),
707
- fs.promises.realpath(skillPath),
708
797
  scanRootIsCurrent(),
709
798
  options.authorityRoot && authorityIdentity
710
799
  ? directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)
711
800
  : true,
801
+ skillPathAndLinksAreCurrent(skillPath, expectedFile, linkIdentities),
712
802
  ]);
713
803
  if (
714
804
  !opened.isFile() ||
@@ -717,9 +807,10 @@ export async function scanSkillsFromDir(
717
807
  !observed.isFile() ||
718
808
  opened.dev !== observed.dev ||
719
809
  opened.ino !== observed.ino ||
720
- !isWithinRoot(currentPath) ||
810
+ (expectedFile !== undefined && (opened.dev !== expectedFile.dev || opened.ino !== expectedFile.ino)) ||
721
811
  !currentScanRoot ||
722
- !currentAuthorityRoot
812
+ !currentAuthorityRoot ||
813
+ !currentSkillPath
723
814
  ) {
724
815
  throw new Error(`Unsafe skill file: ${skillPath}`);
725
816
  }
@@ -729,15 +820,20 @@ export async function scanSkillsFromDir(
729
820
  throw error;
730
821
  }
731
822
  };
732
- const readSkillContentSafely = async (skillPath: string): Promise<string> => {
733
- 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);
734
829
  try {
735
830
  const content = await handle.readFile({ encoding: "utf8" });
736
831
  if (
737
832
  !(await scanRootIsCurrent()) ||
738
833
  (options.authorityRoot &&
739
834
  authorityIdentity &&
740
- !(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)))
835
+ !(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity))) ||
836
+ !(await skillPathAndLinksAreCurrent(skillPath, expectedFile, linkIdentities))
741
837
  ) {
742
838
  throw new Error(`Unsafe skill authority root for: ${skillPath}`);
743
839
  }
@@ -748,8 +844,10 @@ export async function scanSkillsFromDir(
748
844
  };
749
845
  const readSkillFrontmatterSafely = async (
750
846
  skillPath: string,
847
+ expectedFile: SkillFileIdentity | undefined,
848
+ linkIdentities: readonly SkillLinkIdentity[],
751
849
  ): Promise<{ frontmatter: SkillFrontmatter | null; size: number }> => {
752
- const handle = await openSkillFileSafely(skillPath);
850
+ const handle = await openSkillFileSafely(skillPath, expectedFile, linkIdentities);
753
851
  try {
754
852
  const size = Number((await handle.stat({ bigint: true })).size);
755
853
  const scanLimit = Math.min(size, SKILL_FRONTMATTER_SCAN_TOTAL_BYTES);
@@ -782,18 +880,55 @@ export async function scanSkillsFromDir(
782
880
  };
783
881
  const loadSkill = async (candidatePath: string) => {
784
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);
785
890
  const skillPath = await fs.promises.realpath(candidatePath);
786
- if (!isWithinRoot(skillPath)) {
787
- warnings.push(`Refusing skill path outside scan root: ${candidatePath}`);
891
+ await SkillDiscoveryTestHooks.afterSkillPathResolved?.(candidatePath);
892
+ const allowOutsideRoot = options.allowExternalUserSkillSymlinks && level === "user";
893
+ if (!isWithinRoot(skillPath) && !allowOutsideRoot) {
894
+ const remedy =
895
+ level === "user"
896
+ ? `add ${path.dirname(path.dirname(skillPath))} to skills.customDirectories to load it, or enable skills.trustUserSkills`
897
+ : "keep project skill targets inside the skills scan root";
898
+ warnings.push(
899
+ `Refusing skill path outside scan root: ${candidatePath} (resolves to ${skillPath}; ${remedy})`,
900
+ );
788
901
  return;
789
902
  }
790
- const stat = await fs.promises.stat(skillPath);
791
- if (!stat.isFile() || stat.nlink !== 1) {
903
+ const stat = await fs.promises.stat(skillPath, { bigint: true });
904
+ if (!stat.isFile() || stat.nlink !== 1n) {
792
905
  warnings.push(`Skill path is not a regular file: ${candidatePath}`);
793
906
  return;
794
907
  }
908
+ const pinExternalTarget = !isWithinRoot(skillPath) && allowOutsideRoot;
909
+ const fileLink = linkIdentities.find(identity => identity.path === candidatePath);
910
+ const parentLink = linkIdentities.find(identity => identity.path === path.dirname(candidatePath));
911
+ let capturedLinkTargetPath: string | undefined;
912
+ if (fileLink?.target.kind === "file") {
913
+ capturedLinkTargetPath = fileLink.target.realPath;
914
+ } else if (parentLink?.target.kind === "directory") {
915
+ capturedLinkTargetPath = path.join(parentLink.target.realPath, path.basename(candidatePath));
916
+ }
917
+ if (
918
+ pinExternalTarget &&
919
+ (!capturedLinkTargetPath ||
920
+ normalizePathForComparison(skillPath) !== normalizePathForComparison(capturedLinkTargetPath))
921
+ ) {
922
+ warnings.push(`Refusing skill path not bound to its captured external link target: ${candidatePath}`);
923
+ return;
924
+ }
925
+ const fileIdentity = pinExternalTarget ? { dev: stat.dev, ino: stat.ino, realPath: skillPath } : undefined;
926
+ const pinnedLinkIdentities = pinExternalTarget ? linkIdentities : [];
795
927
  await SkillDiscoveryTestHooks.afterCandidateValidated?.(candidatePath);
796
- const { frontmatter, size } = await readSkillFrontmatterSafely(skillPath);
928
+ const { frontmatter, size } = await readSkillFrontmatterSafely(skillPath, fileIdentity, pinnedLinkIdentities);
929
+ if (!(await skillPathAndLinksAreCurrent(skillPath, fileIdentity, pinnedLinkIdentities))) {
930
+ throw new Error(`Unsafe skill link for: ${candidatePath}`);
931
+ }
797
932
  if (!frontmatter) {
798
933
  if (size > SKILL_FRONTMATTER_SCAN_TOTAL_BYTES) {
799
934
  warnings.push(
@@ -817,8 +952,9 @@ export async function scanSkillsFromDir(
817
952
  items.push({
818
953
  name,
819
954
  path: skillPath,
955
+ discoveryPath: candidatePath,
820
956
  loadContent: async () => {
821
- const content = await readSkillContentSafely(skillPath);
957
+ const content = await readSkillContentSafely(skillPath, fileIdentity, pinnedLinkIdentities);
822
958
  return parseFrontmatter(content, { source: skillPath }).body;
823
959
  },
824
960
  frontmatter: frontmatter as SkillFrontmatter,
@@ -917,8 +1053,17 @@ export async function readContainedFile(
917
1053
  * Expand environment variables in a string.
918
1054
  * Supports ${VAR} and ${VAR:-default} syntax.
919
1055
  */
920
- function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
1056
+ const SENSITIVE_ENV_NAME = /key|secret|token|pass|auth|credential|cookie|dsn|url$/i;
1057
+
1058
+ export function isSensitiveEnvName(name: string): boolean {
1059
+ return SENSITIVE_ENV_NAME.test(name);
1060
+ }
1061
+
1062
+ function expandEnvVars(value: string, extraEnv?: Record<string, string>, skipSensitiveNames = false): string {
921
1063
  return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => {
1064
+ if (skipSensitiveNames && isSensitiveEnvName(varName)) {
1065
+ return defaultValue !== undefined ? defaultValue : `\${${varName}}`;
1066
+ }
922
1067
  const envValue = extraEnv?.[varName] ?? Bun.env[varName];
923
1068
  if (envValue !== undefined) return envValue;
924
1069
  if (defaultValue !== undefined) return defaultValue;
@@ -929,17 +1074,17 @@ function expandEnvVars(value: string, extraEnv?: Record<string, string>): string
929
1074
  /**
930
1075
  * Recursively expand environment variables in an object.
931
1076
  */
932
- export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>): T {
1077
+ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>, skipSensitiveNames = false): T {
933
1078
  if (typeof obj === "string") {
934
- return expandEnvVars(obj, extraEnv) as T;
1079
+ return expandEnvVars(obj, extraEnv, skipSensitiveNames) as T;
935
1080
  }
936
1081
  if (Array.isArray(obj)) {
937
- return obj.map(item => expandEnvVarsDeep(item, extraEnv)) as T;
1082
+ return obj.map(item => expandEnvVarsDeep(item, extraEnv, skipSensitiveNames)) as T;
938
1083
  }
939
1084
  if (obj !== null && typeof obj === "object") {
940
1085
  const result: Record<string, unknown> = {};
941
1086
  for (const [key, value] of Object.entries(obj)) {
942
- result[key] = expandEnvVarsDeep(value, extraEnv);
1087
+ result[key] = expandEnvVarsDeep(value, extraEnv, skipSensitiveNames);
943
1088
  }
944
1089
  return result as T;
945
1090
  }
@@ -217,6 +217,12 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta, quiet = f
217
217
  cwd: serverConfig.cwd,
218
218
  url: serverConfig.url,
219
219
  headers: serverConfig.headers,
220
+ publicNetwork:
221
+ serverConfig.type === "http" &&
222
+ "publicNetwork" in serverConfig &&
223
+ (serverConfig as { publicNetwork?: boolean }).publicNetwork === true
224
+ ? true
225
+ : undefined,
220
226
  auth: serverConfig.auth,
221
227
  oauth: serverConfig.oauth,
222
228
  transport: serverConfig.type,
@@ -34,9 +34,14 @@ interface SSHConfigFile {
34
34
 
35
35
  function parsePort(value: number | string | undefined): number | undefined {
36
36
  if (value === undefined) return undefined;
37
- if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
38
- const parsed = Number.parseInt(value, 10);
39
- return Number.isNaN(parsed) ? undefined : parsed;
37
+ // The JSON is not validated against SSHConfigFile, so anything else (e.g. an
38
+ // array, which parseInt would coerce: parseInt([2222]) === 2222) is rejected.
39
+ if (typeof value !== "number" && typeof value !== "string") return undefined;
40
+ // `Number.parseInt` accepts trailing garbage (parseInt("22oops") === 22), so a
41
+ // string must be plain digits.
42
+ if (typeof value === "string" && !/^\d+$/.test(value)) return undefined;
43
+ const port = typeof value === "number" ? value : Number.parseInt(value, 10);
44
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : undefined;
40
45
  }
41
46
 
42
47
  function parseCompat(value: boolean | string | undefined): boolean | undefined {
@@ -114,7 +119,7 @@ async function loadSshJsonFile(
114
119
  warnings.push(`Failed to parse JSON in ${canonicalPath}`);
115
120
  return { items, warnings };
116
121
  }
117
- const config = expandEnvVarsDeep(parsed);
122
+ const config = expandEnvVarsDeep(parsed, undefined, level === "project");
118
123
  if (!config.hosts || typeof config.hosts !== "object") {
119
124
  warnings.push(`Missing hosts in ${canonicalPath}`);
120
125
  return { items, warnings };
@@ -8,7 +8,9 @@ const GJC_VAR = "$" + "{GJC_PLUGIN_ROOT}";
8
8
 
9
9
  export function substitutePluginRoot<T>(value: T, rootPath: string): T {
10
10
  if (typeof value === "string") {
11
- return value.replaceAll(CLAUDE_VAR, rootPath).replaceAll(GJC_VAR, rootPath) as T;
11
+ // Function replacers: a string replacement would expand `$&`, `$$`, `` $` ``
12
+ // and `$'` inside the install path instead of inserting it verbatim.
13
+ return value.replaceAll(CLAUDE_VAR, () => rootPath).replaceAll(GJC_VAR, () => rootPath) as T;
12
14
  }
13
15
  if (Array.isArray(value)) {
14
16
  return value.map(v => substitutePluginRoot(v, rootPath)) as T;
@@ -27,7 +27,7 @@ import type { ToolSession } from "../tools";
27
27
 
28
28
  const MAX_PATHS_PER_SESSION = 30;
29
29
  /** Snapshot generations retained per path (newest first). */
30
- const MAX_GENERATIONS_PER_PATH = 4;
30
+ const MAX_GENERATIONS_PER_PATH = 8;
31
31
 
32
32
  export interface FileReadSnapshot {
33
33
  /** 1-indexed line number → exact line content as observed by `read`/`search`. */