commandmate 0.25.0 → 0.26.1

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 (219) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +141 -141
  3. package/.next/app-path-routes-manifest.json +28 -28
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/prerender-manifest.json +12 -12
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
  9. package/.next/server/app/api/app/update-check/route.js +1 -1
  10. package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
  11. package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
  12. package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
  24. package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
  26. package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/hooks/permission-request/route.js +1 -1
  31. package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
  40. package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
  46. package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
  48. package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
  52. package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  54. package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
  58. package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
  60. package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
  64. package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
  66. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  67. package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
  70. package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
  72. package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
  76. package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
  78. package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
  82. package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
  84. package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
  88. package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
  90. package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
  94. package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
  96. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
  100. package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
  102. package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
  105. package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
  106. package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
  108. package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
  112. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
  114. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
  116. package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
  117. package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
  118. package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
  119. package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
  120. package/.next/server/app/api/worktrees/[id]/start-polling/route_client-reference-manifest.js +1 -1
  121. package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
  123. package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
  124. package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
  126. package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
  128. package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
  129. package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
  130. package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
  131. package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
  133. package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  135. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  136. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  137. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  138. package/.next/server/app/more/page_client-reference-manifest.js +1 -1
  139. package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
  140. package/.next/server/app/page_client-reference-manifest.js +1 -1
  141. package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
  143. package/.next/server/app/review/page_client-reference-manifest.js +1 -1
  144. package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  145. package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
  146. package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
  147. package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  148. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  149. package/.next/server/app/worktrees/[id]/page.js +1 -1
  150. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  151. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  152. package/.next/server/app-paths-manifest.json +28 -28
  153. package/.next/server/chunks/3241.js +1 -1
  154. package/.next/server/chunks/4455.js +1 -1
  155. package/.next/server/chunks/5959.js +4 -4
  156. package/.next/server/chunks/6130.js +1 -1
  157. package/.next/server/chunks/6274.js +12 -9
  158. package/.next/server/chunks/7458.js +6 -6
  159. package/.next/server/chunks/9373.js +4 -3
  160. package/.next/server/middleware-manifest.json +5 -5
  161. package/.next/server/pages/500.html +1 -1
  162. package/.next/server/pages-manifest.json +1 -1
  163. package/.next/server/server-reference-manifest.json +1 -1
  164. package/.next/static/chunks/{6381-51f9c4a128aadc0b.js → 6381-0e097c0286155883.js} +1 -1
  165. package/.next/static/chunks/app/worktrees/[id]/{page-207c69540fd856ef.js → page-fdfca039b7f5837e.js} +1 -1
  166. package/.next/trace +12 -12
  167. package/dist/cli/commands/task.d.ts.map +1 -1
  168. package/dist/cli/commands/task.js +2 -1
  169. package/dist/cli/commands/verify.d.ts +14 -0
  170. package/dist/cli/commands/verify.d.ts.map +1 -1
  171. package/dist/cli/commands/verify.js +109 -3
  172. package/dist/cli/commands/wait.d.ts +2 -0
  173. package/dist/cli/commands/wait.d.ts.map +1 -1
  174. package/dist/cli/commands/wait.js +184 -4
  175. package/dist/cli/types/api-responses.d.ts +172 -0
  176. package/dist/cli/types/api-responses.d.ts.map +1 -1
  177. package/dist/cli/types/index.d.ts +31 -0
  178. package/dist/cli/types/index.d.ts.map +1 -1
  179. package/dist/cli/types/index.js +26 -0
  180. package/dist/cli/utils/verify-runner.d.ts +36 -1
  181. package/dist/cli/utils/verify-runner.d.ts.map +1 -1
  182. package/dist/cli/utils/verify-runner.js +114 -6
  183. package/dist/server/src/lib/auto-yes-state.js +108 -3
  184. package/dist/server/src/lib/cli-tools/antigravity.js +8 -4
  185. package/dist/server/src/lib/cli-tools/codex.js +5 -1
  186. package/dist/server/src/lib/cli-tools/copilot.js +5 -1
  187. package/dist/server/src/lib/cli-tools/gemini.js +15 -15
  188. package/dist/server/src/lib/cli-tools/opencode.js +9 -3
  189. package/dist/server/src/lib/detection/upstream-faults.js +120 -0
  190. package/dist/server/src/lib/hooks/permission-decision-service.js +13 -5
  191. package/dist/server/src/lib/hooks/sources/antigravity/hooks-config.js +18 -11
  192. package/dist/server/src/lib/hooks/sources/antigravity/source.js +10 -9
  193. package/dist/server/src/lib/hooks/sources/claude/source.js +5 -2
  194. package/dist/server/src/lib/hooks/sources/codex/hooks-config.js +26 -18
  195. package/dist/server/src/lib/hooks/sources/codex/source.js +6 -9
  196. package/dist/server/src/lib/hooks/sources/copilot/hook-settings.js +22 -12
  197. package/dist/server/src/lib/hooks/sources/copilot/source.js +1 -1
  198. package/dist/server/src/lib/hooks/sources/define-source.js +6 -4
  199. package/dist/server/src/lib/hooks/sources/gemini/settings-generator.js +14 -8
  200. package/dist/server/src/lib/hooks/sources/gemini/source.js +29 -29
  201. package/dist/server/src/lib/hooks/sources/index.js +4 -1
  202. package/dist/server/src/lib/hooks/sources/launch-command.js +61 -0
  203. package/dist/server/src/lib/hooks/sources/legacy-relay.js +2 -1
  204. package/dist/server/src/lib/hooks/sources/opencode/source.js +10 -7
  205. package/dist/server/src/lib/polling/prompt-dedup-state.js +95 -0
  206. package/dist/server/src/lib/polling/response-checker.js +12 -0
  207. package/dist/server/src/lib/polling/response-poller-core.js +11 -1
  208. package/dist/server/src/lib/session/agent-session-lifecycle.js +28 -7
  209. package/dist/server/src/lib/session/claude-session.js +5 -1
  210. package/dist/server/src/lib/session/current-output-builder.js +34 -0
  211. package/dist/server/src/lib/verification/env-snapshot.js +21 -3
  212. package/dist/server/src/lib/verification/gate-runner.js +258 -7
  213. package/dist/server/src/lib/verification/machine-lock.js +204 -0
  214. package/dist/server/src/lib/verification/scope-gate.js +126 -13
  215. package/dist/server/src/lib/verification/verify-config.js +85 -3
  216. package/dist/server/src/lib/verification/worktree-index.js +140 -0
  217. package/package.json +1 -1
  218. /package/.next/static/{ehkNc5xDMDkXlua08Bwq0 → wyFLOpmKpr45BHZ4svYpE}/_buildManifest.js +0 -0
  219. /package/.next/static/{ehkNc5xDMDkXlua08Bwq0 → wyFLOpmKpr45BHZ4svYpE}/_ssgManifest.js +0 -0
