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
@@ -13,6 +13,7 @@ const logger_1 = require("../../lib/logger");
13
13
  const tui_accumulator_1 = require("../tui-accumulator");
14
14
  const prompt_dedup_1 = require("./prompt-dedup");
15
15
  const response_dedup_1 = require("./response-dedup");
16
+ const prompt_dedup_state_1 = require("./prompt-dedup-state");
16
17
  const response_checker_1 = require("./response-checker");
17
18
  const terminal_broadcast_1 = require("../../lib/realtime/terminal-broadcast");
18
19
  const logger = (0, logger_1.createLogger)('response-poller');
@@ -205,7 +206,8 @@ function splitPollerKey(pollerKey) {
205
206
  * - `pollingStartTimes`, so MAX_POLLING_DURATION still measures from when the
206
207
  * turn actually began rather than restarting the 30-minute budget;
207
208
  * - the prompt and response dedup hashes, so the screen currently on display is
208
- * not saved a second time as a new message.
209
+ * not saved a second time as a new message, together with the prompt-skip
210
+ * tally that explains those suppressions to `capture --json` (Issue #1695).
209
211
  *
210
212
  * The TUI accumulator (opencode / copilot only) is re-initialised empty at the
211
213
  * new key instead of carried over: `tui-accumulator.ts` exposes no way to write
@@ -266,6 +268,14 @@ function migrateResponsePollerWorktreeIds(renames, resolveCliToolId) {
266
268
  exports.pollingStartTimes.delete(move.oldKey);
267
269
  (0, prompt_dedup_1.renamePromptHashCacheKey)(move.oldKey, move.newKey);
268
270
  (0, response_dedup_1.renameResponseHashCacheKey)(move.oldKey, move.newKey);
271
+ // Issue #1695: the prompt hash moved on the line above, so the guard keeps
272
+ // suppressing the prompt on screen under the new ID. Its tally has to move
273
+ // with it, or `capture --json` reports zero skips for a session that is
274
+ // still skipping. Old worktree ID comes back out of the key we just left.
275
+ const movedFrom = splitPollerKey(move.oldKey);
276
+ if (movedFrom) {
277
+ (0, prompt_dedup_state_1.renamePromptDedupSkips)(movedFrom.worktreeId, move.newWorktreeId, move.cliToolId, move.instanceId);
278
+ }
269
279
  (0, tui_accumulator_1.clearTuiAccumulator)(move.oldKey);
270
280
  }
271
281
  for (const pollerKey of abandoned) {
@@ -39,6 +39,7 @@
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.beginAgentSession = beginAgentSession;
41
41
  exports.prepareAgentLaunch = prepareAgentLaunch;
42
+ exports.buildAgentLaunchCommandLine = buildAgentLaunchCommandLine;
42
43
  const sources_1 = require("../../lib/hooks/sources");
43
44
  const agent_event_state_1 = require("../../lib/session/agent-event-state");
44
45
  const logger_1 = require("../../lib/logger");
@@ -70,13 +71,33 @@ function beginAgentSession(target, at = Date.now()) {
70
71
  * does not import a settings generator: which file gets written, and whether
71
72
  * one gets written at all, is the source's business.
72
73
  *
73
- * Never throws — see {@link AgentEventSource.prepareLaunch}. A tool whose
74
- * config could not be written starts bare, which is the pre-#1722 status quo.
74
+ * Never throws — see `AgentEventSource.prepareLaunch`. A tool whose config
75
+ * could not be written starts bare, which is the pre-#1722 status quo.
75
76
  *
76
- * @param target - The instance being started
77
- * @param executablePath - Resolved path to the agent CLI
78
- * @returns The command line, and the config file when one was written
77
+ * @param context - The instance, its executable, and the worktree it runs in
78
+ * @returns The command, its environment, and the config file when one landed
79
79
  */
80
- function prepareAgentLaunch(target, executablePath) {
81
- return (0, sources_1.getAgentEventSource)(target.cliToolId).prepareLaunch(target, executablePath);
80
+ function prepareAgentLaunch(context) {
81
+ return (0, sources_1.getAgentEventSource)(context.target.cliToolId).prepareLaunch(context);
82
+ }
83
+ /**
84
+ * The single line a `startSession` types into its pane (Issue #1846).
85
+ *
86
+ * `prepareAgentLaunch` returns a plan whose environment is *data*; this is the
87
+ * one function that turns that data into shell assignments. Before #1846 four
88
+ * sources each wrote their own `NAME=value ` prefix onto `plan.command`, so
89
+ * "does this launcher apply the environment?" was answered four times, by
90
+ * accident, and a fifth source that forgot would have produced a session whose
91
+ * hooks fire and cannot be attributed — no error, events landing on the wrong
92
+ * instance.
93
+ *
94
+ * Callers that need the settings path as well should call
95
+ * {@link prepareAgentLaunch} and render the plan themselves; every caller in
96
+ * `src/lib/cli-tools/` wants only the line.
97
+ *
98
+ * @param context - The instance, its executable, and the worktree it runs in
99
+ * @returns `NAME='value' … command`, ready for `sendKeys`
100
+ */
101
+ function buildAgentLaunchCommandLine(context) {
102
+ return (0, sources_1.renderAgentLaunchCommand)(prepareAgentLaunch(context));
82
103
  }
@@ -601,7 +601,11 @@ async function startClaudeSession(options) {
601
601
  // Issue #1759: which config file gets written, and whether one is written
602
602
  // at all, belongs to the tool's `AgentEventSource` (S3/S4). Claude's
603
603
  // delegates to `buildClaudeLaunchCommand`, unchanged.
604
- const launchCommand = (0, agent_session_lifecycle_1.prepareAgentLaunch)({ worktreeId, cliToolId: sources_1.CLAUDE_CLI_TOOL_ID, instanceId }, claudePath).command;
604
+ const launchCommand = (0, agent_session_lifecycle_1.buildAgentLaunchCommandLine)({
605
+ target: { worktreeId, cliToolId: sources_1.CLAUDE_CLI_TOOL_ID, instanceId },
606
+ executablePath: claudePath,
607
+ worktreePath,
608
+ });
605
609
  // Start Claude CLI in interactive mode using dynamically resolved path
606
610
  await (0, tmux_1.sendKeys)(sessionName, launchCommand, true);
607
611
  // Wait for Claude to initialize with dynamic detection (OCP-001)
@@ -12,6 +12,7 @@ exports.mergeStructuredStatus = mergeStructuredStatus;
12
12
  exports.buildCurrentOutput = buildCurrentOutput;
13
13
  const db_1 = require("../../lib/db");
14
14
  const unclassified_frame_tracker_1 = require("../../lib/detection/unclassified-frame-tracker");
15
+ const upstream_faults_1 = require("../../lib/detection/upstream-faults");
15
16
  const models_1 = require("../../types/models");
16
17
  const logger_1 = require("../../lib/logger");
17
18
  const manager_1 = require("../../lib/cli-tools/manager");
@@ -19,6 +20,7 @@ const cli_session_1 = require("../../lib/session/cli-session");
19
20
  const status_detector_1 = require("../../lib/detection/status-detector");
20
21
  const auto_yes_manager_1 = require("../../lib/polling/auto-yes-manager");
21
22
  const auto_yes_suppression_state_1 = require("../../lib/polling/auto-yes-suppression-state");
23
+ const prompt_dedup_state_1 = require("../../lib/polling/prompt-dedup-state");
22
24
  const status_capture_config_1 = require("../../config/status-capture-config");
23
25
  const tmux_capture_cache_1 = require("../../lib/tmux/tmux-capture-cache");
24
26
  const agent_event_state_1 = require("../../lib/session/agent-event-state");
@@ -300,6 +302,18 @@ async function buildCurrentOutput(db, worktreeId, cliToolId, instanceId) {
300
302
  // the server's job, done here and in exactly one place.
301
303
  model: null,
302
304
  reasoningEffort: null,
305
+ // Issue #1695: the real tally, not zeros — and deliberately unlike the two
306
+ // fields above. A model latch describes a process, so on a dead session it
307
+ // would assert something false; a skip count describes what already
308
+ // happened, and `lastSkippedAt` dates it. Zeroing it here would erase the
309
+ // evidence at exactly the moment an operator goes looking for it — the
310
+ // session ended and the prompt they were waiting on was never saved.
311
+ promptDedup: (0, prompt_dedup_state_1.getPromptDedupSkips)(worktreeId, cliToolId, instanceId),
312
+ // Issue #1839: there is no frame to read, so there is nothing to report.
313
+ // Unlike `promptDedup` this is not a tally of what already happened — it
314
+ // is a claim about what is on screen right now, and a dead session has no
315
+ // screen.
316
+ upstreamFault: null,
303
317
  };
304
318
  }
305
319
  const sessionState = (0, db_1.getSessionState)(db, worktreeId, resolvedInstanceId);
@@ -455,6 +469,10 @@ async function buildCurrentOutput(db, worktreeId, cliToolId, instanceId) {
455
469
  });
456
470
  }
457
471
  const realtimeSnippet = lines.slice(-100).join('\n');
472
+ // Issue #1839: judged on exactly what is published as `realtimeSnippet`, not
473
+ // on `output`. The wider capture keeps a banner from an hour ago in scope, and
474
+ // a fault the operator cannot see in the payload next to it is unverifiable.
475
+ const upstreamFaultMatch = (0, upstream_faults_1.matchUpstreamFault)(realtimeSnippet);
458
476
  const autoYesState = (0, auto_yes_manager_1.getAutoYesState)(worktreeId, cliToolId, instanceId);
459
477
  // Issue #1785 + #1784: ONE resolution for both halves, not two readers.
460
478
  //
@@ -489,6 +507,9 @@ async function buildCurrentOutput(db, worktreeId, cliToolId, instanceId) {
489
507
  expiresAt: autoYesState?.enabled ? autoYesState.expiresAt : null,
490
508
  stopReason: autoYesState?.stopReason,
491
509
  lastSuppression: (0, auto_yes_suppression_state_1.getLastPolicySuppression)(worktreeId, cliToolId, instanceId),
510
+ // Issue #1694: undefined (so the key is absent from the JSON) unless a
511
+ // stop pattern actually fired — the state clears it on every other path.
512
+ stopMatchedText: autoYesState?.stopMatchedText,
492
513
  },
493
514
  isSelectionListActive,
494
515
  isPagerActive,
@@ -501,5 +522,18 @@ async function buildCurrentOutput(db, worktreeId, cliToolId, instanceId) {
501
522
  // docs on CurrentOutputPayload for why nothing is normalised on the way out.
502
523
  model,
503
524
  reasoningEffort: effort,
525
+ // Issue #1695: appended last on purpose — every field above is a published
526
+ // CLI contract and reordering them churns the diff for no reader's benefit.
527
+ promptDedup: (0, prompt_dedup_state_1.getPromptDedupSkips)(worktreeId, cliToolId, instanceId),
528
+ // Issue #1839: read from `realtimeSnippet`, the same rows an operator sees
529
+ // in `capture --json`, so what the field claims can be checked against what
530
+ // is printed next to it.
531
+ upstreamFault: upstreamFaultMatch
532
+ ? {
533
+ id: upstreamFaultMatch.fault.id,
534
+ matchedText: upstreamFaultMatch.matchedText,
535
+ at: Date.now(),
536
+ }
537
+ : null,
504
538
  };
505
539
  }
@@ -38,6 +38,8 @@ const os_1 = require("os");
38
38
  const path_1 = require("path");
39
39
  const env_1 = require("../../lib/env");
40
40
  const logger_1 = require("../../lib/logger");
41
+ const machine_lock_1 = require("./machine-lock");
42
+ const worktree_index_1 = require("./worktree-index");
41
43
  const logger = (0, logger_1.createLogger)('lib/verification/env-snapshot');
42
44
  /** Bumped only when a stored snapshot stops being readable by this module. */
43
45
  exports.ENV_SNAPSHOT_VERSION = 1;
@@ -299,12 +301,25 @@ async function probeTmuxSessions(deps) {
299
301
  exports.VOLATILE_ENTRY_PATTERNS = [/-wal$/, /-shm$/, /-journal$/, /^\.DS_Store$/];
300
302
  /** This module's own storage, which must not read as something a task created. */
301
303
  exports.ENV_SNAPSHOT_DIR_NAME = 'env-snapshots';
304
+ /**
305
+ * Coordination directories the verification runner itself creates under
306
+ * `~/.commandmate` (Issue #1771): the gate mutex locks and the worktree index
307
+ * registry. Neither is a side effect of the delegation — the first mutexed gate
308
+ * on a machine creates them — so counting them would report the runner's own
309
+ * bookkeeping as pollution the agent left behind.
310
+ *
311
+ * Scoped to the `~/.commandmate` probe rather than added to
312
+ * {@link isIgnoredEntry}: a `~/locks` directory in `$HOME` is not ours, and
313
+ * blanket-ignoring the name would put a hole in the probe that exists to catch
314
+ * exactly that.
315
+ */
316
+ const COMMANDMATE_RUNTIME_ENTRY_NAMES = [machine_lock_1.MACHINE_LOCK_DIR_NAME, worktree_index_1.WORKTREE_INDEX_DIR_NAME];
302
317
  function isIgnoredEntry(name) {
303
318
  if (name === exports.ENV_SNAPSHOT_DIR_NAME)
304
319
  return true;
305
320
  return exports.VOLATILE_ENTRY_PATTERNS.some((pattern) => pattern.test(name));
306
321
  }
307
- function probeDirectory(deps, path, { missingIsEmpty }) {
322
+ function probeDirectory(deps, path, { missingIsEmpty, alsoIgnored = [] }) {
308
323
  let names;
309
324
  try {
310
325
  names = deps.readDir(path);
@@ -318,7 +333,7 @@ function probeDirectory(deps, path, { missingIsEmpty }) {
318
333
  return unavailable(`could not list ${path}: ${error.message}`);
319
334
  }
320
335
  return ok(names
321
- .filter((name) => !isIgnoredEntry(name))
336
+ .filter((name) => !isIgnoredEntry(name) && !alsoIgnored.includes(name))
322
337
  .map((name) => ({ key: name, detail: null, anchor: null })));
323
338
  }
324
339
  /** Entries directly under `$HOME`. A worker leaving `~/.commandmate-uat-1726` shows up here. */
@@ -327,7 +342,10 @@ function probeHomeEntries(deps) {
327
342
  }
328
343
  /** Entries directly under `~/.commandmate` — the state directory #1722 polluted. */
329
344
  function probeCommandmateEntries(deps) {
330
- return probeDirectory(deps, (0, path_1.join)(deps.homeDir(), '.commandmate'), { missingIsEmpty: true });
345
+ return probeDirectory(deps, (0, path_1.join)(deps.homeDir(), '.commandmate'), {
346
+ missingIsEmpty: true,
347
+ alsoIgnored: COMMANDMATE_RUNTIME_ENTRY_NAMES,
348
+ });
331
349
  }
332
350
  /**
333
351
  * Take one snapshot of all four probes.
@@ -21,7 +21,7 @@
21
21
  * @module lib/verification/gate-runner
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.VerificationConflictError = exports.MAX_CONCURRENT_VERIFICATIONS = exports.CONFIG_GATE_ID = exports.ENV_CLEAN_GATE_ID = exports.SCOPE_GATE_ID = exports.WORK_EVIDENCE_GATE_ID = void 0;
24
+ exports.VerificationConflictError = exports.FLAKY_LOG_PREFIX = exports.MUTEX_WAIT_SKIP_REASON = exports.MUTEX_LOG_PREFIX = exports.WORKTREE_INDEX_ENV = exports.WORKTREE_ID_ENV = exports.MAX_CONCURRENT_VERIFICATIONS = exports.CONFIG_GATE_ID = exports.ENV_CLEAN_GATE_ID = exports.SCOPE_GATE_ID = exports.WORK_EVIDENCE_GATE_ID = void 0;
25
25
  exports.waitForVerification = waitForVerification;
26
26
  exports.startVerification = startVerification;
27
27
  const child_process_1 = require("child_process");
@@ -39,6 +39,8 @@ const contract_message_1 = require("../../lib/tasks/contract-message");
39
39
  const task_transition_service_1 = require("../../lib/tasks/task-transition-service");
40
40
  const env_clean_gate_1 = require("./env-clean-gate");
41
41
  const env_snapshot_1 = require("./env-snapshot");
42
+ const machine_lock_1 = require("./machine-lock");
43
+ const worktree_index_1 = require("./worktree-index");
42
44
  const scope_gate_1 = require("./scope-gate");
43
45
  const verify_config_1 = require("./verify-config");
44
46
  Object.defineProperty(exports, "ENV_CLEAN_GATE_ID", { enumerable: true, get: function () { return verify_config_1.ENV_CLEAN_GATE_ID; } });
@@ -74,6 +76,85 @@ const ENV_CLEAN_GATE_COMMAND = 'lsof -iTCP -sTCP:LISTEN / tmux list-sessions / $
74
76
  exports.MAX_CONCURRENT_VERIFICATIONS = 2;
75
77
  /** Grace period between SIGTERM and SIGKILL for a gate that overran. */
76
78
  const SIGKILL_GRACE_MS = 5000;
79
+ /**
80
+ * Per-worktree environment every command gate is given (Issue #1771).
81
+ *
82
+ * `CM_WORKTREE_INDEX` is what lets a repository write
83
+ * `E2E_PORT=$((60400+CM_WORKTREE_INDEX))` and stop N parallel worktrees from
84
+ * fighting over one port — removing the collision instead of serializing around
85
+ * it, which is the only option that keeps the parallelism. `CM_WORKTREE_ID` is
86
+ * for everything a number cannot name: a container name, a database prefix, a
87
+ * log directory.
88
+ */
89
+ exports.WORKTREE_ID_ENV = 'CM_WORKTREE_ID';
90
+ exports.WORKTREE_INDEX_ENV = 'CM_WORKTREE_INDEX';
91
+ /**
92
+ * Marker line carrying a mutexed gate's wait into `log_tail` (Issue #1771).
93
+ *
94
+ * `verification_gate_results` has no column for the wait, and it must not be
95
+ * folded into `duration_ms`: the duration is what the gate's own command took,
96
+ * and adding another run's queueing to it corrupts the number every timeout
97
+ * budget and every "this gate got slower" judgement is made from. The log tail
98
+ * is the same channel work-evidence's counts and the scope gate's evidence
99
+ * already travel on (`src/cli/commands/verify.ts:60-99`).
100
+ *
101
+ * Mirrored by `MUTEX_LOG_PREFIX` in `src/cli/utils/verify-runner.ts`, which
102
+ * cannot import this module — `tsconfig.cli.json` compiles `src/cli/**` alone.
103
+ */
104
+ exports.MUTEX_LOG_PREFIX = '[mutex]';
105
+ /**
106
+ * First line of a gate that never ran because the lock stayed held.
107
+ *
108
+ * Deliberately not `timeout`: the gate's command was never started, so there is
109
+ * nothing to say it ran long. Deliberately not `failed` either — the whole point
110
+ * of Issue #1771 is that a resource conflict and a broken change must stop
111
+ * reading the same. `skipped` aggregates the run to `error` (exit 99), which is
112
+ * "no verdict was reached", not "the work is bad".
113
+ */
114
+ exports.MUTEX_WAIT_SKIP_REASON = 'reason=mutex-wait';
115
+ /**
116
+ * Marker line carrying a retried gate's two runs into `log_tail` (Issue #1772).
117
+ *
118
+ * `verification_gate_results` stores one status, one exit code and one duration
119
+ * per gate, and a DB migration was out of scope for #1772, so the second run's
120
+ * numbers travel the same way #1771's wait does: a line-anchored, machine
121
+ * readable first line the CLI reads back.
122
+ *
123
+ * Written whenever a retry actually ran — for `outcome=fail` too, not only for
124
+ * `outcome=flaky`. A gate that failed twice is evidence *against* flakiness,
125
+ * and a flake advisor mining history needs both halves of that ratio; a marker
126
+ * present only on the flaky half would make every retried gate look flaky.
127
+ *
128
+ * Mirrored by `FLAKY_LOG_PREFIX` in `src/cli/utils/verify-runner.ts`, which
129
+ * cannot import this module — `tsconfig.cli.json` compiles `src/cli/**` alone.
130
+ */
131
+ exports.FLAKY_LOG_PREFIX = '[flaky]';
132
+ /**
133
+ * Runs a gate is allowed. Not derived from `retryOnFail` at the call site: the
134
+ * marker states the number so a reader of stored history never has to look up
135
+ * the verify.yaml that was in effect months ago.
136
+ */
137
+ const RETRY_TOTAL_RUNS = 2;
138
+ /**
139
+ * Separator between the two runs' outputs inside the composed log tail.
140
+ *
141
+ * Both are kept. Keeping only one makes the single question this feature exists
142
+ * to answer — *what differed between the two runs* — unanswerable from the
143
+ * record. `maxLogTailBytes` is applied per run, because it caps what one gate
144
+ * command may contribute and there were two commands.
145
+ */
146
+ function flakyRunHeader(index, outcome) {
147
+ return (`--- ${exports.FLAKY_LOG_PREFIX} run ${index}/${RETRY_TOTAL_RUNS}: ${outcome.status} ` +
148
+ `exit=${formatExit(outcome.exitCode)} duration=${formatSeconds(outcome.durationMs)} ---`);
149
+ }
150
+ /** One decimal second, the spelling both `waited=` and `duration=` use. */
151
+ function formatSeconds(ms) {
152
+ return `${(ms / 1000).toFixed(1)}s`;
153
+ }
154
+ /** `exit_code` is null for a gate killed by a signal; say so rather than lie. */
155
+ function formatExit(exitCode) {
156
+ return exitCode === null ? 'n/a' : String(exitCode);
157
+ }
77
158
  /** A run is already in flight for this worktree. Surfaces as HTTP 409. */
78
159
  class VerificationConflictError extends Error {
79
160
  worktreeId;
@@ -191,7 +272,7 @@ class TailBuffer {
191
272
  * process group — killing only the shell leaves the `npm`/`vitest` grandchildren
192
273
  * running and holding the worktree.
193
274
  */
194
- function runCommand(command, cwd, timeoutSec, maxLogTailBytes) {
275
+ function runCommand(command, cwd, timeoutSec, maxLogTailBytes, gateEnv) {
195
276
  return new Promise((resolve) => {
196
277
  const startedAt = Date.now();
197
278
  const tail = new TailBuffer(maxLogTailBytes);
@@ -203,8 +284,9 @@ function runCommand(command, cwd, timeoutSec, maxLogTailBytes) {
203
284
  cwd,
204
285
  // CI=true matches how the same commands run in the pipeline; vitest picks
205
286
  // a different fileParallelism otherwise, and a gate that passes here but
206
- // fails in CI is worse than no gate.
207
- env: { ...process.env, CI: 'true' },
287
+ // fails in CI is worse than no gate. `gateEnv` carries the per-worktree
288
+ // identity (#1771) and is code-supplied, never user input.
289
+ env: { ...process.env, CI: 'true', ...gateEnv },
208
290
  detached: true,
209
291
  stdio: ['ignore', 'pipe', 'pipe'],
210
292
  });
@@ -272,6 +354,158 @@ function runCommand(command, cwd, timeoutSec, maxLogTailBytes) {
272
354
  });
273
355
  });
274
356
  }
357
+ /**
358
+ * Prefix a gate's log with what it waited for, so the wait is on the record
359
+ * even when the command itself printed nothing.
360
+ *
361
+ * First line rather than last: the reader's question is "why did this gate take
362
+ * so long", and the answer belongs before the output it is about. The GATE line
363
+ * is built from the whole stored tail, so placement never decides whether
364
+ * `waited=` reaches the terminal.
365
+ */
366
+ function withMutexMarker(name, waitedMs, lockPath, logTail) {
367
+ const marker = `${exports.MUTEX_LOG_PREFIX} name=${name} waited=${formatSeconds(waitedMs)} lock=${lockPath}`;
368
+ return prefixLogTail(marker, logTail);
369
+ }
370
+ /** The log of a gate whose lock never came free. */
371
+ function mutexWaitTimeoutLog(gate, name, result) {
372
+ if (result.acquired)
373
+ throw new Error('mutexWaitTimeoutLog called for an acquired lock');
374
+ const held = result.heldBy ? ` (held by ${result.heldBy})` : '';
375
+ return [
376
+ `${exports.MUTEX_WAIT_SKIP_REASON} waited=${formatSeconds(result.waitedMs)}`,
377
+ `${exports.MUTEX_LOG_PREFIX} name=${name} lock=${machineLockPathFor(name)}${held}`,
378
+ `Gate '${gate.id}' declares mutex '${name}' and the machine-wide lock stayed held for its ` +
379
+ `whole ${gate.timeoutSec}s budget, so the command was never started. This is a resource ` +
380
+ 'conflict, not a verdict on the work: re-run once the other run finishes, or raise ' +
381
+ "the gate's timeoutSec.",
382
+ ].join('\n');
383
+ }
384
+ /** Resolved separately from the acquisition so the message can name it either way. */
385
+ function machineLockPathFor(name) {
386
+ return (0, machine_lock_1.machineLockPath)(name, (0, machine_lock_1.resolveMachineLockRoot)());
387
+ }
388
+ /**
389
+ * Run one attempt at a command gate, holding its `mutex` for the duration
390
+ * (Issue #1771).
391
+ *
392
+ * The wait happens *outside* the measured interval: `startedAt`/`durationMs`
393
+ * come from {@link runCommand} and describe the command alone, so a gate that
394
+ * queued for 42s and then ran for 190s records 190s and reports the 42s beside
395
+ * it. Merging them would inflate every duration by however busy the machine was.
396
+ *
397
+ * One *attempt*, not one gate, since #1772: a gate declaring `retryOnFail: 1`
398
+ * calls this twice. The lock is taken and released per attempt rather than held
399
+ * across both, so a retry never keeps a machine-wide resource out of another
400
+ * worktree's hands for a run that has already failed once.
401
+ */
402
+ async function runGateAttempt(gate, worktreePath, maxLogTailBytes, gateEnv) {
403
+ const mutex = gate.mutex;
404
+ if (!mutex) {
405
+ return runCommand(gate.command, worktreePath, gate.timeoutSec, maxLogTailBytes, gateEnv);
406
+ }
407
+ // The gate's own timeout is the wait budget: a gate allowed 600s of execution
408
+ // has already declared how long this run may spend on it, and a second knob
409
+ // would only let the two disagree.
410
+ const lock = await (0, machine_lock_1.acquireMachineLock)(mutex, { timeoutMs: gate.timeoutSec * 1000 });
411
+ if (!lock.acquired) {
412
+ const recordedAt = Date.now();
413
+ return {
414
+ status: 'skipped',
415
+ exitCode: null,
416
+ startedAt: recordedAt,
417
+ durationMs: 0,
418
+ logTail: mutexWaitTimeoutLog(gate, mutex, lock),
419
+ };
420
+ }
421
+ try {
422
+ const outcome = await runCommand(gate.command, worktreePath, gate.timeoutSec, maxLogTailBytes, gateEnv);
423
+ return {
424
+ ...outcome,
425
+ logTail: withMutexMarker(mutex, lock.waitedMs, lock.handle.path, outcome.logTail),
426
+ };
427
+ }
428
+ finally {
429
+ lock.handle.release();
430
+ }
431
+ }
432
+ /**
433
+ * Run one command gate, re-running it once when it fails and the gate asked for
434
+ * that (Issue #1772).
435
+ *
436
+ * A gate that fails on the machine's luck — a random UUID that happens to
437
+ * contain a forbidden substring, a port that was still in TIME_WAIT — is
438
+ * indistinguishable in the record from a gate that fails on the work, and
439
+ * "re-run the one red gate before believing it" has been tribal knowledge
440
+ * rather than something the runner does. This makes it a declaration.
441
+ *
442
+ * Only a `failed` attempt is retried. A `timeout` is not: the gate already
443
+ * spent its whole budget, and a second attempt would double the wall clock of
444
+ * exactly the gates whose budgets are largest. A `skipped` (the mutex never
445
+ * came free) and an `error` (the command could not be spawned) never started a
446
+ * command, so there is no verdict to seek a second opinion on.
447
+ */
448
+ async function runCommandGate(gate, worktreePath, maxLogTailBytes, gateEnv) {
449
+ const first = await runGateAttempt(gate, worktreePath, maxLogTailBytes, gateEnv);
450
+ if (gate.retryOnFail !== 1 || first.status !== 'failed')
451
+ return first;
452
+ const second = await runGateAttempt(gate, worktreePath, maxLogTailBytes, gateEnv);
453
+ if (second.status !== 'passed' && second.status !== 'failed') {
454
+ // The retry reached no verdict of its own, so there is nothing to compare
455
+ // the first run against. The first run's FAIL stands unchanged — reporting
456
+ // the retry's `skipped`/`timeout` instead would turn a gate that judged the
457
+ // work into a run with no verdict (exit 99), which is strictly weaker.
458
+ return {
459
+ ...first,
460
+ logTail: prefixLogTail(`${exports.FLAKY_LOG_PREFIX} retry reached no verdict (${second.status}); ` +
461
+ "the first run's result stands. Retry log:\n" +
462
+ (second.logTail ?? '(no output)'), first.logTail),
463
+ };
464
+ }
465
+ return composeRetriedGate(gate, first, second);
466
+ }
467
+ /**
468
+ * Fold two runs of one gate into the single row the schema has (Issue #1772).
469
+ *
470
+ * `status` and `exitCode` come from the run whose verdict is being reported, so
471
+ * the row never says `failed` beside `exit=0`: a FLAKY gate counted as a
472
+ * failure reports the failing run, one counted as a pass reports the passing
473
+ * one, and a gate that failed twice reports the later failure. `durationMs` is
474
+ * the sum, because both runs were this gate's own command executing — the same
475
+ * rule #1771 applied when it kept the lock *wait* out of the number.
476
+ *
477
+ * Both runs' numbers reach the terminal through the {@link FLAKY_LOG_PREFIX}
478
+ * marker, which the CLI reads to print `GATE <id> FLAKY (exit=1,0, 45.0s,44.0s)`.
479
+ */
480
+ function composeRetriedGate(gate, first, second) {
481
+ const outcome = second.status === 'passed' ? 'flaky' : 'fail';
482
+ const passes = outcome === 'flaky' && gate.flakyIsPass === true;
483
+ const reported = passes ? second : outcome === 'flaky' ? first : second;
484
+ const marker = `${exports.FLAKY_LOG_PREFIX} runs=${RETRY_TOTAL_RUNS} outcome=${outcome} ` +
485
+ `exit=${formatExit(first.exitCode)},${formatExit(second.exitCode)} ` +
486
+ `duration=${formatSeconds(first.durationMs)},${formatSeconds(second.durationMs)} ` +
487
+ `verdict=${passes ? 'pass' : 'fail'}`;
488
+ const body = [
489
+ flakyRunHeader(1, first),
490
+ first.logTail ?? '',
491
+ flakyRunHeader(2, second),
492
+ second.logTail ?? '',
493
+ ].join('\n');
494
+ return {
495
+ status: passes ? 'passed' : 'failed',
496
+ exitCode: reported.exitCode,
497
+ // The window opens where the first run opened and closes `durationMs`
498
+ // later, so #1625's `finished_at - started_at === duration_ms` still holds
499
+ // and the pair still describes execution rather than the database write.
500
+ startedAt: first.startedAt,
501
+ durationMs: first.durationMs + second.durationMs,
502
+ logTail: `${marker}\n${body}`,
503
+ };
504
+ }
505
+ /** Put a machine-readable line ahead of a gate's output, keeping both. */
506
+ function prefixLogTail(marker, logTail) {
507
+ return logTail ? `${marker}\n${logTail}` : marker;
508
+ }
275
509
  /** Run a git plumbing command. No shell: every argument here is code-supplied. */
276
510
  function runGit(args, cwd) {
277
511
  return new Promise((resolve) => {
@@ -483,12 +717,29 @@ function aggregateRunStatus(gateStatuses) {
483
717
  return 'error';
484
718
  return 'passed';
485
719
  }
486
- async function executeRun(db, runId, worktreePath, config, selection, baseRef, task, detachedContract, envClean) {
720
+ async function executeRun(db, runId, worktreeId, worktreePath, config, selection, baseRef, task, detachedContract, envClean) {
487
721
  const { maxLogTailBytes, skipInPrimaryCheckout } = config.options;
488
722
  const { runWorkEvidence, gates } = selection;
489
723
  // ORed with the repository-wide switch rather than replacing it (#1642): a
490
724
  // delegation may tighten the rule, never relax one the repository declared.
491
725
  const requireCommit = (0, contract_message_1.resolveRequireCommit)(task?.contract ?? null, config);
726
+ /**
727
+ * The per-worktree environment, resolved on first use (#1771).
728
+ *
729
+ * Lazy because claiming an index writes to `~/.commandmate/worktree-index`,
730
+ * and a run that reaches no command gate — work-evidence failed, the primary
731
+ * checkout guard skipped everything — must leave nothing behind. It also has
732
+ * to stay behind the env-clean gate below, which lists `~/.commandmate` and
733
+ * would otherwise diff against a directory this very run had just created.
734
+ */
735
+ let gateEnvCache = null;
736
+ const gateEnv = () => {
737
+ gateEnvCache ??= {
738
+ [exports.WORKTREE_ID_ENV]: worktreeId,
739
+ [exports.WORKTREE_INDEX_ENV]: String((0, worktree_index_1.resolveWorktreeIndex)(worktreeId)),
740
+ };
741
+ return gateEnvCache;
742
+ };
492
743
  /**
493
744
  * Close an open gate row with the interval its evaluator measured.
494
745
  *
@@ -595,7 +846,7 @@ async function executeRun(db, runId, worktreePath, config, selection, baseRef, t
595
846
  'options.skipInPrimaryCheckout is true.').status);
596
847
  continue;
597
848
  }
598
- const outcome = await runGate(gate.id, gate.command, gate.source, () => runCommand(gate.command, worktreePath, gate.timeoutSec, maxLogTailBytes));
849
+ const outcome = await runGate(gate.id, gate.command, gate.source, () => runCommandGate(gate, worktreePath, maxLogTailBytes, gateEnv()));
599
850
  statuses.push(outcome.status);
600
851
  }
601
852
  return aggregateRunStatus(statuses);
@@ -813,7 +1064,7 @@ async function startVerification(input) {
813
1064
  await acquireSlot();
814
1065
  let terminalStatus = 'error';
815
1066
  try {
816
- const status = await executeRun(db, run.id, input.worktreePath, resolvedConfig, resolvedSelection, baseRef, task, detachedContract, envCleanContext);
1067
+ const status = await executeRun(db, run.id, input.worktreeId, input.worktreePath, resolvedConfig, resolvedSelection, baseRef, task, detachedContract, envCleanContext);
817
1068
  terminalStatus = status;
818
1069
  (0, db_1.finishVerificationRun)(db, run.id, status);
819
1070
  return status;