@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -179,6 +179,86 @@ async function assertDirectoryNotSymlink(directoryPath: string): Promise<void> {
179
179
  }
180
180
  }
181
181
 
182
+ /** Group-write and world-write bits. Either one lets another user rename the session directory. */
183
+ const GROUP_OR_WORLD_WRITABLE = 0o022;
184
+
185
+ interface DirectoryIdentityStat {
186
+ isDirectory(): boolean;
187
+ isSymbolicLink(): boolean;
188
+ dev: number;
189
+ ino: number;
190
+ uid: number;
191
+ mode: number;
192
+ }
193
+
194
+ interface ManagedRootIdentity {
195
+ readonly dev: number;
196
+ readonly ino: number;
197
+ readonly uid: number;
198
+ }
199
+
200
+ function isUnsafeDirectory(stat: DirectoryIdentityStat): boolean {
201
+ return !stat.isDirectory() || stat.isSymbolicLink();
202
+ }
203
+
204
+ function managedRootIdentity(stat: DirectoryIdentityStat): ManagedRootIdentity {
205
+ return { dev: stat.dev, ino: stat.ino, uid: stat.uid };
206
+ }
207
+
208
+ function sameManagedRoot(stat: DirectoryIdentityStat, recorded: ManagedRootIdentity): boolean {
209
+ if (isUnsafeDirectory(stat) || stat.dev !== recorded.dev || stat.ino !== recorded.ino) return false;
210
+ // A later chmod to 0755 keeps the same directory. Only a writable bit, a new
211
+ // inode, or a different owner means the root was replaced or opened up.
212
+ if (process.platform === "win32") return true;
213
+ return stat.uid === recorded.uid && (stat.mode & GROUP_OR_WORLD_WRITABLE) === 0;
214
+ }
215
+
216
+ /**
217
+ * An existing managed session directory is adopted only when this user owns it
218
+ * and the group and world cannot write it. Windows has no POSIX owner bits.
219
+ */
220
+ function assertAdoptedManagedRoot(stat: DirectoryIdentityStat): void {
221
+ if (isUnsafeDirectory(stat)) throw new Error("Unsafe local:// root");
222
+ if (process.platform === "win32") return;
223
+ const uid = process.getuid?.();
224
+ if (uid === undefined || stat.uid !== uid || (stat.mode & GROUP_OR_WORLD_WRITABLE) !== 0) {
225
+ throw new Error("Unsafe local:// root");
226
+ }
227
+ }
228
+
229
+ function assertManagedParentSync(localRoot: string): void {
230
+ let parentStat: fsSync.Stats;
231
+ try {
232
+ parentStat = fsSync.lstatSync(path.dirname(localRoot));
233
+ } catch {
234
+ throw new Error("Unsafe local:// root");
235
+ }
236
+ assertAdoptedManagedRoot(parentStat);
237
+ }
238
+
239
+ /**
240
+ * Rebind a pinned root only when this call created it because the path was
241
+ * absent. An already-present directory must still be the pinned one.
242
+ */
243
+ function acceptManagedRootForInit(
244
+ localRoot: string,
245
+ rootStat: DirectoryIdentityStat,
246
+ recorded: ManagedRootIdentity | undefined,
247
+ adoptedExisting: boolean,
248
+ ): void {
249
+ if (recorded && adoptedExisting) {
250
+ if (sameManagedRoot(rootStat, recorded)) return;
251
+ const pending = pendingCreatedRoots.get(localRoot);
252
+ if (pending && sameManagedRoot(rootStat, pending)) return;
253
+ throw new Error("Unsafe local:// root");
254
+ }
255
+ if (recorded || adoptedExisting) {
256
+ assertAdoptedManagedRoot(rootStat);
257
+ return;
258
+ }
259
+ if (isUnsafeDirectory(rootStat)) throw new Error("Unsafe local:// root");
260
+ }
261
+
182
262
  const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
183
263
  const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
184
264
 