@@ -63,7 +63,7 @@ exports.buildCodexPermissionHookCommand = buildCodexPermissionHookCommand;
63
63
  exports.buildCodexHookSettings = buildCodexHookSettings;
64
64
  exports.mergeCodexHookSettings = mergeCodexHookSettings;
65
65
  exports.writeCodexHookSettings = writeCodexHookSettings;
66
- exports.buildCodexLaunchCommand = buildCodexLaunchCommand;
66
+ exports.buildCodexLaunchPlan = buildCodexLaunchPlan;
67
67
  const fs_1 = require("fs");
68
68
  const os_1 = require("os");
69
69
  const path_1 = require("path");
@@ -396,7 +396,7 @@ function writeCodexHookSettings(options = {}) {
396
396
  return settingsPath;
397
397
  }
398
398
  /**
399
- * The command that starts codex for one instance.
399
+ * The plan that starts codex for one instance.
400
400
  *
401
401
  * Never throws: hooks are an enhancement to a session that has to start anyway,
402
402
  * so anything that goes wrong here returns the bare executable — which is
@@ -409,32 +409,40 @@ function writeCodexHookSettings(options = {}) {
409
409
  * own (#1757 §5.1.7 found no `CLAUDE_PROJECT_DIR` equivalent). What is left is
410
410
  * the environment of the process CommandMate starts, which a hook inherits.
411
411
  *
412
+ * Since Issue #1846 they are returned as {@link AgentLaunchPlan.env} rather than
413
+ * written onto the front of the command. The bytes a pane receives are
414
+ * unchanged — `renderAgentLaunchCommand` re-emits them in declaration order —
415
+ * but the assignments are now data the launcher applies, which is what let the
416
+ * other three sources that had written the same prefix by hand stop writing it.
417
+ *
412
418
  * @param executablePath - Resolved codex CLI path or bare command name
413
419
  * @param target - The instance being started
414
- * @returns `<assignments> <codex>`, or `executablePath` unchanged when
415
- * injection is off, the ids are unusable, or the file could not be written
420
+ * @returns The command and its environment, or the bare executable with an
421
+ * empty environment when injection is off, the ids are unusable, or the file
422
+ * could not be written
416
423
  */
