@humwick/runner 0.130.4 → 0.130.6

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 (217) hide show
  1. package/LICENSE +661 -167
  2. package/agent-bin/humwick +2 -2
  3. package/agent-bin/humwick-browser +4 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
  5. package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
  6. package/package.json +8 -11
  7. package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
  8. package/plugins/claude/.claude-plugin/plugin.json +3 -3
  9. package/plugins/claude/claude-statusline-probe.cjs +26 -16
  10. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
  11. package/plugins/claude/hooks/gate-run-guard.sh +3 -3
  12. package/plugins/claude/hooks/gate-run-guard.ts +5 -5
  13. package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
  14. package/plugins/claude/hooks/kill-guard.sh +4 -4
  15. package/plugins/claude/hooks/kill-guard.ts +2 -2
  16. package/plugins/claude/hooks/permission-request.sh +1 -1
  17. package/plugins/claude/hooks/post-compact.sh +1 -1
  18. package/plugins/claude/hooks/post-tool-use.sh +3 -3
  19. package/plugins/claude/hooks/pre-compact.sh +2 -2
  20. package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
  21. package/plugins/claude/hooks/relay-status.sh +32 -32
  22. package/plugins/claude/hooks/session-end.sh +2 -2
  23. package/plugins/claude/hooks/session-start.sh +3 -3
  24. package/plugins/claude/hooks/shell-trust.ts +1 -1
  25. package/plugins/claude/hooks/stop-failure.sh +2 -2
  26. package/plugins/claude/hooks/stop.sh +6 -6
  27. package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
  28. package/plugins/claude/hooks/subagent-stop.sh +3 -3
  29. package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
  30. package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
  31. package/plugins/claude/monitors/monitors.json +2 -2
  32. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
  33. package/plugins/claude/monitors/relay-monitor.ts +5 -5
  34. package/plugins/claude/skills/label/SKILL.md +3 -3
  35. package/plugins/claude/skills/message/SKILL.md +5 -5
  36. package/plugins/claude/skills/react/SKILL.md +1 -1
  37. package/plugins/claude/skills/read-message/SKILL.md +4 -4
  38. package/plugins/claude/skills/reply/SKILL.md +5 -5
  39. package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
  40. package/plugins/claude/skills/slice-first/SKILL.md +5 -3
  41. package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
  42. package/plugins/claude/skills/status/SKILL.md +3 -3
  43. package/plugins/claude/skills/tags/SKILL.md +5 -5
  44. package/plugins/claude/skills/task/SKILL.md +10 -10
  45. package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
  47. package/plugins/codex/skills/guide/SKILL.md +3 -3
  48. package/plugins/codex/skills/label/SKILL.md +3 -3
  49. package/plugins/codex/skills/message/SKILL.md +5 -5
  50. package/plugins/codex/skills/react/SKILL.md +1 -1
  51. package/plugins/codex/skills/read-message/SKILL.md +4 -4
  52. package/plugins/codex/skills/reply/SKILL.md +5 -5
  53. package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
  54. package/plugins/codex/skills/slice-first/SKILL.md +5 -3
  55. package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
  56. package/plugins/codex/skills/status/SKILL.md +3 -3
  57. package/plugins/codex/skills/tags/SKILL.md +5 -5
  58. package/plugins/codex/skills/task/SKILL.md +11 -10
  59. package/src/adapter.ts +95 -92
  60. package/src/adapters/claude-api.ts +8 -11
  61. package/src/adapters/claude-context-command.ts +1 -1
  62. package/src/adapters/claude-credentials.ts +7 -3
  63. package/src/adapters/claude-delivery-lanes.ts +2 -2
  64. package/src/adapters/claude-delivery.ts +16 -16
  65. package/src/adapters/claude-effort.ts +2 -2
  66. package/src/adapters/claude-exit.ts +2 -2
  67. package/src/adapters/claude-headless-mirror.ts +18 -2
  68. package/src/adapters/claude-headless-refusal.ts +3 -3
  69. package/src/adapters/claude-headless.ts +11 -10
  70. package/src/adapters/claude-launch-exit.ts +1 -1
  71. package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
  72. package/src/adapters/claude-permission.ts +5 -5
  73. package/src/adapters/claude-prompt-gates.ts +9 -9
  74. package/src/adapters/claude-prompt-hook.ts +2 -2
  75. package/src/adapters/claude-session-capture.ts +6 -6
  76. package/src/adapters/claude-session-probe.ts +25 -25
  77. package/src/adapters/claude-shutdown.ts +11 -11
  78. package/src/adapters/claude-status-detectors.ts +31 -31
  79. package/src/adapters/claude-subagents.ts +7 -7
  80. package/src/adapters/claude-submit-confirmation.ts +6 -6
  81. package/src/adapters/claude-tmux.ts +43 -43
  82. package/src/adapters/claude-token-usage-capture.ts +5 -5
  83. package/src/adapters/claude-tool-policy.ts +7 -7
  84. package/src/adapters/claude-transcript.ts +24 -24
  85. package/src/adapters/claude.ts +100 -100
  86. package/src/adapters/codex-agent-message-capture.ts +26 -63
  87. package/src/adapters/codex-app-server.ts +1 -1
  88. package/src/adapters/codex-client.ts +9 -9
  89. package/src/adapters/codex-effective-config.ts +1 -1
  90. package/src/adapters/codex-launch-config.ts +5 -5
  91. package/src/adapters/codex-rollout-context.ts +2 -2
  92. package/src/adapters/codex-skill-discovery.ts +1 -1
  93. package/src/adapters/codex-token-usage-capture.ts +16 -16
  94. package/src/adapters/codex-tool-output.ts +4 -4
  95. package/src/adapters/codex-version.ts +3 -3
  96. package/src/adapters/codex.ts +64 -64
  97. package/src/adapters/opencode-api.ts +1 -1
  98. package/src/adapters/opencode-context.ts +20 -22
  99. package/src/adapters/opencode-model-registry.ts +7 -7
  100. package/src/adapters/opencode-provider.ts +8 -8
  101. package/src/adapters/opencode-provisioning.ts +30 -0
  102. package/src/adapters/opencode-server-retry.ts +1 -1
  103. package/src/adapters/opencode-soft-steer.ts +5 -5
  104. package/src/adapters/opencode-spawn.ts +18 -14
  105. package/src/adapters/opencode-token-usage-capture.ts +12 -12
  106. package/src/adapters/opencode-tool-events.ts +15 -15
  107. package/src/adapters/opencode.ts +232 -232
  108. package/src/adapters/project-shell.ts +11 -11
  109. package/src/adapters/service-access.ts +6 -6
  110. package/src/adapters/tmux-offload.ts +9 -9
  111. package/src/adapters/tool-summary.ts +5 -5
  112. package/src/agent-redact.ts +1 -1
  113. package/src/agent-runner/commands/dispatch.ts +10 -10
  114. package/src/agent-runner/commands/lifecycle.ts +2 -2
  115. package/src/agent-runner/commands/prompt.ts +19 -19
  116. package/src/agent-runner/credentials.ts +5 -5
  117. package/src/agent-runner/inbox.ts +1 -1
  118. package/src/agent-runner/lifecycle-milestones.ts +3 -3
  119. package/src/agent-runner/lifecycle-phase.ts +2 -2
  120. package/src/agent-runner/liveness-exchange.ts +4 -10
  121. package/src/agent-runner/registration.ts +8 -8
  122. package/src/agent-runner/runner-context.ts +9 -14
  123. package/src/agent-runner/session-mirror.ts +12 -12
  124. package/src/agent-runner/task-claims.ts +2 -2
  125. package/src/agent-runner/token-usage-coverage.ts +1 -1
  126. package/src/agent-runner/turn-report.ts +12 -12
  127. package/src/api-budget-accounting.ts +3 -3
  128. package/src/area-memory.ts +6 -6
  129. package/src/artifact-path-materialize.ts +1 -1
  130. package/src/async-guard.ts +5 -5
  131. package/src/attachment-cache.ts +17 -22
  132. package/src/background-script-reconcile.ts +6 -6
  133. package/src/bounded-log.ts +1 -1
  134. package/src/busy-reconciler.ts +22 -22
  135. package/src/claim-tracker.ts +3 -3
  136. package/src/claude-settings.ts +8 -8
  137. package/src/claude-system-prompt.ts +1 -1
  138. package/src/claude-workspace-trust.ts +1 -1
  139. package/src/config.ts +62 -61
  140. package/src/context-boundary.ts +5 -5
  141. package/src/context-command.ts +3 -3
  142. package/src/control-server.ts +32 -32
  143. package/src/delivery-result.ts +2 -2
  144. package/src/effort-command.ts +2 -2
  145. package/src/empty-turn.ts +22 -14
  146. package/src/free-port.ts +1 -1
  147. package/src/gate-run-config.ts +3 -3
  148. package/src/host-asset-plan.ts +24 -17
  149. package/src/host-claude-config-worker.ts +2 -2
  150. package/src/host-sources.ts +2 -2
  151. package/src/index.ts +16 -16
  152. package/src/launch-assembly.ts +94 -96
  153. package/src/liveness.ts +15 -15
  154. package/src/logger.ts +3 -3
  155. package/src/mcp-outbox.ts +3 -3
  156. package/src/mcp-preflight.ts +2 -2
  157. package/src/mcp-source-selection.ts +2 -2
  158. package/src/mcp-tool-deferral.ts +1 -1
  159. package/src/mcp-vault-env.ts +2 -2
  160. package/src/message-body-config/config.ts +4 -4
  161. package/src/mid-turn-delivery.ts +2 -2
  162. package/src/native-memory-lever.ts +8 -8
  163. package/src/outbox.ts +15 -15
  164. package/src/pending-permission-store.ts +1 -1
  165. package/src/profile-home.ts +19 -19
  166. package/src/profile-projection.ts +26 -24
  167. package/src/project-instructions.ts +5 -5
  168. package/src/provider-auth-warning.ts +17 -0
  169. package/src/provider-double.ts +1 -1
  170. package/src/provider-hold.ts +3 -3
  171. package/src/provider-home-marker.ts +1 -1
  172. package/src/provider-home-tombstone.ts +1 -1
  173. package/src/provider-input-queue.ts +2 -2
  174. package/src/provider-integration.ts +15 -13
  175. package/src/provider-setup.ts +15 -3
  176. package/src/providers.ts +1 -1
  177. package/src/provisioning.ts +37 -40
  178. package/src/question-decision.ts +2 -2
  179. package/src/quota.ts +0 -6
  180. package/src/rate-limit.ts +24 -13
  181. package/src/refused-prompt-store.ts +2 -2
  182. package/src/relay-injection-events.ts +2 -2
  183. package/src/relay-instructions.ts +5 -5
  184. package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
  185. package/src/relay-mcp-proxy.ts +57 -58
  186. package/src/relay-mcp.ts +55 -69
  187. package/src/reply-obligation-cache.ts +9 -9
  188. package/src/response-capture-report.ts +9 -9
  189. package/src/response-routing.ts +1 -1
  190. package/src/runner-core.ts +209 -222
  191. package/src/runner-exit.ts +1 -1
  192. package/src/runner-helpers.ts +10 -10
  193. package/src/runner-insights.ts +4 -2
  194. package/src/runner-mcp-proxy.ts +3 -3
  195. package/src/runner-spawn-config.ts +3 -3
  196. package/src/runtime-token-file.ts +1 -1
  197. package/src/runtime-token-remint.ts +1 -1
  198. package/src/service-access.ts +2 -2
  199. package/src/session-destroy.ts +1 -1
  200. package/src/session-insights.ts +1 -1
  201. package/src/session-scratch.ts +24 -37
  202. package/src/spawn-input.ts +6 -6
  203. package/src/startup-config-fetch.ts +4 -4
  204. package/src/subreaper.ts +3 -5
  205. package/src/test-run-dedup.ts +13 -13
  206. package/src/transcript-context.ts +2 -2
  207. package/src/transport-errors.ts +1 -1
  208. package/src/turn-opener.ts +1 -1
  209. package/src/turn-reconcile-event.ts +2 -2
  210. package/src/vault-proxy-hop.ts +2 -2
  211. package/src/web-tools.ts +2 -2
  212. package/agent-bin/agent-relay-browser +0 -10
  213. package/plugins/claude/skills/disconnect/SKILL.md +0 -16
  214. package/plugins/claude/skills/pair/SKILL.md +0 -30
  215. package/plugins/codex/skills/disconnect/SKILL.md +0 -15
  216. package/plugins/codex/skills/pair/SKILL.md +0 -29
  217. package/src/adapters/provider-quota-harvest.ts +0 -76
