@gajae-code/coding-agent 0.18.10 → 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 +295 -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
@@ -62,6 +62,7 @@ import {
62
62
  validateAutoroutingEffective,
63
63
  validateAutoroutingLocal,
64
64
  } from "./autorouting-contract";
65
+ import type { ConfigHotReloadFileSnapshot } from "./config-hot-reload";
65
66
  import { isModelSelectorValue, type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
66
67
 
67
68
  import {
@@ -92,6 +93,53 @@ export interface RawSettings {
92
93
  [key: string]: unknown;
93
94
  }
94
95
 
96
+ /** Validated candidate view. `get` includes local patches and effective overlays. */
97
+ export interface SettingsGlobalConfigCandidate {
98
+ readonly configPath: string;
99
+ readonly diagnostics: SettingsSchemaReport;
100
+ get<P extends SettingPath>(settingPath: P): SettingValue<P>;
101
+ /** Candidate global values include pending local patches, but exclude other layers. */
102
+ getGlobal<P extends SettingPath>(settingPath: P): SettingValue<P> | undefined;
103
+ }
104
+
105
+ export interface SettingsConfigReloadPublication {
106
+ finalize(): void;
107
+ rollback(): void;
108
+ }
109
+
110
+ export class SettingsGlobalConfigReloadError extends Error {
111
+ constructor(
112
+ message: string,
113
+ readonly configPath: string | null,
114
+ readonly diagnostics?: SettingsSchemaReport,
115
+ ) {
116
+ super(message);
117
+ this.name = "SettingsGlobalConfigReloadError";
118
+ }
119
+ }
120
+
121
+ interface SettingsGlobalConfigCandidateState {
122
+ owner: Settings;
123
+ configPath: string;
124
+ globalWithPendingPatches: RawSettings;
125
+ effective: RawSettings;
126
+ diagnostics: SettingsSchemaReport;
127
+ rawConfig: RawSettings;
128
+ currentGlobal: RawSettings;
129
+ currentProject: RawSettings;
130
+ currentOverrides: RawSettings;
131
+ currentMerged: RawSettings;
132
+ cwd: string;
133
+ sourcePresent: boolean;
134
+ futureSchemaVersion: boolean;
135
+ published: boolean;
136
+ }
137
+
138
+ const settingsGlobalConfigCandidateStates = new WeakMap<
139
+ SettingsGlobalConfigCandidate,
140
+ SettingsGlobalConfigCandidateState
141
+ >();
142
+
95
143
  const UNSAFE_EDIT_VARIANT_PATTERNS = new Set(["__proto__", "constructor", "prototype"]);
96
144
 
97
145
  const CONFIG_ROOT_WORKFLOW_MIGRATION_KEYS: readonly WorkflowSettingKey[] = [
@@ -263,6 +311,12 @@ export class NotificationSettingsOverrideError extends Error {
263
311
  }
264
312
 
265
313
  const LOCAL_NOTIFICATION_SETTING_KEYS = new Set(["terminalBell", "bellOnComplete", "bellOnApproval", "bellOnAsk"]);
314
+ /** `auth.*` keys that select credentials; startup reads them from global config only, so project copies are dropped. */
315
+ const GLOBAL_ONLY_AUTH_SETTING_KEYS = new Set([
316
+ "credentialPins",
317
+ "credentialPinStoreIdentity",
318
+ "credentialPreferences",
319
+ ]);
266
320
  const LOCAL_NOTIFICATION_SETTING_PATHS = new Set(
267
321
  [...LOCAL_NOTIFICATION_SETTING_KEYS].map(key => `notifications.${key}`),
268
322
  );
@@ -336,6 +390,11 @@ const GLOBAL_ONLY_SETTINGS = new Set<SettingPath>([
336
390
  "paseo.autoImport",
337
391
  "ui.language",
338
392
  "telemetry.enabled",
393
+ // Destination of the operator Hindsight server. A project file must not redirect it.
394
+ "hindsight.apiUrl",
395
+ // Operator consent for repository skills. A project file must not flip it.
396
+ "skills.trustProjectSkills",
397
+ "skills.enablePiProject",
339
398
  ]);
340
399
  const LEGACY_THEME_NAME_REPLACEMENTS = {
341
400
  dark: "red-claw",
@@ -544,6 +603,8 @@ export class Settings implements NotificationSettingsReader {
544
603
  #schemaMigrationPending = false;
545
604
  /** A newer config schema must never be rewritten by legacy migrations. */
546
605
  #futureSchemaVersion = false;
606
+ /** Set only after a valid disk-backed global config has been loaded or published. */
607
+ #hasAcceptedGlobalConfigFile = false;
547
608
  #hasMalformedConfigRoot = false;
548
609
  /** YAML syntax was unrecoverable, so the loaded defaults are read-only until config.yml is repaired. */
549
610
  #hasRecoveredConfigSyntax = false;
@@ -745,6 +806,357 @@ export class Settings implements NotificationSettingsReader {
745
806
  return () => this.#changeListeners.delete(listener);
746
807
  }
747
808
 
809
+ /** The resolved global config.yml path, or null for in-memory settings. */
810
+ getGlobalConfigPath(): string | null {
811
+ return this.#configPath;
812
+ }
813
+
814
+ /**
815
+ * Validate the watcher's exact snapshot without writing or rereading it.
816
+ * Rebase pending local patches after prior durable operations settle.
817
+ */
818
+ async stageGlobalConfigReload(snapshot: ConfigHotReloadFileSnapshot): Promise<SettingsGlobalConfigCandidate> {
819
+ const configPath = this.#configPath;
820
+ if (!configPath) {
821
+ throw new SettingsGlobalConfigReloadError(
822
+ "Global config reload requires a disk-backed Settings instance.",
823
+ null,
824
+ );
825
+ }
826
+
827
+ if (path.resolve(snapshot.path) !== path.resolve(configPath)) {
828
+ throw new SettingsGlobalConfigReloadError("Global config snapshot path does not match Settings.", configPath);
829
+ }
830
+ const pendingAtStage = structuredClone(this.#pendingPatchesInGenerationOrder());
831
+ return enqueueAtomicYamlOperation(configPath, async () => {
832
+ const sourcePresent = snapshot.text !== null;
833
+ const content = snapshot.text ?? "";
834
+ if (!sourcePresent) {
835
+ if (this.#hasAcceptedGlobalConfigFile) {
836
+ const diagnostics: SettingsSchemaReport = {
837
+ valid: false,
838
+ issues: [
839
+ {
840
+ path: "config.yml",
841
+ kind: "invalid",
842
+ detail: "The previously accepted global config.yml is missing.",
843
+ },
844
+ ],
845
+ };
846
+ throw new SettingsGlobalConfigReloadError(
847
+ "Cannot stage global config reload because the previously accepted config.yml is missing.",
848
+ configPath,
849
+ diagnostics,
850
+ );
851
+ }
852
+ }
853
+
854
+ let rawConfig: RawSettings = {};
855
+ if (content.trim() !== "") {
856
+ let parsed: unknown;
857
+ try {
858
+ parsed = YAML.parse(content);
859
+ } catch {
860
+ const diagnostics: SettingsSchemaReport = {
861
+ valid: false,
862
+ issues: [
863
+ {
864
+ path: "config.yml",
865
+ kind: "invalid",
866
+ detail: "Configuration YAML syntax is invalid; repair config.yml before reloading.",
867
+ },
868
+ ],
869
+ };
870
+ throw new SettingsGlobalConfigReloadError(
871
+ "Cannot stage global config reload because config.yml has invalid YAML syntax.",
872
+ configPath,
873
+ diagnostics,
874
+ );
875
+ }
876
+ if (parsed !== undefined) {
877
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
878
+ const diagnostics: SettingsSchemaReport = {
879
+ valid: false,
880
+ issues: [
881
+ {
882
+ path: "config.yml",
883
+ kind: "invalid",
884
+ detail: "Configuration root must be a YAML mapping.",
885
+ },
886
+ ],
887
+ };
888
+ throw new SettingsGlobalConfigReloadError(
889
+ "Cannot stage global config reload because the config.yml root is not a mapping.",
890
+ configPath,
891
+ diagnostics,
892
+ );
893
+ }
894
+ rawConfig = parsed as RawSettings;
895
+ }
896
+ }
897
+
898
+ const configSchemaVersion = normalizeConfigSchemaVersion(rawConfig.configSchemaVersion);
899
+ const futureSchemaVersion =
900
+ typeof configSchemaVersion === "number" && configSchemaVersion > CONFIG_SCHEMA_VERSION;
901
+ const migratedRawConfig = this.#migrateRawSettings(structuredClone(rawConfig));
902
+ const reconciled = reconcileSettingsSchema(migratedRawConfig);
903
+ const diagnostics = reconciled.report;
904
+ for (const [settingPath, pathSegments] of [
905
+ ["modelRoles", ["modelRoles"]],
906
+ ["task.agentModelOverrides", ["task", "agentModelOverrides"]],
907
+ ] as const) {
908
+ const value = getByPath(reconciled.settings, [...pathSegments]);
909
+ if (value === undefined) continue;
910
+ const record = rawSettingsRecord(value);
911
+ if (!record || Object.keys(shallowModelSelectorRecord(record)).length !== Object.keys(record).length) {
912
+ diagnostics.issues.push({
913
+ path: settingPath,
914
+ kind: "invalid",
915
+ detail: "Model selector records contain invalid values.",
916
+ });
917
+ diagnostics.valid = false;
918
+ }
919
+ }
920
+ if (futureSchemaVersion) {
921
+ diagnostics.issues.push({
922
+ path: "configSchemaVersion",
923
+ kind: "pending-migration",
924
+ detail: `Configuration requires schema version ${configSchemaVersion}.`,
925
+ });
926
+ }
927
+ if (sourcePresent && hasLegacyCustomImageProvider(rawConfig)) {
928
+ diagnostics.issues.push({
929
+ path: "providers.image",
930
+ kind: "invalid",
931
+ detail: LEGACY_CUSTOM_IMAGE_PROVIDER_DIAGNOSTIC,
932
+ });
933
+ diagnostics.valid = false;
934
+ }
935
+ try {
936
+ parseNotificationSettingsSnapshot(rawConfig);
937
+ } catch (error) {
938
+ if (!(error instanceof Error) || error.message !== "gjc_notify_daemon_invalid_configuration") throw error;
939
+ diagnostics.issues.push({
940
+ path: "notifications",
941
+ kind: "invalid",
942
+ detail: "Global notification settings are invalid.",
943
+ });
944
+ diagnostics.valid = false;
945
+ }
946
+ const currentGlobal = structuredClone(this.#global);
947
+ const currentProject = structuredClone(this.#project);
948
+ const currentOverrides = structuredClone(this.#overrides);
949
+ const currentMerged = structuredClone(this.#merged);
950
+ const globalWithPendingPatches = structuredClone(reconciled.settings) as RawSettings;
951
+ const rawWithPendingPatches = structuredClone(rawConfig);
952
+ for (const patch of [...pendingAtStage, ...this.#pendingPatchesInGenerationOrder()]) {
953
+ applySettingsPatch(globalWithPendingPatches, { ...patch, value: structuredClone(patch.value) });
954
+ applySettingsPatch(rawWithPendingPatches, { ...patch, value: structuredClone(patch.value) });
955
+ }
956
+ const effective = this.#mergeSettingsLayers(globalWithPendingPatches, currentProject, currentOverrides);
957
+ diagnostics.issues = diagnostics.issues.filter(
958
+ issue =>
959
+ !(
960
+ (issue.kind === "invalid" || issue.kind === "unknown") &&
961
+ (issue.path === "task.autorouting" || issue.path.startsWith("task.autorouting."))
962
+ ),
963
+ );
964
+ for (const source of [globalWithPendingPatches, currentProject, currentOverrides]) {
965
+ const fragment = getByPath(source, ["task", "autorouting"]);
966
+ diagnostics.issues.push(
967
+ ...validateAutoroutingLocal(fragment).map(localIssue => ({
968
+ path: localIssue.path ? `task.autorouting.${localIssue.path}` : "task.autorouting",
969
+ kind: "invalid" as const,
970
+ detail: localIssue.detail,
971
+ })),
972
+ );
973
+ }
974
+ const effectiveAutorouting = validateAutoroutingEffective(getByPath(effective, ["task", "autorouting"]));
975
+ if (!effectiveAutorouting.active && effectiveAutorouting.issue) {
976
+ diagnostics.issues.push({
977
+ path: "task.autorouting",
978
+ kind: "invalid",
979
+ detail: effectiveAutorouting.issue.detail,
980
+ });
981
+ }
982
+ diagnostics.valid = !diagnostics.issues.some(issue => issue.kind === "invalid");
983
+ if (!diagnostics.valid) {
984
+ throw new SettingsGlobalConfigReloadError(
985
+ "Cannot stage global config reload because settings validation failed.",
986
+ configPath,
987
+ structuredClone(diagnostics),
988
+ );
989
+ }
990
+ const state: SettingsGlobalConfigCandidateState = {
991
+ owner: this,
992
+ configPath,
993
+ globalWithPendingPatches,
994
+ effective,
995
+ diagnostics: structuredClone(diagnostics),
996
+ rawConfig: rawWithPendingPatches,
997
+ currentGlobal,
998
+ currentProject,
999
+ currentOverrides,
1000
+ currentMerged,
1001
+ cwd: this.#cwd,
1002
+ sourcePresent,
1003
+ futureSchemaVersion,
1004
+ published: false,
1005
+ };
1006
+ const candidate: SettingsGlobalConfigCandidate = {
1007
+ configPath,
1008
+ diagnostics: structuredClone(diagnostics),
1009
+ get<P extends SettingPath>(settingPath: P): SettingValue<P> {
1010
+ const value = getByPath(state.effective, settingPath.split("."));
1011
+ if (value === undefined) return structuredClone(getDefault(settingPath));
1012
+ const pathScopedValue = resolvePathScopedStringArray(settingPath, value, state.cwd);
1013
+ return structuredClone((pathScopedValue ?? value) as SettingValue<P>);
1014
+ },
1015
+ getGlobal<P extends SettingPath>(settingPath: P): SettingValue<P> | undefined {
1016
+ const value = getByPath(state.globalWithPendingPatches, settingPath.split("."));
1017
+ return value === undefined ? undefined : structuredClone(value as SettingValue<P>);
1018
+ },
1019
+ };
1020
+ settingsGlobalConfigCandidateStates.set(candidate, state);
1021
+ return Object.freeze(candidate);
1022
+ });
1023
+ }
1024
+
1025
+ /**
1026
+ * Publish a staged global config and its dependent synchronous model commit as
1027
+ * one in-memory transaction. A thrown commit restores settings without hooks.
1028
+ */
1029
+ publishGlobalConfigReload(
1030
+ candidate: SettingsGlobalConfigCandidate,
1031
+ commitSync: () => void = () => {},
1032
+ ): SettingsConfigReloadPublication {
1033
+ const state = settingsGlobalConfigCandidateStates.get(candidate);
1034
+ if (!state || state.owner !== this) {
1035
+ throw new SettingsGlobalConfigReloadError(
1036
+ "The global config candidate does not belong to this Settings instance.",
1037
+ this.#configPath,
1038
+ );
1039
+ }
1040
+ if (state.published) {
1041
+ throw new SettingsGlobalConfigReloadError(
1042
+ "The global config candidate has already been published.",
1043
+ state.configPath,
1044
+ );
1045
+ }
1046
+ if (
1047
+ this.#configPath !== state.configPath ||
1048
+ !util.isDeepStrictEqual(this.#global, state.currentGlobal) ||
1049
+ !util.isDeepStrictEqual(this.#project, state.currentProject) ||
1050
+ !util.isDeepStrictEqual(this.#overrides, state.currentOverrides) ||
1051
+ !util.isDeepStrictEqual(this.#merged, state.currentMerged)
1052
+ ) {
1053
+ throw new SettingsGlobalConfigReloadError(
1054
+ "Settings changed while the global config candidate was being prepared; stage it again.",
1055
+ state.configPath,
1056
+ structuredClone(state.diagnostics),
1057
+ );
1058
+ }
1059
+
1060
+ const previousValues = new Map<SettingPath, unknown>();
1061
+ for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
1062
+ previousValues.set(settingPath, structuredClone(this.get(settingPath)));
1063
+ }
1064
+ const previous = {
1065
+ global: this.#global,
1066
+ overrides: structuredClone(this.#overrides),
1067
+ rawNotificationConfig: this.#rawNotificationConfig,
1068
+ durableRawNotificationConfig: this.#durableRawNotificationConfig,
1069
+ durableNotificationFingerprint: this.#durableNotificationFingerprint,
1070
+ merged: this.#merged,
1071
+ schemaReport: this.#schemaReport,
1072
+ autoroutingEffective: this.#autoroutingEffective,
1073
+ autoroutingLocalIssues: this.#autoroutingLocalIssues,
1074
+ futureSchemaVersion: this.#futureSchemaVersion,
1075
+ hasAcceptedGlobalConfigFile: this.#hasAcceptedGlobalConfigFile,
1076
+ hasMalformedConfigRoot: this.#hasMalformedConfigRoot,
1077
+ hasRecoveredConfigSyntax: this.#hasRecoveredConfigSyntax,
1078
+ hasInvalidNotificationGlobal: this.#hasInvalidNotificationGlobal,
1079
+ notificationValidationGeneration: this.#notificationValidationGeneration,
1080
+ };
1081
+ let publicationState: "pending" | "finalized" | "rolled-back" = "pending";
1082
+ const rollback = () => {
1083
+ if (publicationState === "rolled-back") return;
1084
+ if (publicationState === "finalized") throw new Error("Cannot roll back finalized configuration.");
1085
+ this.#global = previous.global;
1086
+ this.#overrides = previous.overrides;
1087
+ this.#rawNotificationConfig = previous.rawNotificationConfig;
1088
+ this.#durableRawNotificationConfig = previous.durableRawNotificationConfig;
1089
+ this.#durableNotificationFingerprint = previous.durableNotificationFingerprint;
1090
+ this.#merged = previous.merged;
1091
+ this.#schemaReport = previous.schemaReport;
1092
+ this.#autoroutingEffective = previous.autoroutingEffective;
1093
+ this.#autoroutingLocalIssues = previous.autoroutingLocalIssues;
1094
+ this.#futureSchemaVersion = previous.futureSchemaVersion;
1095
+ this.#hasAcceptedGlobalConfigFile = previous.hasAcceptedGlobalConfigFile;
1096
+ this.#hasMalformedConfigRoot = previous.hasMalformedConfigRoot;
1097
+ this.#hasRecoveredConfigSyntax = previous.hasRecoveredConfigSyntax;
1098
+ this.#hasInvalidNotificationGlobal = previous.hasInvalidNotificationGlobal;
1099
+ this.#notificationValidationGeneration = previous.notificationValidationGeneration;
1100
+ publicationState = "rolled-back";
1101
+ };
1102
+ this.#global = structuredClone(state.globalWithPendingPatches);
1103
+ const previousNotificationFingerprint = this.#durableNotificationFingerprint;
1104
+ this.#captureRawNotificationConfig(state.rawConfig);
1105
+ for (const patch of this.#pendingPatchesInGenerationOrder()) {
1106
+ this.#applyNotificationMutationToRaw(patch.path, patch.value);
1107
+ }
1108
+ if (previousNotificationFingerprint !== this.#durableNotificationFingerprint) {
1109
+ this.#notificationValidationGeneration++;
1110
+ }
1111
+ this.#schemaReport = structuredClone(state.diagnostics);
1112
+ this.#futureSchemaVersion = state.futureSchemaVersion;
1113
+ this.#hasMalformedConfigRoot = false;
1114
+ this.#hasRecoveredConfigSyntax = false;
1115
+ this.#hasInvalidNotificationGlobal = false;
1116
+ this.#rebuildMerged();
1117
+ this.#recomputeNotificationValidationFromRaw();
1118
+
1119
+ try {
1120
+ const result: unknown = commitSync();
1121
+ if (result && typeof result === "object" && "then" in result) {
1122
+ throw new TypeError("Global config reload commit callback must be synchronous.");
1123
+ }
1124
+ } catch (error) {
1125
+ rollback();
1126
+ throw error;
1127
+ }
1128
+
1129
+ this.#hasAcceptedGlobalConfigFile ||= state.sourcePresent;
1130
+ state.published = true;
1131
+ return {
1132
+ rollback,
1133
+ finalize: () => {
1134
+ if (publicationState !== "pending") return;
1135
+ publicationState = "finalized";
1136
+ for (const settingPath of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
1137
+ const previousValue = previousValues.get(settingPath);
1138
+ const nextValue = this.get(settingPath);
1139
+ if (util.isDeepStrictEqual(previousValue, nextValue)) continue;
1140
+ const hook = SETTING_HOOKS[settingPath];
1141
+ if (hook) {
1142
+ try {
1143
+ hook(nextValue, previousValue);
1144
+ } catch {
1145
+ logger.warn("Configuration reload setting hook failed", { settingPath });
1146
+ }
1147
+ }
1148
+ for (const listener of this.#changeListeners) {
1149
+ try {
1150
+ listener(settingPath);
1151
+ } catch {
1152
+ logger.warn("Configuration reload listener failed", { settingPath });
1153
+ }
1154
+ }
1155
+ }
1156
+ },
1157
+ };
1158
+ }
1159
+
748
1160
  /** Whether durable settings mutations are permitted for the loaded configuration. */
749
1161
  canWriteDurableConfig(): boolean {
750
1162
  return !this.#persist || !this.#hasRecoveredConfigSyntax;
@@ -1156,6 +1568,7 @@ export class Settings implements NotificationSettingsReader {
1156
1568
  cloned.#autoroutingLocalIssues = structuredClone(this.#autoroutingLocalIssues);
1157
1569
  cloned.#schemaMigrationPending = this.#schemaMigrationPending;
1158
1570
  cloned.#futureSchemaVersion = this.#futureSchemaVersion;
1571
+ cloned.#hasAcceptedGlobalConfigFile = this.#hasAcceptedGlobalConfigFile;
1159
1572
  cloned.#hasMalformedConfigRoot = this.#hasMalformedConfigRoot;
1160
1573
  cloned.#hasRecoveredConfigSyntax = this.#hasRecoveredConfigSyntax;
1161
1574
  cloned.#hasInvalidNotificationGlobal = this.#hasInvalidNotificationGlobal;
@@ -1206,10 +1619,12 @@ export class Settings implements NotificationSettingsReader {
1206
1619
  }
1207
1620
 
1208
1621
  /**
1209
- * Get shell configuration based on settings.
1622
+ * Shell configuration for bash and the Python executor.
1623
+ * `shellPath` comes from the user/global config only, so a project
1624
+ * `.gjc/settings.json` cannot select the binary those executors run.
1210
1625
  */
1211
1626
  getShellConfig() {
1212
- const shell = this.get("shellPath");
1627
+ const shell = this.getGlobal("shellPath");
1213
1628
  return resolveShellConfig(shell);
1214
1629
  }
1215
1630
 
@@ -1516,7 +1931,10 @@ export class Settings implements NotificationSettingsReader {
1516
1931
  throw error;
1517
1932
  }
1518
1933
  this.#resetYamlLoadState();
1519
- if (content.trim() === "") return {};
1934
+ if (content.trim() === "") {
1935
+ if (filePath === this.#configPath) this.#hasAcceptedGlobalConfigFile = true;
1936
+ return {};
1937
+ }
1520
1938
  let parsed: unknown;
1521
1939
  try {
1522
1940
  parsed = YAML.parse(content);
@@ -1536,7 +1954,10 @@ export class Settings implements NotificationSettingsReader {
1536
1954
  this.#captureRawNotificationConfig(undefined);
1537
1955
  return {};
1538
1956
  }
1539
- if (parsed === undefined) return {};
1957
+ if (parsed === undefined) {
1958
+ if (filePath === this.#configPath) this.#hasAcceptedGlobalConfigFile = true;
1959
+ return {};
1960
+ }
1540
1961
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1541
1962
  this.#hasMalformedConfigRoot = true;
1542
1963
  this.#schemaReport = {
@@ -1604,6 +2025,14 @@ export class Settings implements NotificationSettingsReader {
1604
2025
  });
1605
2026
  }
1606
2027
  this.#schemaReport = reconciled.report;
2028
+ if (
2029
+ filePath === this.#configPath &&
2030
+ reconciled.report.valid &&
2031
+ !this.#hasInvalidNotificationGlobal &&
2032
+ !hasLegacyCustomImageProvider(parsedRaw)
2033
+ ) {
2034
+ this.#hasAcceptedGlobalConfigFile = true;
2035
+ }
1607
2036
  return reconciled.settings;
1608
2037
  }
1609
2038
 
@@ -1625,13 +2054,16 @@ export class Settings implements NotificationSettingsReader {
1625
2054
  const data = item.path.endsWith(`${path.sep}settings.json`)
1626
2055
  ? await this.#stripRetiredWorkflowKeys(item.path, structuredClone(item.data as RawSettings))
1627
2056
  : (item.data as RawSettings);
1628
- const { settings, rejectedNotifications, rejectedCredentialPins } =
2057
+ const { settings, rejectedNotifications, rejectedCredentialSelection } =
1629
2058
  this.#stripProjectNotificationSettings(data);
1630
2059
  if (rejectedNotifications) {
1631
2060
  logger.warn("Settings: ignoring project notification settings", { path: item.path });
1632
2061
  }
1633
- if (rejectedCredentialPins) {
1634
- logger.warn("Settings: ignoring project auth.credentialPins; pins are global-only", { path: item.path });
2062
+ if (rejectedCredentialSelection) {
2063
+ logger.warn(
2064
+ "Settings: ignoring project auth credential pins/preferences; credential selection is global-only",
2065
+ { path: item.path },
2066
+ );
1635
2067
  }
1636
2068
  merged = this.#deepMerge(merged, settings);
1637
2069
  }
@@ -6022,16 +6454,20 @@ export class Settings implements NotificationSettingsReader {
6022
6454
  }
6023
6455
 
6024
6456
  #rebuildMerged(): void {
6025
- const project = structuredClone(this.#project);
6026
- const overrides = structuredClone(this.#overrides);
6457
+ this.#merged = this.#mergeSettingsLayers(this.#global, this.#project, this.#overrides);
6458
+ this.#recomputeAutoroutingDiagnostic();
6459
+ }
6460
+
6461
+ #mergeSettingsLayers(global: RawSettings, projectLayer: RawSettings, overrideLayer: RawSettings): RawSettings {
6462
+ const project = structuredClone(projectLayer);
6463
+ const overrides = structuredClone(overrideLayer);
6027
6464
  for (const settingPath of GLOBAL_ONLY_SETTINGS) {
6028
6465
  const segments = settingPath.split(".");
6029
6466
  deleteByPath(project, segments);
6030
6467
  deleteByPath(overrides, segments);
6031
6468
  }
6032
- this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), project);
6033
- this.#merged = this.#deepMerge(this.#merged, overrides);
6034
- this.#recomputeAutoroutingDiagnostic();
6469
+ const merged = this.#deepMerge(this.#deepMerge({}, global), project);
6470
+ return this.#deepMerge(merged, overrides);
6035
6471
  }
6036
6472
 
6037
6473
  #fireAllHooks(): void {
@@ -6047,27 +6483,25 @@ export class Settings implements NotificationSettingsReader {
6047
6483
  #stripProjectNotificationSettings(settings: RawSettings): {
6048
6484
  settings: RawSettings;
6049
6485
  rejectedNotifications: boolean;
6050
- rejectedCredentialPins: boolean;
6486
+ rejectedCredentialSelection: boolean;
6051
6487
  } {
6052
6488
  let rejectedNotifications = false;
6053
- let rejectedCredentialPins = false;
6489
+ let rejectedCredentialSelection = false;
6054
6490
  const sanitized: RawSettings = {};
6055
6491
  for (const [key, value] of Object.entries(settings)) {
6056
6492
  if (key === "auth" && value && typeof value === "object" && !Array.isArray(value)) {
6057
6493
  const authSettings = { ...(value as Record<string, unknown>) };
6058
- if (Object.hasOwn(authSettings, "credentialPins")) {
6059
- delete authSettings.credentialPins;
6060
- rejectedCredentialPins = true;
6061
- }
6062
- if (Object.hasOwn(authSettings, "credentialPinStoreIdentity")) {
6063
- delete authSettings.credentialPinStoreIdentity;
6064
- rejectedCredentialPins = true;
6494
+ for (const key of GLOBAL_ONLY_AUTH_SETTING_KEYS) {
6495
+ if (Object.hasOwn(authSettings, key)) {
6496
+ delete authSettings[key];
6497
+ rejectedCredentialSelection = true;
6498
+ }
6065
6499
  }
6066
6500
  if (Object.keys(authSettings).length > 0) sanitized[key] = authSettings;
6067
6501
  continue;
6068
6502
  }
6069
- if (key === "auth.credentialPins" || key === "auth.credentialPinStoreIdentity") {
6070
- rejectedCredentialPins = true;
6503
+ if (key.startsWith("auth.") && GLOBAL_ONLY_AUTH_SETTING_KEYS.has(key.slice("auth.".length))) {
6504
+ rejectedCredentialSelection = true;
6071
6505
  continue;
6072
6506
  }
6073
6507
  if (key === "notifications" && value && typeof value === "object" && !Array.isArray(value)) {
@@ -6088,7 +6522,7 @@ export class Settings implements NotificationSettingsReader {
6088
6522
  }
6089
6523
  sanitized[key] = value;
6090
6524
  }
6091
- return { settings: sanitized, rejectedNotifications, rejectedCredentialPins };
6525
+ return { settings: sanitized, rejectedNotifications, rejectedCredentialSelection };
6092
6526
  }
6093
6527
 
6094
6528
  #deepMerge(base: RawSettings, overrides: RawSettings): RawSettings {
@@ -3,6 +3,7 @@ import * as nodeFs from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
+ import { canonicalEnvKey, projectEnvSnapshot } from "@gajae-code/utils/env-file";
6
7
  import { coordinatorMcpStateRoot, gjcRoot } from "../gjc-runtime/session-layout";
7
8
  import {
8
9
  DEFAULT_SESSION_IDLE_TTL_MS,
@@ -121,6 +122,30 @@ function parseRootList(value: string | undefined): string[] {
121
122
  .filter(Boolean);
122
123
  }
123
124
 
125
+ const MANAGED_WORKTREE_DIR_ENV = "GJC_WORKTREE_DIR";
126
+ /** Captured at load so a later chdir or dotenv delete cannot hide the declaration. */
127
+ const startupManagedWorktreeSnapshot = projectEnvSnapshot();
128
+
129
+ /**
130
+ * Operator value only. A project dotenv declaration, including a dynamic one,
131
+ * must not widen managed worktree roots. An operator value the project does
132
+ * not declare is kept.
133
+ */
134
+ function trustedManagedWorktreeDir(env: NodeJS.ProcessEnv): string | undefined {
135
+ const raw = env[MANAGED_WORKTREE_DIR_ENV];
136
+ if (!raw) return undefined;
137
+ const key = canonicalEnvKey(MANAGED_WORKTREE_DIR_ENV);
138
+ const declared = startupManagedWorktreeSnapshot.values[key];
139
+ const trimmed = raw.trim();
140
+ if (
141
+ declared !== undefined &&
142
+ (startupManagedWorktreeSnapshot.dynamic.has(key) || declared === raw || declared === trimmed)
143
+ ) {
144
+ return undefined;
145
+ }
146
+ return trimmed.length > 0 ? trimmed : undefined;
147
+ }
148
+
124
149
  function resolveManagedWorktreeRoot(root: string, configured: string | undefined): string {
125
150
  // Function replacers so a `$` in the home or repository path is kept verbatim.
126
151
  const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, () => os.homedir());
@@ -168,7 +193,7 @@ export function buildCoordinatorMcpConfig(env: NodeJS.ProcessEnv = process.env):
168
193
  return {
169
194
  allowedRoots: parseRootList(env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS).map(root => path.resolve(root)),
170
195
  managedWorktreeRoots: parseRootList(env.GJC_COORDINATOR_MCP_WORKDIR_ROOTS).map(root =>
171
- resolveManagedWorktreeRoot(path.resolve(root), env.GJC_WORKTREE_DIR),
196
+ resolveManagedWorktreeRoot(path.resolve(root), trustedManagedWorktreeDir(env)),
172
197
  ),
173
198
  mutationClasses: parseMutationClasses(
174
199
  env.GJC_COORDINATOR_MCP_MUTATIONS ?? env.GJC_COORDINATOR_MCP_ENABLE_MUTATION_CLASSES,
@@ -49,6 +49,10 @@ const RESIDUAL_PATTERNS: readonly RegExp[] = [
49
49
  /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./,
50
50
  /\bxox[baprs]-[A-Za-z0-9-]{8,}/,
51
51
  /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
52
+ // Same shapes `redactCrashSecrets` rewrites. A survivor means this scanner
53
+ // is not certain, so the submission is refused rather than sent raw.
54
+ /(?<![A-Za-z0-9/+])(?=[A-Za-z0-9/+]{0,39}[/+])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+])/,
55
+ /(?<![A-Za-z0-9_%])session(?:id|[_-]id)%3[Dd][A-Za-z0-9._~%-]{8,}/i,
52
56
  // Opaque URIs the URL rule cannot parse into an origin, and inline payloads.
53
57
  /\bdata:[^\s]{0,64};base64,/i,
54
58
  /:\/\//,
@@ -538,7 +538,9 @@ export async function buildImportPreview(options: BuildImportPreviewOptions): Pr
538
538
  read.kind === "value"
539
539
  ? options.product === "claude-code"
540
540
  ? normalizeClaudeMcpJson(read.content, sourcePath, options.sourceScope)
541
- : normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope)
541
+ : normalizeCodexMcpToml(read.content, sourcePath, options.sourceScope, {
542
+ omitEnvReferences: options.destinationScope === "project",
543
+ })
542
544
  : { items: [], warnings: [] as string[] };
543
545
  for (const warning of normalized.warnings) warnings.push(`${sourceLabel} MCPs: ${warning}`);
544
546
  const unsupportedNestedAuth =