417
- function buildCodexLaunchCommand(executablePath, target, options = {}) {
424
+ function buildCodexLaunchPlan(executablePath, target, options = {}) {
425
+ const bare = { command: executablePath, settingsPath: null, env: {} };
418
426
  if (!(0, hook_settings_generator_1.isHookInjectionEnabled)())
419
- return executablePath;
427
+ return bare;
420
428
  const instanceId = target.instanceId ?? tool_id_1.CODEX_CLI_TOOL_ID;
421
429
  if (!(0, path_validator_1.isValidWorktreeId)(target.worktreeId) || !(0, types_1.isValidInstanceId)(instanceId)) {
422
430
  // Both become URL parameters the receiver re-validates; a value that would
423
431
  // be rejected there is not worth injecting.
424
432
  logger.warn('codex-hooks-invalid-correlation-key', { worktreeId: target.worktreeId });
425
- return executablePath;
433
+ return bare;
426
434
  }
427
435
  try {
428
436
  const settingsPath = writeCodexHookSettings(options);
429
437
  if (!settingsPath)
430
- return executablePath;
438
+ return bare;
431
439
  const port = options.port ?? (0, env_1.getServerPort)();
432
440
  const query = new URLSearchParams({
433
441
  tool: tool_id_1.CODEX_CLI_TOOL_ID,
434
442
  worktreeId: target.worktreeId,
435
443
  instanceId,
436
444
  });
437
- const assignments = [
445
+ const env = {
438
446
  // Pin the home the file was just written to, rather than trusting the
439
447
  // agent to resolve the same one. Measured on 2026-08-13: a tmux session
440
448
  // inherits the environment of the *tmux server*, which was started long
@@ -445,21 +453,21 @@ function buildCodexLaunchCommand(executablePath, target, options = {}) {
445
453
  // wrote" and "the file codex reads" the same file by construction; in the
446
454
  // ordinary case where nothing sets it, this is the default codex would
447
455
  // have picked anyway.
448
- `${exports.CODEX_HOME_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(getCodexHome(options))}`,
449
- `${exports.CODEX_TOOL_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(tool_id_1.CODEX_CLI_TOOL_ID)}`,
450
- `${exports.CODEX_WORKTREE_ID_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(target.worktreeId)}`,
451
- `${exports.CODEX_INSTANCE_ID_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(instanceId)}`,
452
- `${exports.CODEX_EVENT_URL_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(`http://${CODEX_HOOK_HOST}:${port}${hook_settings_generator_1.AGENT_EVENT_PATH}`)}`,
453
- `${exports.CODEX_PERMISSION_URL_ENV_VAR}=${(0, hook_settings_generator_1.shellQuote)(`http://${CODEX_HOOK_HOST}:${port}${hook_settings_generator_1.PERMISSION_REQUEST_PATH}?${query.toString()}`)}`,
454
- ];
456
+ [exports.CODEX_HOME_ENV_VAR]: getCodexHome(options),
457
+ [exports.CODEX_TOOL_ENV_VAR]: tool_id_1.CODEX_CLI_TOOL_ID,
458
+ [exports.CODEX_WORKTREE_ID_ENV_VAR]: target.worktreeId,
459
+ [exports.CODEX_INSTANCE_ID_ENV_VAR]: instanceId,
460
+ [exports.CODEX_EVENT_URL_ENV_VAR]: `http://${CODEX_HOOK_HOST}:${port}${hook_settings_generator_1.AGENT_EVENT_PATH}`,
461
+ [exports.CODEX_PERMISSION_URL_ENV_VAR]: `http://${CODEX_HOOK_HOST}:${port}${hook_settings_generator_1.PERMISSION_REQUEST_PATH}?${query.toString()}`,
462
+ };
455
463
  const trust = isCodexHookTrustBypassEnabled() ? ` ${exports.CODEX_HOOK_TRUST_BYPASS_FLAG}` : '';
456
- return `${assignments.join(' ')} ${(0, hook_settings_generator_1.shellQuote)(executablePath)}${trust}`;
464
+ return { command: `${(0, hook_settings_generator_1.shellQuote)(executablePath)}${trust}`, settingsPath, env };
457
465
  }
458
466
  catch (error) {
459
467
  logger.warn('codex-hooks-config-write-failed', {
460
468
  worktreeId: target.worktreeId,
461
469
  error: error instanceof Error ? error.message : String(error),
462
470
  });
463
- return executablePath;
471
+ return bare;
464
472
  }
465
473
  }
@@ -131,13 +131,10 @@ exports.codexAgentEventSource = (0, define_source_1.definePushHookSource)({
131
131
  // S3 / S4 / S5. The whole of the config generation lives in `./hooks-config`,
132
132
  // which never throws — a session that starts without hooks is the status quo,
133
133
  // a session that fails to start is not.
134
- prepareLaunch: (target, executablePath) => {
135
- const command = (0, hooks_config_1.buildCodexLaunchCommand)(executablePath, target);
136
- // Reported only when the command actually reflects an injection. Both
137
- // `CM_AGENT_HOOKS_INJECT=0` and a file that could not be written return the
138
- // bare executable, and claiming a settings file then would name a file this
139
- // launch is not using.
140
- const settingsPath = command === executablePath ? null : (0, hooks_config_1.getCodexHooksPath)();
141
- return { command, settingsPath };
142
- },
134
+ //
135
+ // Issue #1846: the six correlation variables are the plan's `env` rather than
136
+ // a `NAME=value ` prefix written onto `command` here. `settingsPath` now comes
137
+ // straight out of the builder it used to be re-derived by comparing the
138
+ // command against the bare executable, which was the same fact inferred twice.
139
+ prepareLaunch: ({ target, executablePath }) => (0, hooks_config_1.buildCodexLaunchPlan)(executablePath, target),
143
140
  });
@@ -80,7 +80,7 @@ exports.buildCopilotHookSettings = buildCopilotHookSettings;
80
80
  exports.mergeCopilotHookSettings = mergeCopilotHookSettings;
81
81
  exports.readCopilotSettings = readCopilotSettings;
82
82
  exports.writeCopilotHookSettings = writeCopilotHookSettings;
83
- exports.buildCopilotEnvironmentPrefix = buildCopilotEnvironmentPrefix;
83
+ exports.buildCopilotHookEnvironment = buildCopilotHookEnvironment;
84
84
  exports.isCopilotHookInjectionEnabled = isCopilotHookInjectionEnabled;
85
85
  exports.buildCopilotLaunchCommand = buildCopilotLaunchCommand;
86
86
  const fs_1 = require("fs");
@@ -411,16 +411,25 @@ function writeCopilotHookSettings(options = {}) {
411
411
  return settingsPath;
412
412
  }
413
413
  /**
414
- * The environment assignments that tell a hook which instance fired it.
415
- *
416
- * A prefix on the command line rather than `tmux set-environment`: the pane is
417
- * created before the agent starts and the assignment then travels with the one
418
- * process that matters, out of reach of `sanitizeSessionEnvironment` and of
419
- * anything else that edits the session's environment later.
414
+ * The environment that tells a hook which instance fired it.
415
+ *
416
+ * Applied to the launched process rather than through `tmux set-environment`:
417
+ * the pane is created before the agent starts and the assignment then travels
418
+ * with the one process that matters, out of reach of
419
+ * `sanitizeSessionEnvironment` and of anything else that edits the session's
420
+ * environment later.
421
+ *
422
+ * Returned as a map since Issue #1846. It used to be a pre-quoted
423
+ * `NAME=value NAME=value` string that the caller concatenated in front of the
424
+ * command — the same workaround codex, gemini and antigravity had each written
425
+ * separately, all four assuming a shell nobody had declared. The rendering now
426
+ * happens once, in `../launch-command`, and the bytes are identical.
420
427
  */
421
- function buildCopilotEnvironmentPrefix(worktreeId, instanceId) {
422
- return (`${exports.COPILOT_WORKTREE_ID_ENV}=${(0, hook_settings_generator_1.shellQuote)(worktreeId)} ` +
423
- `${exports.COPILOT_INSTANCE_ID_ENV}=${(0, hook_settings_generator_1.shellQuote)(instanceId)}`);
428
+ function buildCopilotHookEnvironment(worktreeId, instanceId) {
429
+ return {
430
+ [exports.COPILOT_WORKTREE_ID_ENV]: worktreeId,
431
+ [exports.COPILOT_INSTANCE_ID_ENV]: instanceId,
432
+ };
424
433
  }
425
434
  /**
426
435
  * Whether to inject at all.
@@ -445,7 +454,7 @@ function isCopilotHookInjectionEnabled() {
445
454
  * @param target - The instance being started
446
455
  */
447
456
  function buildCopilotLaunchCommand(executablePath, target, options = {}) {
448
- const bare = { command: executablePath, settingsPath: null };
457
+ const bare = { command: executablePath, settingsPath: null, env: {} };
449
458
  if (!isCopilotHookInjectionEnabled())
450
459
  return bare;
451
460
  const instanceId = target.instanceId ?? tool_id_1.COPILOT_CLI_TOOL_ID;
@@ -459,8 +468,9 @@ function buildCopilotLaunchCommand(executablePath, target, options = {}) {
459
468
  try {
460
469
  const settingsPath = writeCopilotHookSettings(options);
461
470
  return {
462
- command: `${buildCopilotEnvironmentPrefix(target.worktreeId, instanceId)} ${executablePath}`,
471
+ command: executablePath,
463
472
  settingsPath,
473
+ env: buildCopilotHookEnvironment(target.worktreeId, instanceId),
464
474
  };
465
475
  }
466
476
  catch (error) {
@@ -206,5 +206,5 @@ exports.copilotAgentEventSource = (0, define_source_1.definePushHookSource)({
206
206
  // machine without hooks rather than to a guess.
207
207
  parseQuestion: () => null,
208
208
  encodeVerdict: encodeCopilotVerdict,
209
- prepareLaunch: (target, executablePath) => (0, hook_settings_1.buildCopilotLaunchCommand)(executablePath, target),
209
+ prepareLaunch: ({ target, executablePath }) => (0, hook_settings_1.buildCopilotLaunchCommand)(executablePath, target),
210
210
  });
@@ -164,10 +164,12 @@ function definePullEventSource(spec) {
164
164
  // has been open for hours and carries no reply channel.
165
165
  return { kind: 'outOfBand' };
166
166
  },
167
- prepareLaunch: spec.prepareLaunch ?? ((_target, executablePath) => ({
168
- command: executablePath,
169
- settingsPath: null,
170
- })),
167
+ prepareLaunch: spec.prepareLaunch ??
168
+ ((context) => ({
169
+ command: context.executablePath,
170
+ settingsPath: null,
171
+ env: {},
172
+ })),
171
173
  subscribe: spec.subscribe,
172
174
  decide: spec.decide,
173
175
  listPending: spec.listPending,
@@ -60,6 +60,7 @@ const types_1 = require("../../../../lib/cli-tools/types");
60
60
  const hook_settings_generator_1 = require("../../../../lib/hooks/hook-settings-generator");
61
61
  const logger_1 = require("../../../../lib/logger");
62
62
  const event_mapper_1 = require("../event-mapper");
63
+ const launch_command_1 = require("../launch-command");
63
64
  const shared_config_tree_1 = require("./shared-config-tree");
64
65
  const tool_id_1 = require("./tool-id");
65
66
  const logger = (0, logger_1.createLogger)('lib/hooks/sources/gemini/settings-generator');
@@ -278,7 +279,7 @@ function writeGeminiHookSettings(worktreePath, target) {
278
279
  }
279
280
  }
280
281
  /**
281
- * The command that launches gemini for one instance.
282
+ * The command and environment that launch gemini for one instance.
282
283
  *
283
284
  * `CM_HOOK_URL` is the whole of the instance correlation, and it is an
284
285
  * environment variable rather than a flag in the settings file for a reason the
@@ -293,23 +294,28 @@ function writeGeminiHookSettings(worktreePath, target) {
293
294
  * lands on the primary instance. That is a degradation, not a
294
295
  * misattribution.
295
296
  *
296
- * Never throws, and returns the executable unchanged when injection is off, so
297
- * `CM_AGENT_HOOKS_INJECT=0` produces the byte-identical command line this tool
298
- * used before #1762.
297
+ * Returned as a `{ command, env }` pair since Issue #1846, instead of as one
298
+ * string with the variable written onto the front of `gemini`. Three other
299
+ * sources had built the same prefix by hand; `renderAgentLaunchCommand` now
300
+ * writes it once, and the line a pane receives is byte-identical.
301
+ *
302
+ * Never throws, and returns the executable with an empty environment when
303
+ * injection is off, so `CM_AGENT_HOOKS_INJECT=0` produces the byte-identical
304
+ * command line this tool used before #1762.
299
305
  *
300
306
  * @param executablePath - `gemini`, or a resolved path to it
301
307
  * @param target - The instance being started
302
- * @returns The command line to type into the pane
308
+ * @returns The command to type into the pane and the environment it needs
303
309
  */
304
310
  function buildGeminiLaunchCommand(executablePath, target, options = {}) {
305
311
  if (!(0, hook_settings_generator_1.isHookInjectionEnabled)())
306
- return executablePath;
312
+ return { command: executablePath, env: {} };
307
313
  if (!(0, types_1.isValidInstanceId)((0, hook_settings_generator_1.resolveTargetInstanceId)(target))) {
308
314
  // About to become a URL parameter the receiver re-validates; a value that
309
315
  // would be rejected there is not worth injecting.
310
316
  logger.warn('gemini-hook-invalid-instance-id', { worktreeId: target.worktreeId });
311
- return executablePath;
317
+ return { command: executablePath, env: {} };
312
318
  }
313
319
  const url = (0, hook_settings_generator_1.buildAgentEventUrl)({ ...target, cliToolId: tool_id_1.GEMINI_CLI_TOOL_ID }, options);
314
- return `CM_HOOK_URL=${(0, hook_settings_generator_1.shellQuote)(url)} ${(0, hook_settings_generator_1.shellQuote)(executablePath)}`;
320
+ return { command: (0, hook_settings_generator_1.shellQuote)(executablePath), env: { [launch_command_1.AGENT_EVENT_URL_ENV_VAR]: url } };
315
321
  }
@@ -57,7 +57,7 @@
57
57
  */
58
58
  Object.defineProperty(exports, "__esModule", { value: true });
59
59
  exports.geminiAgentEventSource = void 0;
60
- exports.injectGeminiHookSettings = injectGeminiHookSettings;
60
+ exports.prepareGeminiLaunch = prepareGeminiLaunch;
61
61
  const types_1 = require("../../../../lib/cli-tools/types");
62
62
  const define_source_1 = require("../define-source");
63
63
  const event_mapper_1 = require("../event-mapper");
@@ -66,32 +66,40 @@ const event_vocabulary_1 = require("./event-vocabulary");
66
66
  const settings_generator_1 = require("./settings-generator");
67
67
  const tool_id_1 = require("./tool-id");
68
68
  /**
69
- * Write this worktree's `.gemini/settings.json`, on the session-creation path.
69
+ * Write this worktree's `.gemini/settings.json` and build the launch line
70
+ * (S3 / S4 / S5).
70
71
  *
71
- * **This is the one thing about gemini that {@link AgentEventSource} cannot
72
- * express.** `prepareLaunch` receives an {@link AgentInstanceRef}, which is
73
- * `(worktreeId, cliToolId, instanceId)` and carries no filesystem path — the
74
- * right shape for the four tools whose config is global or per-instance, and one
75
- * field short for the only tool whose config is per-*worktree*. Rather than
76
- * widen the interface for one tool, or resolve the path by pulling the database
77
- * into the module every receiver imports, the caller that already holds the path
78
- * passes it here and `prepareLaunch` builds the command line.
72
+ * **This used to be the one thing {@link AgentEventSource} could not express.**
73
+ * `prepareLaunch` took `(target, executablePath)`, gemini's config lives at
74
+ * `<worktree>/.gemini/settings.json`, and an `AgentInstanceRef` carries no
75
+ * filesystem path so #1762 exported `injectGeminiHookSettings(worktreePath,
76
+ * target)` beside the source and `cli-tools/gemini.ts` called both. One of the
77
+ * six sources wrote its config from a different call site than the other five,
78
+ * and a reader of this file could not tell that the config was written at all.
79
79
  *
80
- * Reported rather than worked around: see the Issue #1762 entry in `CHANGELOG.md`.
80
+ * Issue #1846 put `worktreePath` on {@link AgentLaunchContext} instead, so the
81
+ * write happens here, `settingsPath` reports the file that was actually
82
+ * written, and the second entry point is gone.
81
83
  *
82
- * @param worktreePath - Absolute path of the worktree the session runs in
83
- * @param target - The instance being created
84
- * @returns The settings file written, or null when nothing was written
84
+ * Never throws `writeGeminiHookSettings` swallows its own failures and answers
85
+ * null, and a gemini session with no hooks is the pre-#1762 status quo.
86
+ *
87
+ * @param context - The instance, its executable, and the worktree it runs in
88
+ * @returns The command, its environment, and the settings file when one landed
85
89
  */
86
- function injectGeminiHookSettings(worktreePath, target) {
90
+ function prepareGeminiLaunch({ target, executablePath, worktreePath, }) {
87
91
  const settingsTarget = {
88
92
  worktreeId: target.worktreeId,
89
93
  instanceId: target.instanceId,
90
94
  cliToolId: tool_id_1.GEMINI_CLI_TOOL_ID,
91
95
  };
92
- if (!(0, types_1.isValidInstanceId)(target.instanceId ?? tool_id_1.GEMINI_CLI_TOOL_ID))
93
- return null;
94
- return (0, settings_generator_1.writeGeminiHookSettings)(worktreePath, settingsTarget);
96
+ // Checked before the write as well as inside `buildGeminiLaunchCommand`: an
97
+ // id the receiver would reject is an id no settings file should name either.
98
+ const settingsPath = (0, types_1.isValidInstanceId)(target.instanceId ?? tool_id_1.GEMINI_CLI_TOOL_ID)
99
+ ? (0, settings_generator_1.writeGeminiHookSettings)(worktreePath, settingsTarget)
100
+ : null;
101
+ const { command, env } = (0, settings_generator_1.buildGeminiLaunchCommand)(executablePath, settingsTarget);
102
+ return { command, settingsPath, env };
95
103
  }
96
104
  /**
97
105
  * Gemini CLI as an event source.
@@ -137,15 +145,7 @@ exports.geminiAgentEventSource = (0, define_source_1.definePushHookSource)({
137
145
  // escalation would add dialogs the Policy Engine had already settled. So every
138
146
  // verdict encodes to the empty body, which the bundle reads as no opinion.
139
147
  encodeVerdict: () => ({}),
140
- // The settings file is written by `injectGeminiHookSettings`, which the
141
- // session module calls with the worktree path this interface cannot carry.
142
- // `settingsPath` is therefore null here rather than wrong.
143
- prepareLaunch: (target, executablePath) => ({
144
- command: (0, settings_generator_1.buildGeminiLaunchCommand)(executablePath, {
145
- worktreeId: target.worktreeId,
146
- instanceId: target.instanceId,
147
- cliToolId: tool_id_1.GEMINI_CLI_TOOL_ID,
148
- }),
149
- settingsPath: null,
150
- }),
148
+ // Config write and command line, both from here since #1846. `CM_HOOK_URL` is
149
+ // the plan's `env` rather than a prefix on `command`.
150
+ prepareLaunch: prepareGeminiLaunch,
151
151
  });
@@ -12,7 +12,7 @@
12
12
  * @module lib/hooks/sources
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.OPENCODE_CLI_TOOL_ID = exports.CODEX_CLI_TOOL_ID = exports.ANTIGRAVITY_CLI_TOOL_ID = exports.GEMINI_CLI_TOOL_ID = exports.COPILOT_CLI_TOOL_ID = exports.CLAUDE_CLI_TOOL_ID = exports.isAbstainSafe = exports.describeNoDecision = exports.describeAbstain = exports.resetPendingDecisions = exports.openDecisionSlot = exports.listOpenDecisions = exports.closeDecisionSlot = exports.answerPendingDecision = exports.TOOL_CALL_ID_FIELDS = exports.SESSION_ID_FIELDS = exports.extractSnakeCaseEventDetail = exports.CAMEL_CASE_HOOK_EVENT_NAMES = exports.whenNamed = exports.runEventMappers = exports.resetUnknownEventTallies = exports.readStringField = exports.readNestedString = exports.readFirstStringField = exports.isPlainObject = exports.getUnknownEventTally = exports.fromNameTable = exports.boundDetail = exports.definePushHookSource = exports.definePullEventSource = exports.unregisterAgentEventSource = exports.registerAgentEventSource = exports.listAgentEventSources = exports.hasAgentEventSource = exports.getAgentEventSource = void 0;
15
+ exports.OPENCODE_CLI_TOOL_ID = exports.CODEX_CLI_TOOL_ID = exports.ANTIGRAVITY_CLI_TOOL_ID = exports.GEMINI_CLI_TOOL_ID = exports.COPILOT_CLI_TOOL_ID = exports.CLAUDE_CLI_TOOL_ID = exports.isAbstainSafe = exports.describeNoDecision = exports.describeAbstain = exports.resetPendingDecisions = exports.openDecisionSlot = exports.listOpenDecisions = exports.closeDecisionSlot = exports.answerPendingDecision = exports.TOOL_CALL_ID_FIELDS = exports.SESSION_ID_FIELDS = exports.extractSnakeCaseEventDetail = exports.CAMEL_CASE_HOOK_EVENT_NAMES = exports.whenNamed = exports.runEventMappers = exports.resetUnknownEventTallies = exports.readStringField = exports.readNestedString = exports.readFirstStringField = exports.isPlainObject = exports.getUnknownEventTally = exports.fromNameTable = exports.boundDetail = exports.renderAgentLaunchCommand = exports.AGENT_EVENT_URL_ENV_VAR = exports.definePushHookSource = exports.definePullEventSource = exports.unregisterAgentEventSource = exports.registerAgentEventSource = exports.listAgentEventSources = exports.hasAgentEventSource = exports.getAgentEventSource = void 0;
16
16
  var registry_1 = require("./registry");
17
17
  Object.defineProperty(exports, "getAgentEventSource", { enumerable: true, get: function () { return registry_1.getAgentEventSource; } });
18
18
  Object.defineProperty(exports, "hasAgentEventSource", { enumerable: true, get: function () { return registry_1.hasAgentEventSource; } });
@@ -22,6 +22,9 @@ Object.defineProperty(exports, "unregisterAgentEventSource", { enumerable: true,
22
22
  var define_source_1 = require("./define-source");
23
23
  Object.defineProperty(exports, "definePullEventSource", { enumerable: true, get: function () { return define_source_1.definePullEventSource; } });
24
24
  Object.defineProperty(exports, "definePushHookSource", { enumerable: true, get: function () { return define_source_1.definePushHookSource; } });
25
+ var launch_command_1 = require("./launch-command");
26
+ Object.defineProperty(exports, "AGENT_EVENT_URL_ENV_VAR", { enumerable: true, get: function () { return launch_command_1.AGENT_EVENT_URL_ENV_VAR; } });
27
+ Object.defineProperty(exports, "renderAgentLaunchCommand", { enumerable: true, get: function () { return launch_command_1.renderAgentLaunchCommand; } });
25
28
  var event_mapper_1 = require("./event-mapper");
26
29
  Object.defineProperty(exports, "boundDetail", { enumerable: true, get: function () { return event_mapper_1.boundDetail; } });
27
30
  Object.defineProperty(exports, "fromNameTable", { enumerable: true, get: function () { return event_mapper_1.fromNameTable; } });
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * The one place an {@link AgentLaunchPlan}'s environment is applied
4
+ * (Issue #1846, 申し送り 2).
5
+ *
6
+ * Before this module, four of the six sources — codex, copilot, gemini,
7
+ * antigravity — each wrote their own `NAME=value ` prefix onto the front of
8
+ * `AgentLaunchPlan.command` and trusted the caller to paste the result into a
9
+ * shell. Four implementations, four independently written prefixes, one
10
+ * assumption none of them declared: that the launcher is a shell.
11
+ *
12
+ * The assumption holds today, because every one of those launchers is
13
+ * `sendKeys(sessionName, line, true)` typing into a tmux pane. It is still the
14
+ * wrong place for the knowledge:
15
+ *
16
+ * - a source whose executable is argv rather than a shell word cannot use the
17
+ * trick at all, and the seventh tool is as likely to be that as not;
18
+ * - a caller that wants to log or display the command has no way to take the
19
+ * assignments back off, and those values are URLs with correlation keys in
20
+ * them;
21
+ * - quoting is a shell property, and four copies of it is four chances to get
22
+ * a worktree path with a space in it wrong.
23
+ *
24
+ * So the plan declares `env` as data and this renders it, once. A source that
25
+ * needs no environment declares `{}` and renders to its bare command, byte for
26
+ * byte what it produced before.
27
+ *
28
+ * @module lib/hooks/sources/launch-command
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.AGENT_EVENT_URL_ENV_VAR = void 0;
32
+ exports.renderAgentLaunchCommand = renderAgentLaunchCommand;
33
+ const hook_settings_generator_1 = require("../../../lib/hooks/hook-settings-generator");
34
+ /**
35
+ * The variable `scripts/hooks/cmate-agent-event.sh` reads its endpoint from.
36
+ *
37
+ * One constant since Issue #1846. codex, gemini and antigravity each carried
38
+ * their own `'CM_HOOK_URL'` literal, which is three places for the relay's one
39
+ * contract to drift — and the failure mode of a drifted name is a relay that
40
+ * silently falls back to its default endpoint and attributes every event to the
41
+ * primary instance.
42
+ */
43
+ exports.AGENT_EVENT_URL_ENV_VAR = 'CM_HOOK_URL';
44
+ /**
45
+ * Turn a plan into the line a shell can run.
46
+ *
47
+ * Assignments come out in declaration order — `Record` preserves insertion
48
+ * order for string keys — so a plan that puts `CODEX_HOME` first still renders
49
+ * it first, which is what keeps this a refactor rather than a change of bytes.
50
+ * Values are quoted; names are not, because a name that needed quoting would
51
+ * not be a name a shell would assign.
52
+ *
53
+ * @param plan - What the source's `prepareLaunch` returned
54
+ * @returns `NAME='value' … command`, or just `command` when `env` is empty
55
+ */
56
+ function renderAgentLaunchCommand(plan) {
57
+ const assignments = Object.entries(plan.env).map(([name, value]) => `${name}=${(0, hook_settings_generator_1.shellQuote)(value)}`);
58
+ if (assignments.length === 0)
59
+ return plan.command;
60
+ return `${assignments.join(' ')} ${plan.command}`;
61
+ }
@@ -76,9 +76,10 @@ function createLegacyRelaySource(cliToolId) {
76
76
  encodeVerdict: source_1.encodeClaudeVerdict,
77
77
  // CommandMate does not inject config for these tools; their `startSession`
78
78
  // in `src/lib/cli-tools/<tool>.ts` launches them bare.
79
- prepareLaunch: (_target, executablePath) => ({
79
+ prepareLaunch: ({ executablePath }) => ({
80
80
  command: executablePath,
81
81
  settingsPath: null,
82
+ env: {},
82
83
  }),
83
84
  });
84
85
  }
@@ -202,19 +202,22 @@ async function probeOpencodeActivity(target) {
202
202
  * because the default is the security property: the server is unauthenticated,
203
203
  * and `--mdns` (never passed) would move it to `0.0.0.0` (#1758 §5.8.3).
204
204
  */
205
- function prepareOpencodeLaunch(target, executablePath) {
206
- if (!(0, hook_settings_generator_1.isHookInjectionEnabled)()) {
207
- return { command: executablePath, settingsPath: null };
208
- }
205
+ function prepareOpencodeLaunch({ target, executablePath, }) {
206
+ // `env` is empty for every branch: opencode is the one source that needs no
207
+ // correlation variable at all, because CommandMate holds the connection and
208
+ // therefore already knows which instance the frames belong to (#1846).
209
+ const bare = { command: executablePath, settingsPath: null, env: {} };
210
+ if (!(0, hook_settings_generator_1.isHookInjectionEnabled)())
211
+ return bare;
209
212
  const port = (0, ports_1.getAssignedOpencodePort)(target);
210
- if (port === null) {
211
- return { command: executablePath, settingsPath: null };
212
- }
213
+ if (port === null)
214
+ return bare;
213
215
  return {
214
216
  command: `${(0, hook_settings_generator_1.shellQuote)(executablePath)} --port ${port} --hostname ${client_1.OPENCODE_SERVER_HOST}`,
215
217
  // Nothing is written to disk. `configScope: 'none'` says the same thing to
216
218
  // a caller that asks the capabilities instead of the plan.
217
219
  settingsPath: null,
220
+ env: {},
218
221
  };
219
222
  }
220
223
  /**
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * In-memory tally of prompts the content-hash dedup guard declined to save
4
+ * (Issue #1695).
5
+ *
6
+ * `isDuplicatePrompt` (see `prompt-dedup.ts`, Issue #565) drops a detected
7
+ * prompt without writing anything to the database, and until now the only trace
8
+ * it left was `logger.info('duplicate-prompt-skipped', …)` in the server log.
9
+ * From `commandmate capture --json` the outcome is indistinguishable from the
10
+ * detection layer never having classified the frame at all (Issue #1676): both
11
+ * answer "no prompt was recorded". This module keeps the skip count and the
12
+ * last skip time per session so `buildCurrentOutput` can publish them and an
13
+ * operator can tell "the guard suppressed it" from "nothing saw it".
14
+ *
15
+ * Exposure only: nothing reads this to make a decision. The guard itself keeps
16
+ * its own hash cache and is untouched by whether anyone reads the tally.
17
+ *
18
+ * **Cumulative, not per-turn.** The hash cache behind the guard is cleared by
19
+ * `stopPolling()` at the end of every polling cycle; the tally deliberately is
20
+ * not, because the question it answers ("has this session been dropping
21
+ * prompts, and how recently?") spans turns. `lastSkippedAt` is what dates the
22
+ * evidence — a large `skippedCount` with an hours-old `lastSkippedAt` is
23
+ * history, the same count with a fresh one is happening right now.
24
+ *
25
+ * Keyed like `auto-yes-suppression-state` (worktreeId / cliToolId / instanceId
26
+ * via `buildCompositeKey`) so the reader in `current-output-builder` can look it
27
+ * up with the identifiers the request already carries, rather than with the
28
+ * poller key, whose builder lives behind `response-poller-core`'s import graph.
29
+ *
30
+ * In-memory and not in SQLite for the same reason the suppression record is:
31
+ * it describes a live tmux session, and a session does not survive a server
32
+ * restart for the record to still be about.
33
+ *
34
+ * @module lib/polling/prompt-dedup-state
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.recordPromptDedupSkip = recordPromptDedupSkip;
38
+ exports.getPromptDedupSkips = getPromptDedupSkips;
39
+ exports.renamePromptDedupSkips = renamePromptDedupSkips;
40
+ exports.clearPromptDedupSkips = clearPromptDedupSkips;
41
+ const auto_yes_state_1 = require("../../lib/auto-yes-state");
42
+ /** compositeKey -> running tally of suppressed prompts for that session. */
43
+ const promptDedupSkips = globalThis.__promptDedupSkips ??
44
+ (globalThis.__promptDedupSkips = new Map());
45
+ /**
46
+ * Count one prompt the dedup guard suppressed for `instanceId`.
47
+ *
48
+ * @param at - Epoch ms; defaults to now. Overridable so tests are deterministic.
49
+ */
50
+ function recordPromptDedupSkip(worktreeId, cliToolId, instanceId, at = Date.now()) {
51
+ const key = (0, auto_yes_state_1.buildCompositeKey)(worktreeId, cliToolId, instanceId);
52
+ const previous = promptDedupSkips.get(key);
53
+ promptDedupSkips.set(key, {
54
+ skippedCount: (previous?.skippedCount ?? 0) + 1,
55
+ lastSkippedAt: at,
56
+ });
57
+ }
58
+ /**
59
+ * @returns The tally for this session. Never null: a session the guard has
60
+ * never fired for reports `{ skippedCount: 0, lastSkippedAt: null }`, so
61
+ * `capture --json` prints an explicit "no prompt was suppressed" rather than
62
+ * an absent key a consumer has to guess about.
63
+ */
64
+ function getPromptDedupSkips(worktreeId, cliToolId, instanceId) {
65
+ return (promptDedupSkips.get((0, auto_yes_state_1.buildCompositeKey)(worktreeId, cliToolId, instanceId)) ?? {
66
+ skippedCount: 0,
67
+ lastSkippedAt: null,
68
+ });
69
+ }
70
+ /**
71
+ * Carry the tally across a worktree-ID rename (Issue #1621 Phase 3).
72
+ *
73
+ * The dedup hash caches are deliberately moved rather than dropped by
74
+ * `migrateResponsePollerWorktreeIds`, so the guard goes on suppressing the
75
+ * prompt currently on screen under the new ID. A tally left behind would then
76
+ * report zero skips for a session that is still skipping — the false negative
77
+ * this module exists to prevent, appearing at exactly the moment the record
78
+ * matters.
79
+ *
80
+ * No-op when the IDs match or nothing is recorded under the old one.
81
+ */
82
+ function renamePromptDedupSkips(oldWorktreeId, newWorktreeId, cliToolId, instanceId) {
83
+ if (oldWorktreeId === newWorktreeId)
84
+ return;
85
+ const oldKey = (0, auto_yes_state_1.buildCompositeKey)(oldWorktreeId, cliToolId, instanceId);
86
+ const record = promptDedupSkips.get(oldKey);
87
+ promptDedupSkips.delete(oldKey);
88
+ if (record !== undefined) {
89
+ promptDedupSkips.set((0, auto_yes_state_1.buildCompositeKey)(newWorktreeId, cliToolId, instanceId), record);
90
+ }
91
+ }
92
+ /** Drop every recorded tally. Test seam. */
93
+ function clearPromptDedupSkips() {
94
+ promptDedupSkips.clear();
95
+ }
@@ -25,6 +25,7 @@ const response_cleaner_1 = require("../response-cleaner");
25
25
  const copilot_constants_1 = require("../../config/copilot-constants");
26
26
  const tui_accumulator_1 = require("../tui-accumulator");
27
27
  const prompt_dedup_1 = require("./prompt-dedup");
28
+ const prompt_dedup_state_1 = require("./prompt-dedup-state");
28
29
  const response_dedup_1 = require("./response-dedup");
29
30
  const response_poller_core_1 = require("./response-poller-core");
30
31
  const push_1 = require("../../lib/push");
@@ -446,6 +447,11 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
446
447
  const pollerKey = (0, response_poller_core_1.getPollerKey)(worktreeId, cliToolId, instanceId);
447
448
  const normalizedForDedup = (0, prompt_dedup_1.normalizePromptForDedup)(promptContent, cliToolId);
448
449
  if ((0, prompt_dedup_1.isDuplicatePrompt)(pollerKey, normalizedForDedup)) {
450
+ // Issue #1695: the log line below is invisible to `commandmate capture
451
+ // --json`, so a suppressed prompt and a prompt the detection layer never
452
+ // classified (#1676) look identical from the CLI — both say "nothing was
453
+ // recorded". Count the skip so the payload can tell them apart.
454
+ (0, prompt_dedup_state_1.recordPromptDedupSkip)(worktreeId, cliToolId, instanceId);
449
455
  logger.info('duplicate-prompt-skipped', { worktreeId, cliToolId });
450
456
  return false;
451
457
  }
@@ -565,6 +571,12 @@ async function checkForResponse(worktreeId, cliToolId, instanceId) {
565
571
  if (!lineCountIsCursor) {
566
572
  const pollerKey = (0, response_poller_core_1.getPollerKey)(worktreeId, cliToolId, instanceId);
567
573
  if ((0, response_dedup_1.isDuplicateResponse)(pollerKey, cleanedResponse)) {
574
+ // Issue #1695: this branch used to drop the response silently — the
575
+ // prompt-side guard above has logged its skip since #565, this one
576
+ // logged nothing at all, so a reply that never reached History left no
577
+ // trace anywhere. Same action name shape as its sibling so both skips
578
+ // are found by one grep.
579
+ logger.info('duplicate-response-skipped', { worktreeId, cliToolId, instanceId: resolvedInstanceId });
568
580
  (0, db_1.updateSessionState)(db, worktreeId, cliToolId, result.lineCount, resolvedInstanceId);
569
581
  return false;
570
582
  }