@@ -508,9 +588,33 @@ async function migrateLegacyLocal(
508
588
  }
509
589
  }
510
590
 
511
- const initializedLocalRoots = new Set<string>();
591
+ const initializedLocalRoots = new Map<string, ManagedRootIdentity>();
592
+ /** A directory this process created and has not finished migrating. */
593
+ const pendingCreatedRoots = new Map<string, ManagedRootIdentity>();
512
594
  const initializingLocalRoots = new Map<string, Promise<string>>();
513
595
 
596
+ function assertRememberedManagedRoot(localRoot: string, recorded: ManagedRootIdentity): void {
597
+ let stat: fsSync.Stats;
598
+ try {
599
+ stat = fsSync.lstatSync(localRoot);
600
+ } catch {
601
+ throw new Error("Unsafe local:// root");
602
+ }
603
+ if (!sameManagedRoot(stat, recorded)) throw new Error("Unsafe local:// root");
604
+ }
605
+
606
+ function rememberManagedRoot(localRoot: string, expected: ManagedRootIdentity): void {
607
+ let stat: fsSync.Stats;
608
+ try {
609
+ stat = fsSync.lstatSync(localRoot);
610
+ } catch {
611
+ throw new Error("Unsafe local:// root");
612
+ }
613
+ if (!sameManagedRoot(stat, expected)) throw new Error("Unsafe local:// root");
614
+ initializedLocalRoots.set(localRoot, managedRootIdentity(stat));
615
+ pendingCreatedRoots.delete(localRoot);
616
+ }
617
+
514
618
  /**
515
619
  * Completes the mandatory legacy migration gate for a local root.
516
620
  * Call and await this before using the synchronous path resolver for reads or writes.
@@ -559,16 +663,21 @@ async function initializeManagedLocalRoot(options: LocalProtocolOptions, localRo
559
663
 
560
664
  await fs.mkdir(scratchParent, { recursive: true, mode: 0o700 });
561
665
  await assertDirectoryNotSymlink(scratchParent);
666
+ assertAdoptedManagedRoot(await fs.lstat(scratchParent));
667
+ let adoptedExisting = false;
562
668
  try {
563
669
  await fs.mkdir(localRoot, { mode: 0o700 });
564
670
  } catch (error) {
565
671
  if (error instanceof Error && "code" in error && error.code === "EEXIST") {
566
- await assertDirectoryNotSymlink(localRoot);
672
+ adoptedExisting = true;
567
673
  } else {
568
674
  throw error;
569
675
  }
570
676
  }
571
- await assertDirectoryNotSymlink(localRoot);
677
+ const rootStat = await fs.lstat(localRoot);
678
+ const recorded = initializedLocalRoots.get(localRoot);
679
+ acceptManagedRootForInit(localRoot, rootStat, recorded, adoptedExisting);
680
+ if (!adoptedExisting) pendingCreatedRoots.set(localRoot, managedRootIdentity(rootStat));
572
681
  await migrateLegacyLocal(options, localRoot, scratchParent);
573
682
 
574
683
  const resolvedScratchParent = await fs.realpath(scratchParent);
@@ -577,7 +686,10 @@ async function initializeManagedLocalRoot(options: LocalProtocolOptions, localRo
577
686
  if (resolvedLocalRoot === resolvedScratchParent) {
578
687
  throw new Error("Unsafe local:// root");
579
688
  }
580
- initializedLocalRoots.add(resolvedLocalRoot);
689
+ // Key the lexical path the sync resolver checks. realpath(/var -> /private/var)
690
+ // is a different string for the same directory, so caching only the real path
691
+ // let a later rename-swap skip this identity check.
692
+ rememberManagedRoot(localRoot, managedRootIdentity(rootStat));
581
693
  return resolvedLocalRoot;
582
694
  }
583
695
 
@@ -587,18 +699,39 @@ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions,
587
699
  initializeExplicitLocalRootSync(explicitRoot);
588
700
  return;
589
701
  }
590
- if (initializedLocalRoots.has(localRoot)) return;
702
+ const recorded = initializedLocalRoots.get(localRoot);
703
+ if (recorded) {
704
+ assertManagedParentSync(localRoot);
705
+ let present: fsSync.Stats | undefined;
706
+ try {
707
+ present = fsSync.lstatSync(localRoot);
708
+ } catch (error) {
709
+ if (!isEnoent(error)) throw new Error("Unsafe local:// root");
710
+ }
711
+ if (present) {
712
+ if (sameManagedRoot(present, recorded)) {
713
+ assertRememberedManagedRoot(localRoot, recorded);
714
+ return;
715
+ }
716
+ const pending = pendingCreatedRoots.get(localRoot);
717
+ if (!(pending && sameManagedRoot(present, pending))) throw new Error("Unsafe local:// root");
718
+ }
719
+ }
591
720
  const scratchParent = path.dirname(localRoot);
592
721
  fsSync.mkdirSync(scratchParent, { recursive: true, mode: 0o700 });
593
722
  const parentStat = fsSync.lstatSync(scratchParent);
594
723
  if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
724
+ assertAdoptedManagedRoot(parentStat);
725
+ let adoptedExisting = false;
595
726
  try {
596
727
  fsSync.mkdirSync(localRoot, { mode: 0o700 });
597
728
  } catch (error) {
598
729
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
730
+ adoptedExisting = true;
599
731
  }
600
732
  const rootStat = fsSync.lstatSync(localRoot);
601
- if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
733
+ acceptManagedRootForInit(localRoot, rootStat, recorded, adoptedExisting);
734
+ if (!adoptedExisting) pendingCreatedRoots.set(localRoot, managedRootIdentity(rootStat));
602
735
  const marker = path.join(localRoot, LEGACY_MIGRATION_MARKER);
603
736
  try {
604
737
  // Accept exactly the marker states the async gate treats as settled. A
@@ -608,7 +741,7 @@ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions,
608
741
  // async gate had already completed.
609
742
  if (!isSettledMigrationMarkerValue(fsSync.readFileSync(marker, "utf8")))
610
743
  throw new Error("Unsafe local:// migration marker");
611
- initializedLocalRoots.add(localRoot);
744
+ rememberManagedRoot(localRoot, managedRootIdentity(rootStat));
612
745
  return;
613
746
  } catch (error) {
614
747
  if (!isEnoent(error)) throw error;
@@ -629,7 +762,7 @@ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions,
629
762
  if (!isSettledMigrationMarkerValue(fsSync.readFileSync(marker, "utf8")))
630
763
  throw new Error("Unsafe local:// migration marker");
631
764
  }
632
- initializedLocalRoots.add(localRoot);
765
+ rememberManagedRoot(localRoot, managedRootIdentity(rootStat));
633
766
  }
634
767
 
635
768
  export function resolveLocalRoot(options: LocalProtocolOptions): string {
@@ -696,6 +829,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
696
829
  LocalProtocolHandler.#override = undefined;
697
830
  LocalProtocolHandler.#ownedOverrides = [];
698
831
  initializedLocalRoots.clear();
832
+ pendingCreatedRoots.clear();
699
833
  initializingLocalRoots.clear();
700
834
  }
701
835
 
package/src/lsp/index.ts CHANGED
@@ -77,6 +77,7 @@ import {
77
77
  symbolKindToIcon,
78
78
  uriToFile,
79
79
  } from "./utils";
80
+ import { assertInsideWorkspace } from "./workspace-path";
80
81
 
81
82
  export type { LspServerStatus } from "./client";
82
83
  export type { LspToolDetails } from "./types";
@@ -347,6 +348,17 @@ interface FileRenamePair {
347
348
  * For files this is a single pair. For directories this walks every regular file underneath
348
349
  * and produces a parallel pair anchored at the new directory root.
349
350
  */