@@ -43,7 +43,7 @@ type MonitorlessTurnWatchDeps = {
43
43
  };
44
44
 
45
45
  export class ClaudeAdapter implements ProviderAdapter {
46
- // #2513 — provider-id-dynamic (the id-parameterized OpenCodeAdapter pattern): the
46
+ // provider-id-dynamic (the id-parameterized OpenCodeAdapter pattern): the
47
47
  // claude-b provider reuses this adapter whole, launching the same binary through the
48
48
  // same wiring under its own id/home/credential. Everything that identified "claude"
49
49
  // flows from this id; the launch assemblies/profile homes key off it the same way.
@@ -54,37 +54,37 @@ export class ClaudeAdapter implements ProviderAdapter {
54
54
  readonly permissionPromptHandler = claudePermissionPromptHandler;
55
55
  readonly compactSupportsInstructions = false;
56
56
  readonly compactReportsSubmission = true;
57
- // #1588 — Claude's clearContext is a fire-and-forget tmux `/clear` keystroke with no structured
57
+ // Claude's clearContext is a fire-and-forget tmux `/clear` keystroke with no structured
58
58
  // completion barrier, so the self-resume continuation MUST be delivered in-process coupled to the
59
59
  // clear command (after waitForClaudeInputReady settles it) instead of via a separate inject that
60
60
  // would race the still-processing async clear. Codex omits this (atomic awaited threadStart).
61
61
  readonly couplesClearInjectContinuation = true;
62
- // #2043 F1 — Claude's /compact and /clear are blind tmux keystrokes with no delivery handshake:
62
+ // F1 — Claude's /compact and /clear are blind tmux keystrokes with no delivery handshake:
63
63
  // submitted mid-turn they queue behind the live turn and dequeue as literal chat (never parsed as
64
64
  // slash commands), so the compaction silently no-ops. The runner gates the submit on the live
65
65
  // provider turn clearing so the keystrokes always land on an idle prompt. Codex omits this
66
66
  // (awaited app-server handshake — the await is the barrier).
67
67
  readonly awaitsIdleBeforeContextCommand = true;
68
- // #352: initial prompt is seeded as Claude's positional launch arg (buildSpawnArgs) — reliable,
68
+ // initial prompt is seeded as Claude's positional launch arg (buildSpawnArgs) — reliable,
69
69
  // no send-keys/onboarding race; tells the runner to skip the redundant post-launch delivery.
70
70
  readonly seedsInitialPromptAtLaunch = true;
71
71
  private readonly monitorlessTurnWatch: MonitorlessTurnWatchDeps;
72
72
  private statusCb: (status: ProviderStatusUpdate) => void = () => {};
73
- // #1651 — the pane watcher's ONLY outbound channel for anything it inferred from pane text.
73
+ // the pane watcher's ONLY outbound channel for anything it inferred from pane text.
74
74
  // Pane observations cannot end work or send input. A startup modal may carry only
75
75
  // a display gate, which the runner publishes as a badge and timeline event.
76
76
  private paneObservationCb: (observation: PaneObservation) => void = () => {};
77
77
  private tmuxWatcher?: Timer; private turnWatcher?: Timer;
78
78
  private turnWatchResume?: { process: ManagedProcess; sessionName: string; socketName?: string };
79
79
  private statusWatchersStopped = false;
80
- // #1838 review HIGH-1 — monotonic turn-watch generation. Captured when a monitorless
80
+ // review HIGH-1 — monotonic turn-watch generation. Captured when a monitorless
81
81
  // turn-watch is armed and bumped by stopTurnWatch(); the async setup and every in-flight
82
82
  // tick re-check it after each await and before any state mutation / status emit, so a
83
83
  // shutdown or a replacement invalidates a predecessor's setup AND its outstanding ticks. The
84
84
  // pane watcher fences on Timer identity (`this.tmuxWatcher !== watcher`) instead — the same
85
85
  // guarantee, checked after every await there.
86
86
  private turnWatchGen = 0;
87
- // #1838 review MED — the pane pid resolved OFF the loop once per spawn and cached, so the
87
+ // review MED — the pane pid resolved OFF the loop once per spawn and cached, so the
88
88
  // synchronous resolveBusyPid() (called inline from the runner core on every provider-turn
89
89
  // busy edge) can answer without a blocking tmux display-message spawn. undefined until the
90
90
  // async resolve lands (graceful: the runner falls back to its other liveness signals).
@@ -94,13 +94,13 @@ export class ClaudeAdapter implements ProviderAdapter {
94
94
  private startupReadyObserved = false;
95
95
  private startupBypassDialogActive = false;
96
96
  private startupOpenApproval = false;
97
- // #1621 — when relay LAST typed a turn-cancelling keystroke into the managed pane itself. A
97
+ // when relay LAST typed a turn-cancelling keystroke into the managed pane itself. A
98
98
  // probe whose last write predates this cannot be describing the state after the cancellation,
99
- // so its busy verdict stops being authoritative at once. #1651 removed the prompt gate's
99
+ // so its busy verdict stops being authoritative at once. Removed the prompt gate's
100
100
  // auto-Escape, so the sole remaining writer is the explicit `interrupt()` command — a
101
101
  // deliberate, operator/runner-initiated cancel, not something inferred from pane text.
102
102
  private paneInterruptAt?: number;
103
- // #1647 — provider epoch: bumped on every spawn, stamped into the ManagedProcess and
103
+ // provider epoch: bumped on every spawn, stamped into the ManagedProcess and
104
104
  // into any shutdown intent minted for it. A shutdown request that outlives a restart
105
105
  // therefore cannot authorize typing `/exit` into the replacement session.
106
106
  private providerEpoch = 0;
@@ -124,7 +124,7 @@ export class ClaudeAdapter implements ProviderAdapter {
124
124
  private readonly sessionCapture = new ClaudeSessionCapture();
125
125
  private tokenUsageCapture?: ClaudeTranscriptTokenUsageCapture;
126
126
  private tokenUsageReporter?: ProviderTokenUsageReporter;
127
- // #2491 — dedupes the empty-pass INFO log to once per turn: a turn's own capture pass (and
127
+ // dedupes the empty-pass INFO log to once per turn: a turn's own capture pass (and
128
128
  // any mid-turn permission pre-flush pass) can otherwise post 0 entries repeatedly for the
129
129
  // same turn while its JSONL flush is still in flight.
130
130
  private lastEmptyTokenUsageLogTurnKey?: string;
@@ -152,7 +152,7 @@ export class ClaudeAdapter implements ProviderAdapter {
152
152
  const transcriptPath = input.transcriptPath;
153
153
  const turnKey = ctx.turnId ?? input.promptId ?? transcriptPath;
154
154
  void this.tokenUsageCapture.capture(transcriptPath).then((postedCount) => {
155
- // #2491 — an empty pass previously logged nothing at all; a turn's own one-shot
155
+ // an empty pass previously logged nothing at all; a turn's own one-shot
156
156
  // capture racing the JSONL flush is exactly the condition this fix addresses, so it
157
157
  // must be visible at INFO without flipping log level (see also the reportFailure
158
158
  // wiring in runner-core.ts, moved off DEBUG for the same reason).
@@ -173,7 +173,7 @@ export class ClaudeAdapter implements ProviderAdapter {
173
173
  }
174
174
 
175
175
  async captureFinalTokenUsage(transcriptPath: string | undefined, ctx: ProviderSessionTurnContext): Promise<void> {
176
- // #2484 round 2 review (MED-1) — this adapter owns the transcript-presence bail now
176
+ // round 2 review (MED-1) — this adapter owns the transcript-presence bail now
177
177
  // (previously lived in the shared runner-core seam, which made it Claude-shaped for every
178
178
  // adapter): Claude's capture tails a JSONL transcript file, so it has nothing to settle
179
179
  // without a real path.
@@ -192,7 +192,7 @@ export class ClaudeAdapter implements ProviderAdapter {
192
192
  this.paneObservationCb = cb;
193
193
  }
194
194
 
195
- // §4.1 contract verdict (#746). Claude reference (#713): Claude is token-silent during
195
+ // §4.1 contract verdict. Claude reference: Claude is token-silent during
196
196
  // native compaction, but the runner publishes a `compacting` timeline transition while it
197
197
  // runs — so `timelineEvent.timestamp` (folded into lastProgressAt) keeps a compacting Opus
198
198
  // turn reading as progressing instead of wedged. Transport frames/acks + the server's
@@ -211,7 +211,7 @@ export class ClaudeAdapter implements ProviderAdapter {
211
211
  return hydrateClaudeHostConfig(config, this.provider);
212
212
  }
213
213
 
214
- async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
214
+ async spawn(config: RunnerSpawnConfig, preparedArgs?: SpawnArgs): Promise<ManagedProcess> {
215
215
  // A provider spawn is the adapter incarnation boundary on both tmux/headless and direct
216
216
  // interactive paths. Invalidate predecessor watchers before reopening the new incarnation,
217
217
  // but retain the stopped predecessor's resume descriptor if spawn itself throws: runner-core
@@ -223,7 +223,7 @@ export class ClaudeAdapter implements ProviderAdapter {
223
223
  this.statusWatchersStopped = false;
224
224
  try {
225
225
  await verifyClaudeSubagentVersion(config);
226
- const hydratedConfig = await this.hydrateSpawnConfig(config); const args = this.buildSpawnArgs(hydratedConfig, hydratedConfig.providerConfig as ProviderConfig);
226
+ const hydratedConfig = preparedArgs ? config : await this.hydrateSpawnConfig(config); const args = preparedArgs ?? this.buildSpawnArgs(hydratedConfig, hydratedConfig.providerConfig as ProviderConfig);
227
227
  if (hydratedConfig.headless) return this.spawnHeadless(hydratedConfig, args);
228
228
 
229
229
  const proc = Bun.spawn([args.command, ...args.args], {
@@ -261,25 +261,25 @@ export class ClaudeAdapter implements ProviderAdapter {
261
261
  const session = process.meta?.tmuxSession as string | undefined;
262
262
  const socket = process.meta?.tmuxSocket as string | undefined;
263
263
  if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for clear");
264
- // #1588 — snapshot the pane BEFORE submitting `/clear` so the clear-aware barrier can detect
264
+ // snapshot the pane BEFORE submitting `/clear` so the clear-aware barrier can detect
265
265
  // the loaded conversation body disappearing (positive clear-commit evidence), not just the
266
266
  // ever-present footer. Best-effort: a capture failure just yields no anchor → clearConfirmed
267
267
  // falls to false → backstop covers.
268
- // #2261 — VISIBLE screen only. The old `-S -80` capture snapshotted 80 lines of tmux
268
+ // VISIBLE screen only. The old `-S -80` capture snapshotted 80 lines of tmux
269
269
  // scrollback too, and `/clear` repaints only the visible screen (it never emits the `ESC[3J`
270
270
  // that would drop history), so the anchor picked out of scrollback was still there after the
271
- // clear committed and the barrier below could never confirm it (the #2261 false negative).
271
+ // clear committed and the barrier below could never confirm it.
272
272
  let preClearPane = "";
273
273
  let preClearCaptured = false;
274
274
  try { preClearPane = await CLAUDE_CLEAR_COMMIT_CAPTURE(session, socket); preClearCaptured = true; } catch {}
275
- // #2261 — the PRIMARY, machine-readable clear-commit signal: Claude mints a new session
275
+ // the PRIMARY, machine-readable clear-commit signal: Claude mints a new session
276
276
  // (new sessionId, new transcript file) the moment `/clear` commits. Snapshot the current one
277
277
  // first, pid-pinned, so the barrier can watch for the rotation. Best-effort: an unavailable
278
278
  // probe (pre-2.1.119, unresolvable config home) just leaves the barrier on its pane heuristic.
279
279
  const readSession = await bindClaudeClearSessionReader(process, session, socket);
280
280
  const preClearSession = readSession ? await readSession() : null;
281
281
  await submitClaudeContextCommand(session, "/clear", socket);
282
- // #1588 — `/clear` is async in the Claude TUI: submitTextToTmux returns as soon as the keys
282
+ // `/clear` is async in the Claude TUI: submitTextToTmux returns as soon as the keys
283
283
  // are typed, before the conversation is actually cleared. The self-resume continuation used to
284
284
  // race this still-processing clear (a separate command, ~8ms later, on a different transport)
285
285
  // and get swallowed — a silent mind-wipe. The OLD barrier (waitForClaudeInputReady) accepted
@@ -287,13 +287,13 @@ export class ClaudeAdapter implements ProviderAdapter {
287
287
  // could return before the clear committed. Use the CLEAR-AWARE barrier instead: it requires
288
288
  // positive evidence the clear committed (pane idle + input-ready + the pre-clear conversation
289
289
  // anchor gone). `clearConfirmed` is surfaced to the coupled deliver: only a genuine confirmation
290
- // permits continuationDelivered:true; a timeout/uncertain settle → false → the relay's #1023
290
+ // permits continuationDelivered:true; a timeout/uncertain settle → false → the relay's
291
291
  // backstop re-injects. Never throws (the clear already ran); uncertainty resolves to false.
292
292
  const clearConfirmed = await waitForClaudeClearCommitted(session, preClearPane, CLAUDE_TMUX_READY_TIMEOUT_MS, socket, { preClearSession, readSession, preClearCaptured });
293
293
  return { method: "tmux-inject", command: "/clear", clearConfirmed };
294
294
  }
295
295
 
296
- // #1512: Claude reports context only through its out-of-band status-line probe,
296
+ // Claude reports context only through its out-of-band status-line probe,
297
297
  // which freezes after an in-process compact/clear/native-resume. The transcript
298
298
  // (handed to the runner every turn by the Stop hook) carries the same per-turn
299
299
  // token usage, so the runner reads it here as a probe-independent fallback that
@@ -305,9 +305,9 @@ export class ClaudeAdapter implements ProviderAdapter {
305
305
  const socket = process.meta?.tmuxSocket as string | undefined;
306
306
  if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session to interrupt");
307
307
  // The same ESC the web terminal's aux key sends: cancels the in-flight turn
308
- // and drops Claude back to its input box without ending the session. #1621: mark the probe's
308
+ // and drops Claude back to its input box without ending the session. Mark the probe's
309
309
  // pre-cancel busy verdict non-authoritative so the runner's post-interrupt reconcile can see
310
- // the idle pane when the cancel writes no probe transition (a `shell` rewrite is idle, #2202).
310
+ // the idle pane when the cancel writes no probe transition (a `shell` rewrite is idle).
311
311
  this.paneInterruptAt = Date.now();
312
312
  const result = spawnSyncBounded(tmuxCommand(socket, "send-keys", "-t", session, "Escape"), {
313
313
  stdin: "ignore", stdout: "ignore", stderr: "pipe",
@@ -322,25 +322,25 @@ export class ClaudeAdapter implements ProviderAdapter {
322
322
  async probeActivity(process: ManagedProcess): Promise<"busy" | "idle" | "unknown"> {
323
323
  const session = process.meta?.tmuxSession as string | undefined;
324
324
  const socket = process.meta?.tmuxSocket as string | undefined;
325
- // #1838 row 14 — the busy reconciler drives this every 4s while busy; its has-session
325
+ // row 14 — the busy reconciler drives this every 4s while busy; its has-session
326
326
  // + probe read + pane fallback now run off the event loop. A non-`present` presence
327
327
  // (absent OR an unreadable wedged read) yields "unknown", the safe hold value — the
328
- // reconciler treats unknown as non-idle and never clears a live turn on it (#1838 HIGH-2).
328
+ // reconciler treats unknown as non-idle and never clears a live turn on it.
329
329
  if (!session || (await tmuxSessionPresence(session, socket)) !== "present") return "unknown";
330
- // #1980 U2 — the busy BACKSTOP consumer: when the intrinsic probe is unavailable, a bare
330
+ // U2 — the busy BACKSTOP consumer: when the intrinsic probe is unavailable, a bare
331
331
  // input-ready pane is NOT proof the turn ended (a long foreground tool renders the same box),
332
332
  // so it must read as `unknown` (hold), never `idle` (force-cancel). paneFallbackIsIdle:false
333
333
  // opts this path out of the legacy pane-scrape idle the turn-watch below still relies on.
334
334
  return this.activityFromProbeOrPane(process, session, socket, { paneFallbackIsIdle: false });
335
335
  }
336
336
 
337
- // #1138: a tmux-launched Claude session's ManagedProcess carries no direct OS pid
338
- // (process.pid is undefined — only the tmux session/socket), so the runner's #650
337
+ // a tmux-launched Claude session's ManagedProcess carries no direct OS pid
338
+ // (process.pid is undefined — only the tmux session/socket), so the runner's
339
339
  // pid-liveness fallback had nothing to check for an ordinary hook-driven turn. Resolve
340
340
  // the tmux pane's real pid so a long, quiet (no subagents, no compaction) busy turn can
341
341
  // prove it is still alive instead of being force-healed to idle by the runner's
342
342
  // stale-progress backstop purely because 10 minutes passed with no interim signal.
343
- // #1838 review MED — this is a SYNCHRONOUS adapter-interface method the runner core calls
343
+ // review MED — this is a SYNCHRONOUS adapter-interface method the runner core calls
344
344
  // inline on every provider-turn `busy` edge, so it must never spawn: the old
345
345
  // resolveClaudePid() shelled out to `tmux display-message` (up to a 10s block on a wedged
346
346
  // socket) on that hot edge. It now returns the direct OS pid when there is one, else the pane
@@ -353,19 +353,19 @@ export class ClaudeAdapter implements ProviderAdapter {
353
353
  return this.tmuxPanePid;
354
354
  }
355
355
 
356
- // Shared probe-primary / pane-fallback activity read (#766/#768), used by both the
356
+ // Shared probe-primary / pane-fallback activity read, used by both the
357
357
  // reconciler-facing probeActivity and the monitorless turn-watch. Primary: Claude
358
358
  // Code's intrinsic session probe (busy→busy — the idle-while-working gap the
359
359
  // pane-scrape misread; idle/shell→idle; waiting→unknown, a parked turn the backstop must
360
360
  // not force-clear). Legacy fallback: scrape the tmux pane (pre-2.1.119 / probe missing).
361
361
  private async activityFromProbeOrPane(process: ManagedProcess, session: string, socket?: string, opts: { paneFallbackIsIdle?: boolean } = {}): Promise<ClaudeProbeActivity> {
362
- // #1838 rows 14/15 — every subprocess/fs read on this path (pane-pid resolve, probe JSON
362
+ // rows 14/15 — every subprocess/fs read on this path (pane-pid resolve, probe JSON
363
363
  // read, /proc liveness read, transcript stat, and the pane-scrape fallback) runs off the
364
364
  // event loop now. The verdict logic itself (claudeProbeVerdict / claudeActivityFromProbeAndPane)
365
365
  // is pure and unchanged.
366
366
  const managed = await readManagedClaudeStatusAsync(claudeProbeArgsFor(process, session, socket));
367
- // #1621/#3026: a busy probe holds the turn unless relay's own interrupt postdates it; the
368
- // transcript mtime (#1642) is only read then, to tell whether that cancel took.
367
+ // a busy probe holds the turn unless relay's own interrupt postdates it; the
368
+ // transcript mtime is only read then, to tell whether that cancel took.
369
369
  const paneInterruptAt = this.paneInterruptAt;
370
370
  const lastActivityAt = paneInterruptAt === undefined ? undefined : await claudeTranscriptMtimeMsAsync(managed?.reading, managed?.configHome);
371
371
  const { activity: probe, busyStale } = claudeProbeVerdict(managed?.reading, { paneInterruptAt, lastActivityAt });
@@ -375,22 +375,22 @@ export class ClaudeAdapter implements ProviderAdapter {
375
375
  return claudeActivityFromProbeAndPane(probe, pane, { probeBusyStale: busyStale, paneFallbackIsIdle: opts.paneFallbackIsIdle ?? true });
376
376
  }
377
377
 
378
- // #769 P1-A: an independent pane-busy read consumed by the busy backstop — a probe-`idle`
378
+ // P1-A: an independent pane-busy read consumed by the busy backstop — a probe-`idle`
379
379
  // must never clear a live turn while the pane spinner still reads busy (a connection-retry/529
380
- // can write status:idle mid-turn without tripping the rate-limit pane detector — the #769 gap).
380
+ // can write status:idle mid-turn without tripping the rate-limit pane detector — the gap).
381
381
  // probeActivity stays probe-primary; this is a corroboration channel only.
382
382
  //
383
- // #1656: this used to answer `false` for all three of "captured, no spinner", "no tmux session"
383
+ // this used to answer `false` for all three of "captured, no spinner", "no tmux session"
384
384
  // and "capture threw", and the backstop read that single `false` as permission to clear a live
385
385
  // turn. A read that failed is a fact about the read, not about the provider — it is reported as
386
386
  // such now, and the backstop refuses to act on it.
387
387
  async probePaneBusy(process: ManagedProcess): Promise<PaneBusyReading> {
388
388
  const session = process.meta?.tmuxSession as string | undefined;
389
389
  const socket = process.meta?.tmuxSocket as string | undefined;
390
- // #1838 row 14 — the reconciler's independent pane-busy corroboration read, off the loop.
390
+ // row 14 — the reconciler's independent pane-busy corroboration read, off the loop.
391
391
  // A wedged-but-live server (presence "unreadable") reports "unreadable" — which the
392
392
  // reconciler already treats as no-evidence and HOLDS on — rather than the false "no-session"
393
- // a boolean has-session would have produced (#1838 HIGH-2).
393
+ // a boolean has-session would have produced.
394
394
  if (!session) return "no-session";
395
395
  const presence = await tmuxSessionPresence(session, socket);
396
396
  if (presence === "absent") return "no-session";
@@ -409,20 +409,20 @@ export class ClaudeAdapter implements ProviderAdapter {
409
409
  // connected" forever); we inject via tmux instead. For monitored profiles we keep the monitor
410
410
  // path (and its retry on transient startup races) to avoid double-delivering against a late monitor.
411
411
  if (process.meta?.monitorless === true || !monitor?.deliver) return this.deliverAsUserTurn(process, messages, ctx);
412
- // #3415 — human chat goes in as a tmux user turn; without a tmux pane everything stays on Monitor.
412
+ // human chat goes in as a tmux user turn; without a tmux pane everything stays on Monitor.
413
413
  const userTurn = process.meta?.tmuxSession ? (batch: Message[]) => this.deliverAsUserTurn(process, batch, ctx) : undefined;
414
414
  return deliverClaudeByLane(messages, { userTurn, narrowTurnOpener: ctx?.narrowTurnOpener, monitor: (batch, reportsOffer) => this.deliverViaMonitor(process, monitor.deliver!.bind(monitor), batch, reportsOffer ? ctx : { ...ctx, inputObserver: undefined }) });
415
415
  }
416
416
 
417
417
  private async deliverViaMonitor(process: ManagedProcess, deliver: (messages: Message[]) => Promise<number[]>, messages: Message[], ctx?: ProviderDeliverContext): Promise<DeliverResult> {
418
- // #1588 F2 edge 2 — for self-resume continuation deliveries, wait for the TUI to be input-ready
418
+ // F2 edge 2 — for self-resume continuation deliveries, wait for the TUI to be input-ready
419
419
  // first so the continuation never lands into a not-yet-settled post-clear context. The coupled
420
420
  // clearContext clear-aware barrier is the authoritative clear-commit gate; this belt-and-
421
421
  // suspenders settle also protects the SEPARATE relay-backstop inject. Best-effort: a settle
422
422
  // timeout must not fail the delivery — the backstop path already IS the recovery.
423
423
  const session = process.meta?.tmuxSession as string | undefined;
424
424
  const socket = process.meta?.tmuxSocket as string | undefined;
425
- // #1838 review — only skip the settle when the session is positively `absent`.
425
+ // review — only skip the settle when the session is positively `absent`.
426
426
  if (session && messagesAreSelfResumeContinuation(messages) && (await tmuxSessionPresence(session, socket)) !== "absent") {
427
427
  await waitForClaudeInputReady(session, CLAUDE_TMUX_READY_TIMEOUT_MS, socket).catch(() => {});
428
428
  }
@@ -434,7 +434,7 @@ export class ClaudeAdapter implements ProviderAdapter {
434
434
 
435
435
  // Type the batch into the interactive tmux session as a real user turn — the transport
436
436
  // deliverInitialPrompt uses. Keeps the agent on interactive Claude (the user's Max quota), never
437
- // `claude -p`/API; carries human chat (#3415) and every message of a monitorless profile.
437
+ // `claude -p`/API; carries human chat and every message of a monitorless profile.
438
438
  private async deliverAsUserTurn(process: ManagedProcess, messages: Message[], ctx?: ProviderDeliverContext): Promise<DeliverResult> {
439
439
  const monitorless = process.meta?.monitorless === true;
440
440
  const { session, socket } = await this.paneReadyForInput(process);
@@ -442,8 +442,8 @@ export class ClaudeAdapter implements ProviderAdapter {
442
442
  // scaffolding (reply reminder, claim-task instruction) — the agent has no way to /reply or /claim.
443
443
  const text = claudeProviderMessageText(messages, { relaySurface: !monitorless }), imagePaths = providerAttachmentLocalPaths(messages);
444
444
  ctx?.recordSubmittedPrompt?.(text, imagePaths);
445
- // #1838 Wave 2a — any tmux failure throws and deliver() propagates it unclassified; a tmux
446
- // exit 0 is not proof of submission (#1844), which the #2664 submit confirmation settles.
445
+ // Wave 2a — any tmux failure throws and deliver propagates it unclassified; a tmux
446
+ // exit 0 is not proof of submission, which the submit confirmation settles.
447
447
  await this.submitUserPromptToTmux(process, session, text, socket, imagePaths, ctx?.inputObserver);
448
448
  // Monitorless = no Stop/UserPromptSubmit hooks, so the runner gets no turn
449
449
  // lifecycle and never marks the claimed task done (the way Codex's app-server
@@ -465,22 +465,22 @@ export class ClaudeAdapter implements ProviderAdapter {
465
465
  if (this.statusWatchersStopped) return;
466
466
  this.stopTurnWatch();
467
467
  this.turnWatchResume = { process, sessionName, ...(socketName ? { socketName } : {}) };
468
- // #1838 review HIGH-1 — the generation this watch owns. stopTurnWatch() just bumped it; a
468
+ // review HIGH-1 — the generation this watch owns. stopTurnWatch just bumped it; a
469
469
  // later stop/replace bumps it again, so every await below re-checks `gen === this.turnWatchGen`
470
470
  // before touching shared state or emitting a status. A shutdown DURING the async setup, or an
471
471
  // in-flight predecessor tick finishing after a successor is armed, is thereby fenced out.
472
472
  const gen = this.turnWatchGen;
473
- // #769 P2-C: feed the resolved claude pid into the provider-turn liveness pids
474
- // so #650's pid-liveness reconcile arms for headless/monitorless turns too (the
473
+ // P2-C: feed the resolved claude pid into the provider-turn liveness pids
474
+ // so the pid-liveness reconcile arms for headless/monitorless turns too (the
475
475
  // process itself isn't tracked here — pid is undefined for tmux-managed claude).
476
476
  // A dead pid means the turn is definitively over, so this can't falsely clear.
477
- // #1838 row 15 — the per-turn-start pane-pid resolve + probe read run off the loop.
477
+ // row 15 — the per-turn-start pane-pid resolve + probe read run off the loop.
478
478
  const livenessPid = await this.monitorlessTurnWatch.resolvePanePid(process.pid, sessionName, socketName);
479
479
  // A numeric pid is not a process identity: an unrelated Claude can reuse it after this pane
480
480
  // exits. Pin /proc's start-time incarnation before accepting the asynchronous session probe.
481
481
  const paneIncarnation = livenessPid === undefined ? undefined : await this.monitorlessTurnWatch.processIncarnation(livenessPid);
482
482
  if (gen !== this.turnWatchGen) return;
483
- // #1235 — monitorless workers have no Stop/UserPromptSubmit hooks, so the transcript
483
+ // monitorless workers have no Stop/UserPromptSubmit hooks, so the transcript
484
484
  // reasoning tail is never hook-triggered and chat stays empty. Resolve the live transcript
485
485
  // path from the intrinsic session probe (sessionId + cwd) and hand it to the runner on the
486
486
  // turn edges so it can drive the SAME reasoning tail natively. undefined on an old/absent
@@ -518,7 +518,7 @@ export class ClaudeAdapter implements ProviderAdapter {
518
518
  let ticks = 0;
519
519
  let idleStreak = 0;
520
520
  let sawBusy = false;
521
- // #1838 row 15 (shares row 14's fix) — the tick's activity read is now off-loop, so the
521
+ // row 15 (shares row 14's fix) — the tick's activity read is now off-loop, so the
522
522
  // body is an async IIFE. `inFlight` keeps a slow (now off-loop) probe from stacking a
523
523
  // second concurrent tick on the 1.5s timer.
524
524
  let inFlight = false;
@@ -529,7 +529,7 @@ export class ClaudeAdapter implements ProviderAdapter {
529
529
  ticks += 1;
530
530
  const presence = await this.monitorlessTurnWatch.sessionPresence(sessionName, socketName);
531
531
  if (gen !== this.turnWatchGen) return; // superseded during the await — do not touch state
532
- // #1838 HIGH-2: only a positive `absent` (or the safety cap) ends the watch. An
532
+ // HIGH-2: only a positive `absent` (or the safety cap) ends the watch. An
533
533
  // `unreadable` read (wedged-but-live server) HOLDS — never abandon a live turn on it.
534
534
  if (presence === "unreadable") return;
535
535
  if (presence === "absent" || ticks > CLAUDE_TURN_MAX_TICKS) {
@@ -563,9 +563,9 @@ export class ClaudeAdapter implements ProviderAdapter {
563
563
  this.statusCb({ status: "busy", reason: "provider-turn", id: CLAUDE_TURN_WATCH_ID, ...(livenessPid ? { pids: [livenessPid] } : {}), transcriptPath });
564
564
  }
565
565
  }
566
- // #768: drive turn boundaries off the intrinsic probe (busy→busy, idle/shell→idle),
566
+ // drive turn boundaries off the intrinsic probe (busy→busy, idle/shell→idle),
567
567
  // falling back to the pane-scrape when the probe is unavailable. A foreground Bash
568
- // gate reads busy; `shell` is a finished turn with a background task alive (#2202).
568
+ // gate reads busy; `shell` is a finished turn with a background task alive.
569
569
  const activity = await this.monitorlessTurnWatch.activity(process, sessionName, socketName);
570
570
  if (gen !== this.turnWatchGen) return; // superseded during the probe await
571
571
  if (activity === "busy") { sawBusy = true; idleStreak = 0; return; }
@@ -576,7 +576,7 @@ export class ClaudeAdapter implements ProviderAdapter {
576
576
  const turnObserved = sawBusy || ticks >= CLAUDE_TURN_QUICK_GRACE_TICKS;
577
577
  if (confirmedIdle && turnObserved) {
578
578
  this.stopTurnWatch();
579
- // Carry the transcript path on the idle edge too (#1235) so the runner can capture the
579
+ // Carry the transcript path on the idle edge too so the runner can capture the
580
580
  // turn's final assistant response natively before it tears down the reasoning tail.
581
581
  this.statusCb({ status: "idle", reason: "provider-turn", id: CLAUDE_TURN_WATCH_ID, ...(transcriptPath ? { transcriptPath } : {}) });
582
582
  }
@@ -585,7 +585,7 @@ export class ClaudeAdapter implements ProviderAdapter {
585
585
  }
586
586
 
587
587
  private stopTurnWatch(): void {
588
- // #1838 review HIGH-1 — bump the generation FIRST so any predecessor setup still awaiting,
588
+ // review HIGH-1 — bump the generation FIRST so any predecessor setup still awaiting,
589
589
  // or an in-flight tick, sees itself invalidated after its next await and neither emits a
590
590
  // status nor tears down a successor. Unconditional (not gated on this.turnWatcher) so a
591
591
  // stop DURING setup (before the interval exists) still invalidates that setup.
@@ -602,7 +602,7 @@ export class ClaudeAdapter implements ProviderAdapter {
602
602
  await this.submitUserPromptToTmux(process, session, prompt, socket, options?.imagePaths, options?.inputObserver);
603
603
  }
604
604
 
605
- // #1838 review — off-loop presence: only a positive `absent` proves there is no session; on
605
+ // review — off-loop presence: only a positive `absent` proves there is no session; on
606
606
  // `unreadable` the deadline-bounded waitForClaudeInputReady sorts it out rather than failing spuriously.
607
607
  private async paneReadyForInput(process: ManagedProcess, readyTimeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS): Promise<{ session: string; socket?: string }> {
608
608
  const session = process.meta?.tmuxSession as string | undefined;
@@ -622,17 +622,17 @@ export class ClaudeAdapter implements ProviderAdapter {
622
622
 
623
623
  private async submitUserPromptToTmux(process: ManagedProcess, session: string, text: string, socket?: string, imagePaths: string[] = [], inputObserver?: ProviderDeliverContext["inputObserver"]): Promise<void> {
624
624
  const resolveTranscriptPath = () => resolveManagedClaudeTranscriptPathAsync(claudeProbeArgsFor(process, session, socket));
625
- // #2664 — the UserPromptSubmit hook confirms the submit; monitorless launches have no hooks.
625
+ // the UserPromptSubmit hook confirms the submit; monitorless launches have no hooks.
626
626
  await submitTextToTmux(session, text, socket, { confirmSubmit: true, transcriptPath: await resolveTranscriptPath().catch(() => undefined), resolveTranscriptPath, imagePaths, inputObserver, ...(process.meta?.monitorless === true ? {} : { promptHooks: this.sessionCapture.promptHooks }) });
627
627
  }
628
628
 
629
629
  buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
630
630
  // Create + bootstrap + auth-link the isolated config home (host base has none) so the
631
631
  // assembler's materialize step has somewhere to write the provisioned skills/plugins.
632
- // #2254 — this also seeds folder trust into whichever config file THIS launch reads,
632
+ // this also seeds folder trust into whichever config file THIS launch reads,
633
633
  // including the shared host config on a host-base/profile-less spawn.
634
634
  prepareClaudeProfileHome(config, this.provider);
635
- // #557 — ONE assembler owns the provisioning/vanilla/instruction launch surface
635
+ // ONE assembler owns the provisioning/vanilla/instruction launch surface
636
636
  // (--setting-sources, --plugin-dir, --mcp-config, status-line, --append-system-prompt);
637
637
  // profile-projection describes the SAME assembled result, so report == launch. The
638
638
  // adapter only adds the bits the assembler does not own: launch command resolution,
@@ -645,10 +645,10 @@ export class ClaudeAdapter implements ProviderAdapter {
645
645
  const providerArgs = claudeCallerAppends(config.headless
646
646
  ? claudeLaunchArgs(defaultArgs, config.providerArgs, config.approvalMode)
647
647
  : [...defaultArgs, ...config.providerArgs]).args;
648
- // #1096 — when the assembler merged the caller's own `--settings` into relay's (so the
648
+ // when the assembler merged the caller's own `--settings` into relay's (so the
649
649
  // baseline hooks/projectSettings/statusLine survive), drop the caller's now-redundant
650
650
  // `--settings` here so exactly ONE reaches the launch (relay's merged copy in assembled.args).
651
- // #2453 — the coding/review profiles' --disallowedTools trim unions in at this single
651
+ // the coding/review profiles' --disallowedTools trim unions in at this single
652
652
  // claude-family launch seam (helpers + per-profile config in ./claude-tool-policy).
653
653
  const args = claudeProfileDisallowedToolsArgs([
654
654
  ...assembled.args,
@@ -656,12 +656,12 @@ export class ClaudeAdapter implements ProviderAdapter {
656
656
  ], config);
657
657
  if (config.model) args.push("--model", config.model);
658
658
  const effortLaunch = claudeEffortLaunch(config.effort, config.env); args.push(...effortLaunch.args);
659
- // #352: seed the first turn at launch for ALL sessions; a transient headless oversized brief
659
+ // seed the first turn at launch for ALL sessions; a transient headless oversized brief
660
660
  // runs in Claude print mode and enters as stdin, not a racy tmux paste or system prompt.
661
- const oneShot = config.prompt ? seedClaudeInitialPrompt(args, sanitizeLaunchText(String(config.prompt), "initial prompt"), { headless: config.headless, lifecycle: config.lifecycle, scratchDir: relayEnv(config.env, "AGENT_RELAY_SCRATCH_DIR") }) : undefined;
661
+ const oneShot = config.prompt ? seedClaudeInitialPrompt(args, sanitizeLaunchText(String(config.prompt), "initial prompt"), { headless: config.headless, lifecycle: config.lifecycle, scratchDir: relayEnv(config.env, "HUMWICK_SCRATCH_DIR") }) : undefined;
662
662
  const env: Record<string, string> = writeChildEnv({}, {
663
663
  ...config.env,
664
- // #557 — the assembler owns the isolated config-home env (CLAUDE_CONFIG_DIR).
664
+ // the assembler owns the isolated config-home env (CLAUDE_CONFIG_DIR).
665
665
  ...assembled.env,
666
666
  // Bare `claude` runs its own CLAUDE_CONFIG_DIR and so never inherits the host's long-lived
667
667
  // CLAUDE_CODE_OAUTH_TOKEN; without it claude falls back to the (often stale) file-based
@@ -677,20 +677,20 @@ export class ClaudeAdapter implements ProviderAdapter {
677
677
  // carries a copy of the runner process env) wins, so an operator can still unset.
678
678
  CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP:
679
679
  config.env?.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP ?? process.env.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP ?? "1",
680
- // Same treatment (T3182 scope expansion): CLAUDE_CODE_SEND_FEEDBACK=0 is the
680
+ // Same treatment: CLAUDE_CODE_SEND_FEEDBACK=0 is the
681
681
  // documented removal of the SendFeedback tool — autonomous agents keep misfiling
682
682
  // humwick bugs to Anthropic with no human to approve the draft. The managed
683
683
  // settings.json additionally carries a permissions.deny belt (launch-assembly).
684
684
  CLAUDE_CODE_SEND_FEEDBACK: config.env?.CLAUDE_CODE_SEND_FEEDBACK ?? process.env.CLAUDE_CODE_SEND_FEEDBACK ?? "0",
685
- // #1546 — a mid-fleet CC auto-update re-rendered startup gates on every restart; managed
685
+ // a mid-fleet CC auto-update re-rendered startup gates on every restart; managed
686
686
  // launches keep the installed version until an operator bump.
687
687
  DISABLE_AUTOUPDATER: config.env?.DISABLE_AUTOUPDATER ?? process.env.DISABLE_AUTOUPDATER ?? "1",
688
- // #2615 — prompt suggestions ship conversation context to the provider to generate
688
+ // prompt suggestions ship conversation context to the provider to generate
689
689
  // follow-up suggestions no Relay-agent human ever sees (pure quota burn). The bundle
690
690
  // gate reads this env first; false outranks settings.json promptSuggestionEnabled.
691
691
  CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION:
692
692
  config.env?.CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION ?? process.env.CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION ?? "false",
693
- // #2616 — remote recap forks the FULL cached conversation through an extra model call at
693
+ // remote recap forks the FULL cached conversation through an extra model call at
694
694
  // turn end to produce a recap pushed to a claude.ai remote UI (`notifyMetadataChanged({
695
695
  // recap})`); no Relay agent ever renders it. The bundle gate reads
696
696
  // CLAUDE_CODE_ENABLE_REMOTE_RECAP first (env outranks settings/statsig): "false" pins it
@@ -698,7 +698,7 @@ export class ClaudeAdapter implements ProviderAdapter {
698
698
  // CCR surface; "true" still lets an operator force it on.
699
699
  CLAUDE_CODE_ENABLE_REMOTE_RECAP:
700
700
  config.env?.CLAUDE_CODE_ENABLE_REMOTE_RECAP ?? process.env.CLAUDE_CODE_ENABLE_REMOTE_RECAP ?? "false",
701
- // #2616 — CLI away summary fires the same recap generator when a pane regains terminal
701
+ // CLI away summary fires the same recap generator when a pane regains terminal
702
702
  // focus after ≥5min blur and injects the result into the live transcript. Relay panes
703
703
  // routinely sit unfocused far longer than that, so every firing is a model call whose
704
704
  // output no agent reads. The bundle gate reads CLAUDE_CODE_ENABLE_AWAY_SUMMARY before the
@@ -706,20 +706,20 @@ export class ClaudeAdapter implements ProviderAdapter {
706
706
  // "true" forces it on.
707
707
  CLAUDE_CODE_ENABLE_AWAY_SUMMARY:
708
708
  config.env?.CLAUDE_CODE_ENABLE_AWAY_SUMMARY ?? process.env.CLAUDE_CODE_ENABLE_AWAY_SUMMARY ?? "false",
709
- // #1222 — a spawned session may not find the humwick CLI in the host login
709
+ // a spawned session may not find the humwick CLI in the host login
710
710
  // shell's PATH, so Relay's own bins lead (layout-agnostic for npm and git-checkout
711
- // installs, #1305) while the inherited PATH keeps its own order behind them: leading
712
- // Homebrew's dirs (#1413) put Homebrew's node ahead of the operator's own on macOS,
713
- // and Claude's shell snapshot keeps this PATH, so no rc file restores it (#2705).
714
- // #3171 — the profile's harness trim (Artifact, Workflow, bundled skills, Explore/Plan) outranks inherited env.
711
+ // installs) while the inherited PATH keeps its own order behind them: leading
712
+ // Homebrew's dirs put Homebrew's node ahead of the operator's own on macOS,
713
+ // and Claude's shell snapshot keeps this PATH, so no rc file restores it.
714
+ // the profile's harness trim (Artifact, Workflow, bundled skills, Explore/Plan) outranks inherited env.
715
715
  ...claudeHarnessTrim(config).env,
716
716
  ...claudeSubagentEnv(config),
717
717
  PATH: launchPath(config.env?.PATH ?? process.env.PATH),
718
- AGENT_RELAY_PROVIDER: this.provider,
718
+ HUMWICK_PROVIDER: this.provider,
719
719
  ...effortLaunch.env,
720
720
  ...claudeProjectShellEnv(config.env),
721
- });
722
- // #2513 review HIGH-2 — credential isolation. config.env carries a copy of the whole
721
+ }, []);
722
+ // review HIGH-2 — credential isolation. config.env carries a copy of the whole
723
723
  // runner process env, so ambient credentials of the PRIMARY subscription reach every
724
724
  // launch — and each of these env vars OUTRANKS the file credential, so a
725
725
  // second-subscription claude-family provider would silently authenticate as the
@@ -730,7 +730,7 @@ export class ClaudeAdapter implements ProviderAdapter {
730
730
  // provider's own values can still ride its per-provider config env (threaded
731
731
  // explicitly here, since runner-core only merges providerConfig.env into host-globals
732
732
  // profiles). Primary subscription tokens survive; a host paid-key file does not. The key set is the SINGLE shared
733
- // constant from @humwick/providers (#2513 round-3 MF3) — the orchestrator's auth
733
+ // constant from @humwick/providers — the orchestrator's auth
734
734
  // preflight scrubs the same list, and a local re-listing is blocked by the
735
735
  // duplication ratchet.
736
736
  const envUnset: string[] = [...effortLaunch.unset, ...(claudeSubagentPolicy(config) ? ["CLAUDE_CODE_AUTO_COMPACT_WINDOW", "DISABLE_AUTO_COMPACT"] : [])];
@@ -762,20 +762,20 @@ export class ClaudeAdapter implements ProviderAdapter {
762
762
  this.startupOpenApproval = config.approvalMode === "open";
763
763
 
764
764
  // Create the gate FIFO *before* the session exists — the launcher script blocks
765
- // on reading it, so no output can be produced until we release it below (#1317).
765
+ // on reading it, so no output can be produced until we release it below.
766
766
  if (gateFifo) createLauncherGate(gateFifo);
767
767
 
768
768
  await createTmuxSessionWithEnv(sessionName, socketName, tmuxArgs, launcherScript, spawnArgs.env, config.providerConfig.env, gateFifo, this.provider);
769
769
 
770
770
  const exitCodeFile = spawnArgs.exitCodeFile ?? `${launcherScript}.exit-code`;
771
771
  this.watchTmuxSession(sessionName, socketName, exitCodeFile, printMode);
772
- // #1838 review MED — resolve the pane pid OFF the loop now that the session exists and cache
772
+ // review MED — resolve the pane pid OFF the loop now that the session exists and cache
773
773
  // it, so the synchronous resolveBusyPid() never has to shell out on a hot busy edge. Fenced
774
774
  // on providerEpoch so a resolve that lands after a restart can't stamp a stale pid.
775
775
  void resolveClaudePanePidAsync(undefined, sessionName, socketName)
776
776
  .then((pid) => { if (this.providerEpoch === providerEpoch) this.tmuxPanePid = pid; })
777
777
  .catch(() => {});
778
- const logFile = relayEnv(spawnArgs.env, "AGENT_RELAY_LOG_FILE");
778
+ const logFile = relayEnv(spawnArgs.env, "HUMWICK_LOG_FILE");
779
779
  if (logFile) {
780
780
  spawnSyncBounded(tmuxCommand(socketName, "pipe-pane", "-o", "-t", sessionName, `cat >> ${shellQuote(logFile)}`), {
781
781
  stdin: "ignore",
@@ -808,7 +808,7 @@ export class ClaudeAdapter implements ProviderAdapter {
808
808
 
809
809
  private watchTmuxSession(sessionName: string, socketName?: string, exitCodeFile?: string, oneShot = false): void {
810
810
  if (this.tmuxWatcher) clearInterval(this.tmuxWatcher);
811
- // #1838 row 12 — the highest-frequency blocking site in the runner (every 2s for the
811
+ // row 12 — the highest-frequency blocking site in the runner (every 2s for the
812
812
  // agent's whole lifetime). Its two tmux subprocesses (has-session + capture-pane) now run
813
813
  // OFF the event loop via the offload subprocess runner, so the tick is async. Guard against
814
814
  // overlapping ticks: if a capture is still in flight when the timer fires again — a wedged
@@ -822,16 +822,16 @@ export class ClaudeAdapter implements ProviderAdapter {
822
822
  tickInFlight = true;
823
823
  void fireAndForget("[runner] claude tmux watch tick", (async () => {
824
824
  const presence = await tmuxSessionPresence(sessionName, socketName);
825
- // #1838 HIGH-1: re-fence after every await — a respawn/shutdown that replaced this
825
+ // HIGH-1: re-fence after every await — a respawn/shutdown that replaced this
826
826
  // watcher mid-await must not let a predecessor tick emit or tear down its successor.
827
827
  if (this.tmuxWatcher !== watcher) return;
828
- // #1838 HIGH-2: only a positive `absent` is evidence of death. An `unreadable` read
828
+ // HIGH-2: only a positive `absent` is evidence of death. An `unreadable` read
829
829
  // (wedged-but-live server, deadline SIGKILL) HOLDS this tick — never emit `offline`.
830
830
  if (presence === "unreadable") return;
831
831
  if (presence === "absent") {
832
832
  // The whole tick (including the delayed second confirmation) is serialized by the
833
833
  // in-flight guard, so an off-loop confirm can no longer race a stacked tick. The
834
- // confirm itself only returns true when BOTH reads are `absent` (#1838 HIGH-2).
834
+ // confirm itself only returns true when BOTH reads are `absent`.
835
835
  const gone = await confirmTmuxSessionGone(sessionName, socketName).catch(() => false);
836
836
  if (this.tmuxWatcher !== watcher || !gone) return;
837
837
  const exitStatus = await claudeLaunchExitStatus(exitCodeFile, this.startupReadyObserved, oneShot, lastPane);
@@ -845,7 +845,7 @@ export class ClaudeAdapter implements ProviderAdapter {
845
845
  } catch {
846
846
  return;
847
847
  }
848
- // #1838 HIGH-1: fence again after the capture await, before any observation/status emit.
848
+ // HIGH-1: fence again after the capture await, before any observation/status emit.
849
849
  if (this.tmuxWatcher !== watcher) return;
850
850
  lastPane = pane;
851
851
  // A reused tmux pane can leave an old ready footer in scrollback. Until the
@@ -867,7 +867,7 @@ export class ClaudeAdapter implements ProviderAdapter {
867
867
  );
868
868
  this.modelUnavailableObservationState = modelUnavailable.state;
869
869
  if (modelUnavailable.observation) this.paneObservationCb(modelUnavailable.observation);
870
- // #769/#1651: connection-retry banner detection, advisory. BOTH edges are diagnostics.
870
+ // connection-retry banner detection, advisory. BOTH edges are diagnostics.
871
871
  // Appearance used to publish an idle + blocked hold, which the runner reads as the
872
872
  // provider turn ENDING — completing the task claims of a turn that was still running.
873
873
  // Disappearance used to publish a bare `idle`, an unconditional turn-end with no evidence
@@ -883,10 +883,10 @@ export class ClaudeAdapter implements ProviderAdapter {
883
883
  this.connectionRetryActive = false;
884
884
  this.paneObservationCb(claudeConnectionRetryClearedObservation(sessionName));
885
885
  }
886
- // #663: prompt-gate fallback. Known gates should be prevented before launch;
887
- // this catches missed/new blocking panes with the same #655 debounce + busy
886
+ // prompt-gate fallback. Known gates should be prevented before launch;
887
+ // this catches missed/new blocking panes with the same debounce + busy
888
888
  // guard used for rate-limit holds.
889
- // #1651: it NOTICES, it does not act. The five startup/modal gates are advisory and
889
+ // it NOTICES, it does not act. The five startup/modal gates are advisory and
890
890
  // return no status at all, so the only thing this branch can do for them is publish a
891
891
  // diagnostic. `status` is present solely for the usage-limit gate.
892
892
  const promptGate = evaluateClaudePromptGatePane(livePane, this.promptGatePaneState, {
@@ -910,7 +910,7 @@ export class ClaudeAdapter implements ProviderAdapter {
910
910
  private publishConfirmedTmuxExit(status: ProviderStatusUpdate): void {
911
911
  // The pane and turn pollers are independent. Invalidate the turn generation BEFORE publishing
912
912
  // the terminal edge so an in-flight incarnation/activity read cannot land a later idle and
913
- // resurrect the row with its shutting-down lifecycle pill still latched (#2505).
913
+ // resurrect the row with its shutting-down lifecycle pill still latched.
914
914
  this.stopStatusWatchers();
915
915
  this.statusCb(status);
916
916
  }
@@ -935,7 +935,7 @@ export class ClaudeAdapter implements ProviderAdapter {
935
935
  if (turnWatchResume) await this.beginMonitorlessTurnWatch(process, turnWatchResume.sessionName, turnWatchResume.socketName, false);
936
936
  }
937
937
 
938
- // #1647 — the shutdown sequence (observe → correlate → maybe /exit) lives in
938
+ // the shutdown sequence (observe → correlate → maybe /exit) lives in
939
939
  // ./claude-shutdown. Only the watcher teardown is the adapter's own business.
940
940
  async shutdownTmux(
941
941
  process: ManagedProcess,
@@ -949,7 +949,7 @@ export class ClaudeAdapter implements ProviderAdapter {
949
949
  }
950
950
  }
951
951
 
952
- // #1588 — a self-resume continuation delivery carries the resumeInjection/selfResume markers the
952
+ // a self-resume continuation delivery carries the resumeInjection/selfResume markers the
953
953
  // runner stamps in injectContext. Used to scope the monitor-path settle re-check to ONLY those
954
954
  // deliveries so ordinary message delivery is unchanged.
955
955
  function messagesAreSelfResumeContinuation(messages: Message[]): boolean {
@@ -959,11 +959,11 @@ function messagesAreSelfResumeContinuation(messages: Message[]): boolean {
959
959
  });
960
960
  }
961
961
 
962
- // #1647 — the exit command and its two writers now live in ./claude-exit, behind the
962
+ // the exit command and its two writers now live in ./claude-exit, behind the
963
963
  // one-shot grant. Re-exported so existing `./claude` consumers + tests keep their path.
964
964
  export { CLAUDE_EXIT_COMMAND } from "./claude-exit";
965
965
  export { claudeShutdownGraceMs, type ClaudeShutdownDeps } from "./claude-shutdown";
966
- // tmux transport plumbing moved to ./claude-tmux (#300 ratchet); re-exported so
966
+ // tmux transport plumbing moved to ./claude-tmux; re-exported so
967
967
  // existing `./claude` consumers + tests keep their import paths.
968
968
  export { CLAUDE_TMUX_SUBMIT_DELAY_MS, CLAUDE_TMUX_SUBMIT_KEY, CLAUDE_TMUX_READY_TIMEOUT_MS, tmuxSessionName, tmuxSocketName, assertTmuxSocketPathFits, tmuxEnvKeys, waitForClaudeClearCommitted } from "./claude-tmux";
969
969
  // Monitorless turn detection (Fix: isolated agents reach task "done" like Codex).
@@ -973,7 +973,7 @@ const CLAUDE_TURN_IDLE_CONFIRM_TICKS = 3; // ~4.5s of stable idle before declar
973
973
  const CLAUDE_TURN_QUICK_GRACE_TICKS = 8; // ~12s fallback when the spinner was never caught
974
974
  const CLAUDE_TURN_MAX_TICKS = 3600; // ~90min safety cap so the timer never leaks
975
975
 
976
- // #557 — sessionStatusLineSettingsArgs now lives in ../launch-assembly (the single
976
+ // sessionStatusLineSettingsArgs now lives in ../launch-assembly (the single
977
977
  // launch-arg home); re-exported so existing ./claude consumers + tests keep their path.
978
978
  export { sessionStatusLineSettingsArgs };
979
979
 
@@ -1034,7 +1034,7 @@ async function terminateSingleProcess(process: ManagedProcess, opts: { graceful:
1034
1034
  try {
1035
1035
  proc.kill(opts.graceful ? "SIGTERM" : "SIGKILL");
1036
1036
  } catch {
1037
- // Already gone (ESRCH) or unsignalable — re-observe rather than trust the throw. (#1746)
1037
+ // Already gone (ESRCH) or unsignalable — re-observe rather than trust the throw.
1038
1038
  return pid !== undefined && isPidAlive(pid) ? "failed-to-kill" : "killed";
1039
1039
  }
1040
1040
  const timeout = new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), opts.timeoutMs));
@@ -1045,6 +1045,6 @@ async function terminateSingleProcess(process: ManagedProcess, opts: { graceful:
1045
1045
  } catch {}
1046
1046
  await proc.exited.catch(() => {});
1047
1047
  }
1048
- // Confirm death rather than assume the kill took (#1746).
1048
+ // Confirm death rather than assume the kill took.
1049
1049
  return pid !== undefined && isPidAlive(pid) ? "failed-to-kill" : "killed";
1050
1050
  }