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