351
+ /**
352
+ * POSIX `/` contains every absolute path, and the shared helper's prefix for
353
+ * it is `//`, so skip containment there. A Windows drive root is only that
354
+ * volume, so it is checked. Any other platform root keeps the previous skip.
355
+ */
356
+ function workspaceRootSkipsContainment(root: string): boolean {
357
+ if (root === "/") return true;
358
+ if (/^[A-Za-z]:[\\/]$/.test(root)) return false;
359
+ return root === path.parse(root).root;
360
+ }
361
+
350
362
  async function enumerateRenamePairs(
351
363
  source: string,
352
364
  dest: string,
@@ -1525,6 +1537,23 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1525
1537
  }
1526
1538
  }
1527
1539
 
1540
+ // Reject every path this rename will write before the first write. Check the
1541
+ // caller signal after each await, including the last one.
1542
+ const cwd = this.session.cwd;
1543
+ throwIfAborted(signal);
1544
+ const workspaceRoot = await fs.promises.realpath(cwd);
1545
+ throwIfAborted(signal);
1546
+ if (!workspaceRootSkipsContainment(workspaceRoot)) {
1547
+ await assertInsideWorkspace(cwd, source);
1548
+ throwIfAborted(signal);
1549
+ await assertInsideWorkspace(cwd, dest);
1550
+ throwIfAborted(signal);
1551
+ for (const uri of acceptedByUri.keys()) {
1552
+ await assertInsideWorkspace(cwd, uriToFile(uri));
1553
+ throwIfAborted(signal);
1554
+ }
1555
+ }
1556
+
1528
1557
  for (const [uri, bucket] of acceptedByUri) {
1529
1558
  const filePath = uriToFile(uri);
1530
1559
  await applyTextEdits(filePath, bucket.edits);
@@ -6,7 +6,30 @@ function isEnoent(error: unknown): boolean {
6
6
  return (error as NodeJS.ErrnoException).code === "ENOENT";
7
7
  }
8
8
 
9
+ /** Drive-letter root such as `C:\`. A UNC share root is not a drive root. */
10
+ function windowsDriveLetter(root: string): string | null {
11
+ const match = /^([A-Za-z]):[\\/]$/.exec(root);
12
+ return match ? match[1].toLowerCase() : null;
13
+ }
14
+
15
+ /**
16
+ * A Windows drive root already ends in a separator. Prefixing another one
17
+ * (`C:\` + `\`) matches nothing on that volume, so compare the drive letter.
18
+ * Same-volume paths stay inside, including `\\?\C:\`. Other drives and UNC
19
+ * shares do not.
20
+ */
21
+ function sameWindowsDrive(driveRoot: string, candidate: string): boolean {
22
+ const letter = windowsDriveLetter(driveRoot);
23
+ if (!letter) return false;
24
+ const normalized = candidate.replace(/\//g, "\\");
25
+ const extended = /^\\\\\?\\([A-Za-z]):\\/.exec(normalized);
26
+ if (extended) return extended[1].toLowerCase() === letter;
27
+ if (normalized.startsWith("\\\\")) return false;
28
+ return path.win32.parse(normalized).root.toLowerCase() === `${letter}:\\`;
29
+ }
30
+
9
31
  function escapes(root: string, candidate: string): boolean {
32
+ if (windowsDriveLetter(root)) return !sameWindowsDrive(root, candidate);
10
33
  return candidate !== root && !candidate.startsWith(`${root}${path.sep}`);
11
34
  }
12
35
 
@@ -314,5 +314,62 @@ export async function planMigration(input: PlanInput): Promise<PlanOutput> {
314
314
  }
315
315
  }
316
316
 
317
+ // 4. Instructions — all selected sources target the same scoped AGENTS.md.
318
+ const plannedInstructions = new Set<string>();
319
+ for (const result of input.results) {
320
+ for (const candidate of result.instructionCandidates) {
321
+ const destination = input.destinations.instructionsPath;
322
+ const base = { source: candidate.source, type: "instruction" as const, name: candidate.name, destination };
323
+ if (plannedInstructions.has(destination)) {
324
+ actions.push({
325
+ ...base,
326
+ operation: "skip",
327
+ status: "skipped_exists",
328
+ reason: "another selected source already targets this file; import one source at a time",
329
+ });
330
+ continue;
331
+ }
332
+
333
+ let exists = false;
334
+ try {
335
+ const stat = await fs.lstat(destination);
336
+ exists = true;
337
+ if (!stat.isFile() || stat.isSymbolicLink()) {
338
+ actions.push({
339
+ ...base,
340
+ operation: input.force ? "fail" : "skip",
341
+ status: input.force ? "failed_invalid_destination" : "skipped_exists",
342
+ reason: "destination is not a regular file; refusing to replace it",
343
+ });
344
+ plannedInstructions.add(destination);
345
+ continue;
346
+ }
347
+ } catch (error) {
348
+ if (!isEnoent(error)) {
349
+ actions.push({
350
+ ...base,
351
+ operation: "fail",
352
+ status: "failed_io",
353
+ reason: `failed to inspect destination: ${(error as Error).message}`,
354
+ });
355
+ plannedInstructions.add(destination);
356
+ continue;
357
+ }
358
+ }
359
+
360
+ if (exists && !input.force) {
361
+ actions.push({ ...base, operation: "skip", status: "skipped_exists" });
362
+ } else {
363
+ actions.push({
364
+ ...base,
365
+ operation: exists ? "update" : "create",
366
+ status: exists ? "updated" : "imported",
367
+ instruction: { content: candidate.content },
368
+ });
369
+ }
370
+ plannedInstructions.add(destination);
371
+ }
372
+ }
373
+
317
374
  return { actions, warnings };
318
375
  }
