@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
@@ -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);
@@ -788,18 +880,55 @@ export async function scanSkillsFromDir(
788
880
  };
789
881
  const loadSkill = async (candidatePath: 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
+ 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
+ );
794
901
  return;
795
902
  }
796
- const stat = await fs.promises.stat(skillPath);
797
- if (!stat.isFile() || stat.nlink !== 1) {
903
+ const stat = await fs.promises.stat(skillPath, { bigint: true });
904
+ if (!stat.isFile() || stat.nlink !== 1n) {
798
905
  warnings.push(`Skill path is not a regular file: ${candidatePath}`);
799
906
  return;
800
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 : [];
801
927
  await SkillDiscoveryTestHooks.afterCandidateValidated?.(candidatePath);
802
- 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
+ }
803
932
  if (!frontmatter) {
804
933
  if (size > SKILL_FRONTMATTER_SCAN_TOTAL_BYTES) {
805
934
  warnings.push(
@@ -823,8 +952,9 @@ export async function scanSkillsFromDir(
823
952
  items.push({
824
953
  name,
825
954
  path: skillPath,
955
+ discoveryPath: candidatePath,
826
956
  loadContent: async () => {
827
- const content = await readSkillContentSafely(skillPath);
957
+ const content = await readSkillContentSafely(skillPath, fileIdentity, pinnedLinkIdentities);
828
958
  return parseFrontmatter(content, { source: skillPath }).body;
829
959
  },
830
960
  frontmatter: frontmatter as SkillFrontmatter,
@@ -923,8 +1053,17 @@ export async function readContainedFile(
923
1053
  * Expand environment variables in a string.
924
1054
  * Supports ${VAR} and ${VAR:-default} syntax.
925
1055
  */
926
- 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 {
927
1063
  return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => {
1064
+ if (skipSensitiveNames && isSensitiveEnvName(varName)) {
1065
+ return defaultValue !== undefined ? defaultValue : `\${${varName}}`;
1066
+ }
928
1067
  const envValue = extraEnv?.[varName] ?? Bun.env[varName];
929
1068
  if (envValue !== undefined) return envValue;
930
1069
  if (defaultValue !== undefined) return defaultValue;
@@ -935,17 +1074,17 @@ function expandEnvVars(value: string, extraEnv?: Record<string, string>): string
935
1074
  /**
936
1075
  * Recursively expand environment variables in an object.
937
1076
  */
938
- 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 {
939
1078
  if (typeof obj === "string") {
940
- return expandEnvVars(obj, extraEnv) as T;
1079
+ return expandEnvVars(obj, extraEnv, skipSensitiveNames) as T;
941
1080
  }
942
1081
  if (Array.isArray(obj)) {
943
- return obj.map(item => expandEnvVarsDeep(item, extraEnv)) as T;
1082
+ return obj.map(item => expandEnvVarsDeep(item, extraEnv, skipSensitiveNames)) as T;
944
1083
  }
945
1084
  if (obj !== null && typeof obj === "object") {
946
1085
  const result: Record<string, unknown> = {};
947
1086
  for (const [key, value] of Object.entries(obj)) {
948
- result[key] = expandEnvVarsDeep(value, extraEnv);
1087
+ result[key] = expandEnvVarsDeep(value, extraEnv, skipSensitiveNames);
949
1088
  }
950
1089
  return result as T;
951
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,
@@ -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 };
@@ -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`. */