@gajae-code/coding-agent 0.18.12 → 0.19.0

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 (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -4,7 +4,12 @@
4
4
  * Manages rules that get injected mid-stream when their condition pattern matches
5
5
  * the agent's output. When a match occurs, the stream is aborted, the rule is
6
6
  * injected as a system reminder, and the request is retried.
7
+ *
8
+ * Rules may also carry `astCondition` ast-grep patterns, which are evaluated
9
+ * against the source a write/edit tool call introduces, before it executes.
7
10
  */
11
+ import * as path from "node:path";
12
+ import { AstMatchStrictness, astMatch } from "@gajae-code/natives";
8
13
  import { logger } from "@gajae-code/utils";
9
14
  import type { Rule } from "../capability/rule";
10
15
  import type { TtsrSettings } from "../config/settings";
@@ -22,6 +27,14 @@ export interface TtsrMatchContext {
22
27
  streamKey?: string;
23
28
  }
24
29
 
30
+ /** An astCondition pattern parse issue observed while checking a target language. */
31
+ export interface TtsrAstConditionDiagnostic {
32
+ ruleName: string;
33
+ lang: string;
34
+ patterns: string[];
35
+ parseErrors: string[];
36
+ }
37
+
25
38
  interface ToolScope {
26
39
  toolName?: string;
27
40
  pathGlob?: Bun.Glob;
@@ -39,6 +52,7 @@ interface TtsrEntry {
39
52
  rule: Rule;
40
53
  conditions: RegExp[];
41
54
  conditionWindows: Array<number | undefined>;
55
+ astConditions?: string[];
42
56
 
43
57
  scope: TtsrScope;
44
58
  globalPathGlobs?: Bun.Glob[];
@@ -69,6 +83,7 @@ const DEFAULT_SETTINGS: Required<TtsrSettings> = {
69
83
 
70
84
  const SAFE_REGEX_WINDOW_CAP = 4096;
71
85
  const SAFE_REGEX_WINDOW_MULTIPLIER = 4;
86
+ const SOURCE_SYNTAX_PARSE_ERROR = "parse error (syntax tree contains error nodes)";
72
87
 
73
88
  const DEFAULT_SCOPE: TtsrScope = {
74
89
  allowText: true,
@@ -81,7 +96,9 @@ export class TtsrManager {
81
96
  readonly #settings: Required<TtsrSettings>;
82
97
  readonly #rules = new Map<string, TtsrEntry>();
83
98
  readonly #injectionRecords = new Map<string, InjectionRecord>();
99
+ readonly #astConditionDiagnostics = new Map<string, TtsrAstConditionDiagnostic>();
84
100
  readonly #buffers = new Map<string, string>();
101
+ readonly #lastAstSnapshots = new Map<string, string>();
85
102
  #messageCount = 0;
86
103
 
87
104
  constructor(settings?: Partial<TtsrSettings>) {
@@ -339,7 +356,10 @@ export class TtsrManager {
339
356
  }
340
357
 
341
358
  const conditions = this.#compileConditions(rule);
342
- if (conditions.length === 0) {
359
+ const astConditions = rule.astCondition ?? [];
360
+
361
+ // Rule must have either regex or AST conditions
362
+ if (conditions.length === 0 && astConditions.length === 0) {
343
363
  return false;
344
364
  }
345
365
 
@@ -357,6 +377,7 @@ export class TtsrManager {
357
377
  rule,
358
378
  conditions,
359
379
  conditionWindows,
380
+ astConditions: astConditions.length > 0 ? astConditions : undefined,
360
381
  scope,
361
382
  globalPathGlobs,
362
383
  });
@@ -364,6 +385,7 @@ export class TtsrManager {
364
385
  logger.debug("TTSR rule registered", {
365
386
  ruleName: rule.name,
366
387
  conditions: rule.condition,
388
+ astConditions: rule.astCondition,
367
389
  scope: rule.scope,
368
390
  globs: rule.globs,
369
391
  });
@@ -446,6 +468,25 @@ export class TtsrManager {
446
468
  return Array.from(this.#injectionRecords.keys());
447
469
  }
448
470
 
471
+ /**
472
+ * Undo a provisional claim whose reminder was never delivered: restore the
473
+ * record the rule had before the claim, or forget it when it had none, so the
474
+ * rule is eligible again exactly as before.
475
+ */
476
+ releaseInjected(ruleName: string, previous: TtsrInjectionRecord | undefined): void {
477
+ const name = ruleName.trim();
478
+ if (name.length === 0) return;
479
+ if (!previous) {
480
+ this.#injectionRecords.delete(name);
481
+ return;
482
+ }
483
+ this.#injectionRecords.set(name, {
484
+ lastInjectedAt: previous.lastInjectedAt,
485
+ repeatMode: previous.repeatMode,
486
+ repeatGap: previous.repeatGap,
487
+ });
488
+ }
489
+
449
490
  /** Get full injected rule records (for persistence). */
450
491
  getInjectedRecords(): TtsrInjectionRecord[] {
451
492
  return Array.from(this.#injectionRecords, ([name, record]) => ({ name, ...record }));
@@ -477,6 +518,7 @@ export class TtsrManager {
477
518
  /** Reset stream buffers (called on new turn). */
478
519
  resetBuffer(): void {
479
520
  this.#buffers.clear();
521
+ this.#lastAstSnapshots.clear();
480
522
  }
481
523
 
482
524
  /** Check if any TTSR rules are registered. */
@@ -509,4 +551,137 @@ export class TtsrManager {
509
551
  getSettings(): Required<TtsrSettings> {
510
552
  return this.#settings;
511
553
  }
554
+
555
+ /** Check if any registered rule carries an `astCondition`. */
556
+ hasAstRules(): boolean {
557
+ if (this.#isDisabled()) {
558
+ return false;
559
+ }
560
+ for (const entry of this.#rules.values()) {
561
+ if (entry.astConditions) {
562
+ return true;
563
+ }
564
+ }
565
+ return false;
566
+ }
567
+
568
+ /** Get observed astCondition pattern parse issues for rule diagnostics. */
569
+ getAstConditionDiagnostics(): TtsrAstConditionDiagnostic[] {
570
+ return Array.from(this.#astConditionDiagnostics.values(), diagnostic => ({
571
+ ...diagnostic,
572
+ patterns: [...diagnostic.patterns],
573
+ parseErrors: [...diagnostic.parseErrors],
574
+ }));
575
+ }
576
+
577
+ /** Derive an ast-grep language alias from the candidate file extensions. */
578
+ #deriveLang(filePaths: string[] | undefined): string | undefined {
579
+ for (const filePath of filePaths ?? []) {
580
+ const ext = path.extname(this.#normalizePath(filePath));
581
+ if (ext.length > 1) return ext.slice(1).toLowerCase();
582
+ }
583
+ return undefined;
584
+ }
585
+
586
+ /** Match the source introduced by finalized tool arguments, before execution. */
587
+ async checkAstSnapshot(snapshot: string, context: TtsrMatchContext, signal?: AbortSignal): Promise<Rule[]> {
588
+ if (this.#isDisabled() || context.source !== "tool" || signal?.aborted) {
589
+ return [];
590
+ }
591
+ const lang = this.#deriveLang(context.filePaths);
592
+ if (!lang) return [];
593
+
594
+ const candidates: TtsrEntry[] = [];
595
+ for (const [name, entry] of this.#rules) {
596
+ if (!entry.astConditions?.length) continue;
597
+ if (
598
+ !this.#canTrigger(name) ||
599
+ !this.#matchesScope(entry, context) ||
600
+ !this.#matchesGlobalPaths(entry, context)
601
+ ) {
602
+ continue;
603
+ }
604
+ candidates.push(entry);
605
+ }
606
+ if (candidates.length === 0) return [];
607
+
608
+ const bufferKey = this.#bufferKey(context);
609
+ if (this.#lastAstSnapshots.get(bufferKey) === snapshot) return [];
610
+ this.#lastAstSnapshots.set(bufferKey, snapshot);
611
+
612
+ const matches: Rule[] = [];
613
+ for (const entry of candidates) {
614
+ if (!entry.astConditions) continue;
615
+ const matched = await this.#astConditionsMatch(entry.rule.name, entry.astConditions, snapshot, lang, signal);
616
+ if (signal?.aborted) return [];
617
+ if (matched) {
618
+ matches.push(entry.rule);
619
+ logger.debug("TTSR ast condition matched", {
620
+ ruleName: entry.rule.name,
621
+ astConditions: entry.rule.astCondition,
622
+ toolName: context.toolName,
623
+ filePaths: context.filePaths,
624
+ });
625
+ }
626
+ }
627
+ return matches;
628
+ }
629
+
630
+ #recordAstConditionDiagnostics(
631
+ ruleName: string,
632
+ patterns: string[],
633
+ lang: string,
634
+ parseErrors: string[] | undefined,
635
+ ): void {
636
+ const patternParseErrors = parseErrors?.filter(parseError => parseError !== SOURCE_SYNTAX_PARSE_ERROR) ?? [];
637
+ if (patternParseErrors.length === 0) return;
638
+
639
+ const key = JSON.stringify([ruleName, lang]);
640
+ if (this.#astConditionDiagnostics.has(key)) return;
641
+
642
+ const diagnostic: TtsrAstConditionDiagnostic = {
643
+ ruleName,
644
+ lang,
645
+ patterns: [...patterns],
646
+ parseErrors: patternParseErrors,
647
+ };
648
+ this.#astConditionDiagnostics.set(key, diagnostic);
649
+ logger.warn("TTSR astCondition pattern is invalid for target language", {
650
+ ruleName,
651
+ lang,
652
+ patterns: diagnostic.patterns,
653
+ parseErrors: diagnostic.parseErrors,
654
+ });
655
+ }
656
+
657
+ async #astConditionsMatch(
658
+ ruleName: string,
659
+ patterns: string[],
660
+ source: string,
661
+ lang: string,
662
+ signal?: AbortSignal,
663
+ ): Promise<boolean> {
664
+ try {
665
+ const result = await astMatch({
666
+ patterns,
667
+ source,
668
+ lang,
669
+ strictness: AstMatchStrictness.Smart,
670
+ limit: 1,
671
+ signal,
672
+ });
673
+ if (!signal?.aborted) {
674
+ this.#recordAstConditionDiagnostics(ruleName, patterns, lang, result.parseErrors);
675
+ }
676
+ return result.totalMatches > 0;
677
+ } catch (error) {
678
+ logger.warn("TTSR ast match failed, treating as no match", {
679
+ ruleName,
680
+ patterns,
681
+ lang,
682
+ error: error instanceof Error ? error.message : String(error),
683
+ });
684
+ return false;
685
+ }
686
+ }
512
687
  }
@@ -15,6 +15,7 @@ import { loadCapability } from "../../discovery";
15
15
  import { getExtensionNameFromPath } from "../../discovery/helpers";
16
16
  import type { ExecOptions } from "../../exec/exec";
17
17
  import { execCommand } from "../../exec/exec";
18
+ import type { QueuedRunStartFailure } from "../../session/agent-session";
18
19
  import type { CustomMessage } from "../../session/messages";
19
20
  import { EventBus } from "../../utils/event-bus";
20
21
  import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
@@ -394,6 +395,7 @@ class ConcreteExtensionAPI implements ExtensionAPI {
394
395
  onPreflightAccepted?: () => void;
395
396
  onPreflightAcceptCommit?: () => void | Promise<void>;
396
397
  onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
398
+ onQueuedRunStartFailed?: (failure: QueuedRunStartFailure) => void;
397
399
  onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
398
400
  preflightSignal?: AbortSignal;
399
401
  },
@@ -1,9 +1,14 @@
1
+ import { ptree } from "@gajae-code/utils";
2
+ import { $credentialEnv, startupProjectEnvSnapshot } from "@gajae-code/utils/env";
3
+ import { canonicalEnvKey } from "@gajae-code/utils/env-file";
4
+ import type { ExecResult } from "../../exec/exec";
1
5
  import type { MCPServerConnection, MCPToolCallResult } from "../../runtime-mcp";
2
6
  import { callTool, connectToServer, disconnectServer } from "../../runtime-mcp";
3
7
  import { createExactPrefixCommandBridge } from "./prefix-command-bridge";
4
8
  import type { ExtensionContext, InputEvent, InputEventResult } from "./types";
5
9
 
6
10
  const OUROBOROS_CLI_ENV = "OUROBOROS_CLI";
11
+ const DEFAULT_OUROBOROS_COMMAND = "ouroboros";
7
12
  const INTERVIEW_COMMAND = "ooo interview";
8
13
  const INTERVIEW_TOOL = "ouroboros_interview";
9
14
  const INTERVIEW_SESSION_PATTERN = /^interview_[A-Za-z0-9_-]+$/;
@@ -23,8 +28,64 @@ interface OuroborosOooBridgeHandler {
23
28
  reset(): Promise<void>;
24
29
  }
25
30
 
31
+ function projectSuppliedOuroborosCli(): boolean {
32
+ const snapshot = startupProjectEnvSnapshot();
33
+ const key = canonicalEnvKey(OUROBOROS_CLI_ENV);
34
+ return Object.hasOwn(snapshot.values, key) || snapshot.dynamic.has(key);
35
+ }
36
+
37
+ /**
38
+ * Executable for `ooo` dispatch and the interview MCP server.
39
+ *
40
+ * The string is spawned directly, so a repository `.env` must not choose it.
41
+ * `$credentialEnv` keeps a launching-shell or user-owned setting and ignores
42
+ * the project dotenv, including a dynamic declaration. Anything else stays the
43
+ * default command name. A rejected project value is removed from `process.env`
44
+ * before either spawn. Prefix dispatch also passes that cleaned object: Bun
45
+ * keeps the startup environment for a spawn that omits `env`, so deleting the
46
+ * key alone would still hand the project value to the child.
47
+ */
26
48
  function resolveOuroborosCommand(): string {
27
- return process.env[OUROBOROS_CLI_ENV]?.trim() || "ouroboros";
49
+ const trusted = $credentialEnv(OUROBOROS_CLI_ENV)?.trim();
50
+ if (trusted) {
51
+ const live = process.env[OUROBOROS_CLI_ENV];
52
+ if (live !== undefined && live.trim() !== trusted) process.env[OUROBOROS_CLI_ENV] = trusted;
53
+ return trusted;
54
+ }
55
+ if (projectSuppliedOuroborosCli()) delete process.env[OUROBOROS_CLI_ENV];
56
+ return DEFAULT_OUROBOROS_COMMAND;
57
+ }
58
+
59
+ function ouroborosSpawnEnv(): Record<string, string> {
60
+ resolveOuroborosCommand();
61
+ const env: Record<string, string> = {};
62
+ for (const key of Object.keys(process.env)) {
63
+ const value = process.env[key];
64
+ if (typeof value === "string") env[key] = value;
65
+ }
66
+ return env;
67
+ }
68
+
69
+ async function dispatchOuroborosCommand(
70
+ command: string,
71
+ args: string[],
72
+ ctx: ExtensionContext,
73
+ execOptions: { timeout?: number },
74
+ ): Promise<ExecResult> {
75
+ const result = await ptree.exec([command, ...args], {
76
+ cwd: ctx.cwd,
77
+ timeout: execOptions.timeout,
78
+ allowNonZero: true,
79
+ allowAbort: true,
80
+ stderr: "full",
81
+ env: ouroborosSpawnEnv(),
82
+ });
83
+ return {
84
+ stdout: result.stdout,
85
+ stderr: result.stderr,
86
+ code: result.exitCode ?? 0,
87
+ killed: Boolean(result.exitError?.aborted),
88
+ };
28
89
  }
29
90
 
30
91
  function interviewArgument(text: string): string | undefined {
@@ -89,6 +150,7 @@ export function createOuroborosOooBridge(options: OuroborosOooBridgeOptions = {}
89
150
  prefix: "ooo",
90
151
  command: resolveOuroborosCommand(),
91
152
  args: ["dispatch", "--runtime", "gjc"],
153
+ dispatch: dispatchOuroborosCommand,
92
154
  });
93
155
 
94
156
  function assertCurrent(generation: number, signal: AbortSignal | undefined): void {
@@ -50,7 +50,7 @@ import type { CustomEditor } from "../../modes/components/custom-editor";
50
50
  import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/workflow-gate-broker";
51
51
  import type { Theme } from "../../modes/theme/theme";
52
52
  import type { ProjectProgressSnapshot } from "../../progress/progress-contract";
53
- import type { AgentSessionEventListener } from "../../session/agent-session";
53
+ import type { AgentSessionEventListener, QueuedRunStartFailure } from "../../session/agent-session";
54
54
  import type {
55
55
  ClientBridge,
56
56
  ClientBridgePermissionOption,
@@ -1347,6 +1347,8 @@ export interface ExtensionAPI {
1347
1347
  onPreflightAccepted?: () => void;
1348
1348
  onPreflightAcceptCommit?: () => void | Promise<void>;
1349
1349
  onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
1350
+ /** Internal positive refusal before an exact queued invocation can start. */
1351
+ onQueuedRunStartFailed?: (failure: QueuedRunStartFailure) => void;
1350
1352
  onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
1351
1353
  preflightSignal?: AbortSignal;
1352
1354
  /** Internal SDK correlation owner for an exact queued follow-up. */
@@ -1577,6 +1579,8 @@ export type SendUserMessageHandler = (
1577
1579
  * grants root abort ownership.
1578
1580
  */
1579
1581
  onQueuedPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
1582
+ /** Internal positive refusal, not an inference from a dispatch error or missing event. */
1583
+ onQueuedRunStartFailed?: (failure: QueuedRunStartFailure) => void;
1580
1584
  /**
1581
1585
  * Fired synchronously when a plain prompt's dispatch resolves at queue
1582
1586
  * time instead of run end (diverted into an in-flight run's steering).
@@ -346,9 +346,17 @@ export async function discoverAndLoadHooks(configuredPaths: string[], cwd: strin
346
346
  };
347
347
 
348
348
  // 1. Discover hooks via capability API and validate the provider descriptor
349
- // against the canonical model before importing project-controlled code.
349
+ // against the canonical model before importing. A repository
350
+ // `<cwd>/.gjc/hooks` module is not imported. Skip those entries before
351
+ // dedup so a same-named user-agent hook still loads. An explicit path
352
+ // below remains a consent path.
350
353
  const discovered = await loadCapability<Hook>(hookCapability.id, { cwd, providers: ["native"] });
351
- for (const hook of discovered.items) {
354
+ const seenHookKeys = new Set<string>();
355
+ for (const hook of discovered.all) {
356
+ if (hook.level === "project") continue;
357
+ const hookKey = `${hook.type}:${hook.tool}:${hook.name}`;
358
+ if (seenHookKeys.has(hookKey)) continue;
359
+ seenHookKeys.add(hookKey);
352
360
  const convention =
353
361
  hook._source.provider === "native"
354
362
  ? HookSourceConvention.NativeGjc
@@ -15,6 +15,7 @@ import {
15
15
  type NativeDirectoryTreeSnapshot,
16
16
  type NativeExactUnlinkResult,
17
17
  renameDirectoryNoReplacePathAsync,
18
+ renameNoReplacePathAsync,
18
19
  snapshotDirectoryTree,
19
20
  } from "@gajae-code/natives";
20
21
  import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
@@ -175,6 +176,13 @@ async function snapshotIfPresent(targetPath: string): Promise<NativeDirectoryTre
175
176
  return snapshot.ok ? snapshot.snapshot : undefined;
176
177
  }
177
178
 
179
+ /** Windows supports directory no-replace through the handle-based path API, not the POSIX directory fallback. */
180
+ function renameDirectoryNoReplace(sourcePath: string, destinationPath: string) {
181
+ return process.platform === "win32"
182
+ ? renameNoReplacePathAsync(sourcePath, destinationPath)
183
+ : renameDirectoryNoReplacePathAsync(sourcePath, destinationPath);
184
+ }
185
+
178
186
  /**
179
187
  * Full identity comparison between two directory tree snapshots. A same-
180
188
  * filesystem `renameat`-class namespace operation changes neither the root's
@@ -850,9 +858,10 @@ export class MarketplaceManager {
850
858
  });
851
859
  if (finalStat?.isSymbolicLink()) throw new MarketplaceRestoreError("final_path_is_symlink");
852
860
  const hadFinal = finalStat !== undefined;
853
- // Single-component name; never renamed elsewhere on collision since the
854
- // native no-replace primitive is what actually refuses an occupied slot.
855
- const backup = `${finalPath}.restore-backup-${process.pid}-${randomBytes(6).toString("hex")}`;
861
+ // Keep the private backup component bounded independently of the canonical
862
+ // cache basename, which can itself be near NAME_MAX through a long version.
863
+ const backupName = `.gjc-restore-backup-${createHash("sha256").update(finalPath).digest("hex")}-${randomBytes(12).toString("hex")}`;
864
+ const backup = path.join(pluginsCacheDir, backupName);
856
865
 
857
866
  const parentRecheck = await fs.lstat(pluginsCacheDir, { bigint: true }).catch(() => undefined);
858
867
  if (!parentRecheck || parentRecheck.dev !== parentBefore.dev || parentRecheck.ino !== parentBefore.ino)
@@ -878,12 +887,29 @@ export class MarketplaceManager {
878
887
  // merely a decision about whether it is safe to proceed. sideEffectStarted
879
888
  // stays false — effects are physical, not procedural.
880
889
  if (!finalSnapshotBefore) throw new MarketplaceRestoreError("final_path_snapshot_unavailable");
881
- const detached = exactRemoveDirectoryTree(finalPath, finalSnapshotBefore, {
882
- dev: parentBefore.dev,
883
- ino: parentBefore.ino,
884
- });
890
+ const detached = exactRemoveDirectoryTree(
891
+ finalPath,
892
+ finalSnapshotBefore,
893
+ {
894
+ dev: parentBefore.dev,
895
+ ino: parentBefore.ino,
896
+ },
897
+ true,
898
+ );
885
899
  const detachedPath = detachedTree(detached);
886
900
  if (!detachedPath) {
901
+ if (detached.code === "restore_failed" && detached.detachedPath) {
902
+ const retainedSnapshot = await snapshotIfPresent(detached.detachedPath).catch(() => undefined);
903
+ const retainedBackupPath =
904
+ retainedSnapshot && sameDirectoryTreeIdentity(finalSnapshotBefore, retainedSnapshot)
905
+ ? detached.detachedPath
906
+ : undefined;
907
+ throw new MarketplaceRestoreError("backup_detach_unverified", {
908
+ sideEffectStarted: true,
909
+ outcome: "uncertain",
910
+ ...(retainedBackupPath ? { retainedBackupPath } : {}),
911
+ });
912
+ }
887
913
  // identity_mismatch means finalPath already changed before we ever
888
914
  // touched it — exactRemoveDirectoryTree refused the mutation entirely,
889
915
  // so this is a pure precheck refusal: nothing moved, sideEffectStarted
@@ -906,7 +932,7 @@ export class MarketplaceManager {
906
932
  // later detach of the same canonical path inside this same transaction (the
907
933
  // rollback path) collides with our own leftover and is misread as a foreign
908
934
  // claim. The rename below is what vacates it; failure is reported, never assumed.
909
- const backupPublish = await renameDirectoryNoReplacePathAsync(detachedPath, backup);
935
+ const backupPublish = await renameDirectoryNoReplace(detachedPath, backup);
910
936
  if (!backupPublish.ok) {
911
937
  // mutationState "not_committed" proves the rename never moved the
912
938
  // object; it is still at the detached path, so that path is real
@@ -962,7 +988,7 @@ export class MarketplaceManager {
962
988
  // devices on cross_device, the dev/ino-stability assumption behind
963
989
  // `stagedSnapshot` breaks silently and every identity-bound check in this
964
990
  // method becomes unsound.
965
- const publish = await renameDirectoryNoReplacePathAsync(token.stagedArtifactPath, finalPath);
991
+ const publish = await renameDirectoryNoReplace(token.stagedArtifactPath, finalPath);
966
992
  // Test seam only (no-op in production): lets a test deterministically
967
993
  // simulate a concurrent second writer republishing finalPath here,
968
994
  // exercising the foreign-owner branch below without weakening it.
@@ -1233,7 +1259,7 @@ export class MarketplaceManager {
1233
1259
  );
1234
1260
  }
1235
1261
  }
1236
- const restorePublish = await renameDirectoryNoReplacePathAsync(backup, finalPath);
1262
+ const restorePublish = await renameDirectoryNoReplace(backup, finalPath);
1237
1263
  if (!restorePublish.ok) {
1238
1264
  // Restore failed: confirm what actually remains at `backup` right now
1239
1265
  // (the no-replace rename may have left it there proven-untouched, or
@@ -116,6 +116,11 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
116
116
  return cmd;
117
117
  });
118
118
 
119
+ export function getBuiltinSlashCommandsForSession(hasExactMcpControls: boolean): typeof BUILTIN_SLASH_COMMANDS {
120
+ if (hasExactMcpControls) return BUILTIN_SLASH_COMMANDS;
121
+ return BUILTIN_SLASH_COMMANDS.filter(command => command.name !== "mcp");
122
+ }
123
+
119
124
  /**
120
125
  * Represents a custom slash command loaded from a file
121
126
  */
@@ -2299,7 +2299,10 @@ async function removeGcDiskEntry(
2299
2299
  return { removed: false, reason: "entry_changed" };
2300
2300
  }
2301
2301
  const retainedAtOriginal = !removal.detachedPath || path.resolve(removal.detachedPath) === path.resolve(target);
2302
- if ((removal.code === "permission_denied" || removal.code === "io_error") && retainedAtOriginal) {
2302
+ if (
2303
+ (removal.code === "permission_denied" || removal.code === "access_denied" || removal.code === "io_error") &&
2304
+ retainedAtOriginal
2305
+ ) {
2303
2306
  return { removed: false, reason: "withheld_evidence_incomplete: tree_unreadable", withheld: true };
2304
2307
  }
2305
2308
  return {
@@ -71,11 +71,13 @@ import {
71
71
  } from "./tmux-owner-isolation";
72
72
  import { assertGjcTmuxStagedMutationAuthoritySync } from "./tmux-provider-context";
73
73
  import {
74
+ childGjcSessionIdAssignment,
74
75
  findGjcTmuxSessionByName,
75
76
  findGjcTmuxSessionByScope,
76
77
  type GjcTmuxSessionStatus,
77
78
  type ProvenTmuxSessionIdentity,
78
79
  proveGjcTmuxSessionMutationTarget,
80
+ tmuxLaunchIdentityEnv,
79
81
  } from "./tmux-sessions";
80
82
  import {
81
83
  buildWindowsPowerShellInnerCommand,
@@ -433,6 +435,8 @@ function stripRootTmuxFlag(rawArgs: string[]): string[] {
433
435
 
434
436
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
435
437
  const env = context.env ?? process.env;
438
+ const cwd = context.cwd ?? process.cwd();
439
+ const identityEnv = tmuxLaunchIdentityEnv(env, cwd);
436
440
  const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
437
441
  // The psmux UX filter (mouse / set-clipboard / mode-style /
438
442
  // set-window-option) now lives in buildGjcTmuxProfileCommands so every
@@ -448,8 +452,8 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
448
452
  branch: context.branch ?? null,
449
453
  branchSlug,
450
454
  project: context.project ?? null,
451
- sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
452
- sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
455
+ sessionId: context.sessionId ?? identityEnv[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
456
+ sessionStateFile: context.sessionStateFile ?? identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
453
457
  ownerGeneration: context.ownerGeneration ?? null,
454
458
  ownerServerKey: context.ownerServerKey ?? null,
455
459
  version: context.version ?? null,
@@ -463,7 +467,6 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
463
467
  });
464
468
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
465
469
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
466
- const cwd = context.cwd ?? process.cwd();
467
470
  const options: TmuxSpawnOptions = {
468
471
  cwd,
469
472
  env,
@@ -1106,22 +1109,24 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1106
1109
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
1107
1110
 
1108
1111
  const cwd = context.cwd ?? process.cwd();
1112
+ const identityEnv = tmuxLaunchIdentityEnv(env, cwd);
1109
1113
  const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
1110
1114
  const project = context.project ?? cwd;
1111
- const sessionName = buildGjcTmuxSessionName(env, { branch });
1115
+ const sessionName = buildGjcTmuxSessionName(identityEnv, { branch });
1112
1116
  // Pick the most appropriate tmux binary for this platform. On native Windows
1113
1117
  // the resolver walks psmux / pmux / tmux and uses the first one present on
1114
1118
  // PATH, so the default `gjc --tmux` flow lands on a real multiplexer even
1115
1119
  // without an explicit GJC_TMUX_COMMAND override.
1116
1120
  const resolvedBinary = resolveGjcTmuxBinary({ platform, env });
1117
1121
  const tmuxCommand = resolvedBinary.command;
1118
- const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
1122
+ const sessionId = identityEnv[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
1119
1123
  // The session ROOT is keyed by the active GJC session (GJC_SESSION_ID), NOT the
1120
1124
  // coordinator/tmux identity. Fall back to the coordinator id only for standalone
1121
- // tmux launches with no GJC session context.
1122
- const gjcSessionId = env.GJC_SESSION_ID?.trim() || sessionId;
1125
+ // tmux launches with no GJC session context. Project dotenv pins are already
1126
+ // removed from identityEnv.
1127
+ const gjcSessionId = identityEnv.GJC_SESSION_ID?.trim() || sessionId;
1123
1128
  const sessionStateFile =
1124
- env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
1129
+ identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
1125
1130
  tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
1126
1131
  const signingEnv = coordinatorSidecarSigningEnv(env);
1127
1132
  // Stage only the one-shot channel descriptor (path + key id). The PKCS#8 key
@@ -1136,13 +1141,13 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1136
1141
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform));
1137
1142
  return undefined;
1138
1143
  }
1139
- const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
1144
+ const existingSessionName = allowsExistingTmuxAttach(context.parsed, identityEnv)
1140
1145
  ? "existingBranchSessionName" in context
1141
1146
  ? (context.existingBranchSessionName ?? undefined)
1142
1147
  : resolvedBinary.isPsmux && platform === "win32"
1143
- ? explicitTmuxSessionName(env)
1148
+ ? explicitTmuxSessionName(identityEnv)
1144
1149
  : findExistingSessionForLaunch({
1145
- env,
1150
+ env: identityEnv,
1146
1151
  project,
1147
1152
  branch,
1148
1153
  })
@@ -1154,6 +1159,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1154
1159
  execPath: context.execPath ?? process.execPath,
1155
1160
  extraEnv: {
1156
1161
  ...childSigningEnv,
1162
+ ...childGjcSessionIdAssignment(env, identityEnv, cwd, gjcSessionId),
1157
1163
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
1158
1164
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
1159
1165
  // Carry the GJC-managed session name into the child so tmux-backed
@@ -1210,6 +1216,9 @@ function trustedReplacementAuthority(
1210
1216
  function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
1211
1217
  if (plan.ownerGeneration) return;
1212
1218
  const sessionId = plan.sessionId ?? plan.sessionName;
1219
+ const launchEnv = context.env ?? process.env;
1220
+ const identityEnv = tmuxLaunchIdentityEnv(launchEnv, plan.cwd);
1221
+ const gjcSessionId = identityEnv.GJC_SESSION_ID?.trim() || sessionId;
1213
1222
  const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
1214
1223
  const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
1215
1224
  const replacement = trustedReplacementAuthority(stateDir, sessionId, baseline);
@@ -1234,6 +1243,7 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
1234
1243
  [GJC_COORDINATOR_SIDECAR_KEY_ID_ENV]: plan.coordinatorSidecarBootstrap.keyId,
1235
1244
  }
1236
1245
  : {}),
1246
+ ...childGjcSessionIdAssignment(launchEnv, identityEnv, plan.cwd, gjcSessionId),
1237
1247
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
1238
1248
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: plan.sessionStateFile ?? "",
1239
1249
  // The tmux server inherits the bootstrap secret as child environment;
@@ -1527,7 +1537,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
1527
1537
  try {
1528
1538
  if (plan.attachSessionName) {
1529
1539
  const stateDir = env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim();
1530
- const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
1540
+ const sessionId = tmuxLaunchIdentityEnv(env, plan.cwd)[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
1531
1541
  const generation = env[GJC_TMUX_OWNER_GENERATION_ENV]?.trim();
1532
1542
  if (!stateDir || !sessionId || !generation) throw new Error("gjc_tmux_provider_authority_unavailable");
1533
1543
  plan.authority = readGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation });
@@ -1596,7 +1606,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
1596
1606
  }
1597
1607
  };
1598
1608
  const creationSpawn = plan.authority ? spawnSync : rawSpawnSync;
1599
- const spawnEnv = { ...env };
1609
+ const spawnEnv = { ...tmuxLaunchIdentityEnv(env, plan.cwd) };
1600
1610
  delete spawnEnv[GJC_COORDINATOR_SIDECAR_SIGNING_KEY_ENV];
1601
1611
  const signingEnv = coordinatorSidecarSigningEnv(env);
1602
1612
  Object.assign(spawnEnv, signingEnv);