@@ -1,16 +1,29 @@
1
1
  /**
2
- * Claude Code adapter: reads `~/.claude.json` (mcpServers) and `~/.claude/skills`.
2
+ * Claude Code adapter: reads `~/.claude.json` (mcpServers), `~/.claude/skills`,
3
+ * and the selected scope's `CLAUDE.md`.
3
4
  */
4
5
  import * as path from "node:path";
5
6
  import type { AdapterResult, McpCandidate } from "../types";
6
- import { type Adapter, type AdapterOptions, collectSkillDir, parseSourceJson, readSourceText } from "./index";
7
+ import {
8
+ type Adapter,
9
+ type AdapterOptions,
10
+ collectInstructionFile,
11
+ collectSkillDir,
12
+ parseSourceJson,
13
+ readSourceText,
14
+ } from "./index";
7
15
 
8
16
  const SOURCE = "claude-code" as const;
9
17
 
10
18
  export const claudeCodeAdapter: Adapter = {
11
19
  source: SOURCE,
12
- async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
13
- const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
20
+ async collect({ homeDir, cwd, scope }: AdapterOptions): Promise<AdapterResult> {
21
+ const result: AdapterResult = {
22
+ mcpCandidates: [],
23
+ skillCandidates: [],
24
+ instructionCandidates: [],
25
+ diagnostics: [],
26
+ };
14
27
 
15
28
  const configPath = path.join(homeDir, ".claude.json");
16
29
  const read = await readSourceText(configPath, SOURCE, "mcp");
@@ -34,6 +47,12 @@ export const claudeCodeAdapter: Adapter = {
34
47
  result.skillCandidates.push(...skills.candidates);
35
48
  result.diagnostics.push(...skills.diagnostics);
36
49
 
50
+ const instructionsPath =
51
+ scope === "project" ? path.join(cwd, "CLAUDE.md") : path.join(homeDir, ".claude", "CLAUDE.md");
52
+ const instructions = await collectInstructionFile(instructionsPath, SOURCE);
53
+ result.instructionCandidates.push(...instructions.candidates);
54
+ result.diagnostics.push(...instructions.diagnostics);
55
+
37
56
  return result;
38
57
  },
39
58
  };
@@ -1,11 +1,18 @@
1
1
  /**
2
- * Codex adapter: reads `~/.codex/config.toml` ([mcp_servers]) and `~/.codex/prompts`.
2
+ * Codex adapter: reads `~/.codex/config.toml` ([mcp_servers]), `~/.codex/prompts`,
3
+ * and user-level `~/.codex/AGENTS.md` instructions. Project AGENTS.md is native GJC input.
3
4
  */
4
5
 
5
6
  import * as path from "node:path";
6
7
  import { TOML } from "bun";
7
8
  import type { AdapterResult, McpCandidate, SourceDiagnostic } from "../types";
8
- import { type Adapter, type AdapterOptions, collectMarkdownPrompts, readSourceText } from "./index";
9
+ import {
10
+ type Adapter,
11
+ type AdapterOptions,
12
+ collectInstructionFile,
13
+ collectMarkdownPrompts,
14
+ readSourceText,
15
+ } from "./index";
9
16
 
10
17
  const SOURCE = "codex" as const;
11
18
 
@@ -40,8 +47,13 @@ function parseToml(
40
47
 
41
48
  export const codexAdapter: Adapter = {
42
49
  source: SOURCE,
43
- async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
44
- const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
50
+ async collect({ homeDir, scope }: AdapterOptions): Promise<AdapterResult> {
51
+ const result: AdapterResult = {
52
+ mcpCandidates: [],
53
+ skillCandidates: [],
54
+ instructionCandidates: [],
55
+ diagnostics: [],
56
+ };
45
57
 
46
58
  const configPath = path.join(homeDir, ".codex", "config.toml");
47
59
  const read = await readSourceText(configPath, SOURCE, "mcp");
@@ -65,6 +77,12 @@ export const codexAdapter: Adapter = {
65
77
  result.skillCandidates.push(...prompts.candidates);
66
78
  result.diagnostics.push(...prompts.diagnostics);
67
79
 
80
+ if (scope === "user") {
81
+ const instructions = await collectInstructionFile(path.join(homeDir, ".codex", "AGENTS.md"), SOURCE);
82
+ result.instructionCandidates.push(...instructions.candidates);
83
+ result.diagnostics.push(...instructions.diagnostics);
84
+ }
85
+
68
86
  return result;
69
87
  },
70
88
  };
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Source adapters for `gjc migrate`.
3
3
  *
4
- * Each adapter reads GLOBAL/home config for one source agent and returns
5
- * normalized MCP + skill candidates plus source-level diagnostics. Adapters never
6
- * read project-level config and never connect to anything.
4
+ * Each adapter returns normalized home-scoped MCP/skill candidates, selected-scope
5
+ * instruction candidates, and source-level diagnostics. Adapters never connect to
6
+ * anything.
7
7
  */
8
8
  import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
9
10
  import { isEnoent } from "@gajae-code/utils";
10
11
  import { normalizeSkill } from "../skill-normalizer";
11
- import type { AdapterResult, MigrateSource, SkillCandidate, SourceDiagnostic } from "../types";
12
+ import type { AdapterResult, InstructionCandidate, MigrateSource, SkillCandidate, SourceDiagnostic } from "../types";
12
13
  import { claudeCodeAdapter } from "./claude-code";
13
14
  import { codexAdapter } from "./codex";
14
15
  import { opencodeAdapter } from "./opencode";
@@ -16,6 +17,10 @@ import { opencodeAdapter } from "./opencode";
16
17
  export interface AdapterOptions {
17
18
  /** Home directory root; overridable for tests. */
18
19
  homeDir: string;
20
+ /** Source scope for instructions; other source files remain home-scoped. */
21
+ scope: "project" | "user";
22
+ /** Project root used when importing project instructions. */
23
+ cwd: string;
19
24
  }
20
25
 
21
26
  export interface Adapter {
@@ -33,6 +38,19 @@ export function getAdapter(source: MigrateSource): Adapter {
33
38
  return ADAPTERS[source];
34
39
  }
35
40
 
41
+ /** Collect one source instruction file without interpreting or rewriting its content. */
42
+ export async function collectInstructionFile(
43
+ filePath: string,
44
+ source: MigrateSource,
45
+ ): Promise<{ candidates: InstructionCandidate[]; diagnostics: SourceDiagnostic[] }> {
46
+ const read = await readSourceText(filePath, source, "instruction");
47
+ if ("diagnostic" in read) return { candidates: [], diagnostics: [read.diagnostic] };
48
+ return {
49
+ candidates: [{ source, name: path.basename(filePath), content: read.text }],
50
+ diagnostics: [],
51
+ };
52
+ }
53
+
36
54
  /** Read a text file, classifying absence/IO errors into source diagnostics. */
37
55
  export async function readSourceText(
38
56
  filePath: string,
@@ -18,7 +18,12 @@ const SOURCE = "opencode" as const;
18
18
  export const opencodeAdapter: Adapter = {
19
19
  source: SOURCE,
20
20
  async collect({ homeDir }: AdapterOptions): Promise<AdapterResult> {
21
- const result: AdapterResult = { mcpCandidates: [], skillCandidates: [], diagnostics: [] };
21
+ const result: AdapterResult = {
22
+ mcpCandidates: [],
23
+ skillCandidates: [],
24
+ instructionCandidates: [],
25
+ diagnostics: [],
26
+ };
22
27
  const baseDir = path.join(homeDir, ".config", "opencode");
23
28
 
24
29
  const configPath = path.join(baseDir, "opencode.json");
@@ -9,6 +9,7 @@
9
9
  import * as fsSync from "node:fs";
10
10
  import * as fs from "node:fs/promises";
11
11
  import * as path from "node:path";
12
+ import { getTrustedHomeDir } from "@gajae-code/utils";
12
13
  import { upsertMCPServer } from "../runtime-mcp/config-writer";
13
14
  import type { MigrateAction } from "./types";
14
15
 
@@ -19,17 +20,23 @@ function trustedSkillDestinationBase(destination: string): string {
19
20
  return path.basename(scopeRoot) === ".gjc" ? path.dirname(scopeRoot) : scopeRoot;
20
21
  }
21
22
 
22
- async function ensureRealDirectoryPathNoFollow(directory: string, trustedBase: string): Promise<void> {
23
+ async function ensureRealDirectoryPathNoFollow(
24
+ directory: string,
25
+ trustedBase: string,
26
+ label = "skill destination",
27
+ createMissing = true,
28
+ mode?: number,
29
+ ): Promise<void> {
23
30
  const resolvedBase = path.resolve(trustedBase);
24
31
  const resolvedDirectory = path.resolve(directory);
25
32
  const relative = path.relative(resolvedBase, resolvedDirectory);
26
33
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
27
- throw new Error(`skill destination escapes trusted base: ${resolvedDirectory}`);
34
+ throw new Error(`${label} escapes trusted base: ${resolvedDirectory}`);
28
35
  }
29
36
 
30
37
  const baseStat = await fs.stat(resolvedBase);
31
38
  if (!baseStat.isDirectory()) {
32
- throw new Error(`skill destination base is not a directory: ${resolvedBase}`);
39
+ throw new Error(`${label} base is not a directory: ${resolvedBase}`);
33
40
  }
34
41
 
35
42
  let current = resolvedBase;
@@ -38,14 +45,17 @@ async function ensureRealDirectoryPathNoFollow(directory: string, trustedBase: s
38
45
  try {
39
46
  const stat = await fs.lstat(current);
40
47
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
41
- throw new Error(`skill destination ancestor is not a real directory: ${current}`);
48
+ throw new Error(`${label} ancestor is not a real directory: ${current}`);
42
49
  }
43
50
  } catch (error) {
44
51
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
45
- await fs.mkdir(current);
52
+ // A missing project `.gjc` is created by the MCP writer (mode 0o700).
53
+ // Do not mkdir here, or that mode is lost. An existing symlink still fails above.
54
+ if (!createMissing) return;
55
+ await fs.mkdir(current, mode === undefined ? undefined : { mode });
46
56
  const stat = await fs.lstat(current);
47
57
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
48
- throw new Error(`skill destination ancestor is not a real directory: ${current}`);
58
+ throw new Error(`${label} ancestor is not a real directory: ${current}`);
49
59
  }
50
60
  }
51
61
  }
@@ -66,7 +76,56 @@ async function writeSkillFileNoFollow(destination: string, content: string): Pro
66
76
  }
67
77
  }
68
78
 
69
- export async function executeActions(actions: MigrateAction[]): Promise<MigrateAction[]> {
79
+ async function ensureInstructionDirectory(directory: string, project: boolean): Promise<void> {
80
+ let trustedBase = path.dirname(directory);
81
+ if (!project) {
82
+ const home = path.resolve(getTrustedHomeDir());
83
+ const resolvedDirectory = path.resolve(directory);
84
+ const relative = path.relative(home, resolvedDirectory);
85
+ const isWithinHome =
86
+ relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
87
+ if (isWithinHome) {
88
+ trustedBase = home;
89
+ } else {
90
+ let current = path.dirname(resolvedDirectory);
91
+ while (true) {
92
+ try {
93
+ const stat = await fs.lstat(current);
94
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
95
+ throw new Error(`user instruction destination ancestor is not a real directory: ${current}`);
96
+ }
97
+ trustedBase = current;
98
+ break;
99
+ } catch (error) {
100
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
101
+ const parent = path.dirname(current);
102
+ if (parent === current) throw error;
103
+ current = parent;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ await ensureRealDirectoryPathNoFollow(directory, trustedBase, "instruction destination", true, 0o700);
109
+ }
110
+
111
+ async function writeInstructionFileNoFollow(destination: string, content: string, project: boolean): Promise<void> {
112
+ await ensureInstructionDirectory(path.dirname(destination), project);
113
+ const handle = await fs.open(
114
+ destination,
115
+ fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW,
116
+ 0o666,
117
+ );
118
+ try {
119
+ await handle.writeFile(content, "utf-8");
120
+ } finally {
121
+ await handle.close();
122
+ }
123
+ }
124
+
125
+ export async function executeActions(
126
+ actions: MigrateAction[],
127
+ options?: { project?: boolean },
128
+ ): Promise<MigrateAction[]> {
70
129
  const out: MigrateAction[] = [];
71
130
  for (const action of actions) {
72
131
  if (action.operation !== "create" && action.operation !== "update") {
@@ -75,6 +134,17 @@ export async function executeActions(actions: MigrateAction[]): Promise<MigrateA
75
134
  }
76
135
  try {
77
136
  if (action.type === "mcp" && action.mcp && action.name && action.destination) {
137
+ // `project` is the migrate scope, not a guess from the directory name.
138
+ // A user agent dir may itself be named `.gjc`.
139
+ if (options?.project) {
140
+ const directory = path.dirname(action.destination);
141
+ await ensureRealDirectoryPathNoFollow(
142
+ directory,
143
+ path.dirname(path.resolve(directory)),
144
+ "mcp destination",
145
+ false,
146
+ );
147
+ }
78
148
  await upsertMCPServer(action.destination, action.name, action.mcp.config, {
79
149
  force: action.operation === "update",
80
150
  });
@@ -82,6 +152,13 @@ export async function executeActions(actions: MigrateAction[]): Promise<MigrateA
82
152
  } else if (action.type === "skill" && action.skill && action.destination) {
83
153
  await writeSkillFileNoFollow(action.destination, action.skill.content);
84
154
  out.push(action);
155
+ } else if (action.type === "instruction" && action.instruction && action.destination) {
156
+ await writeInstructionFileNoFollow(
157
+ action.destination,
158
+ action.instruction.content,
159
+ options?.project ?? false,
160
+ );
161
+ out.push(action);
85
162
  } else {
86
163
  out.push(action);
87
164
  }