@humwick/runner 0.0.0-stage → 0.130.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 (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +42 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
@@ -0,0 +1,269 @@
1
+ import { isRecord } from "@humwick/sdk";
2
+ import type { PaneObservation, RunnerSpawnConfig } from "../adapter";
3
+ import { claudeTrustProjectKeys } from "../claude-workspace-trust";
4
+ import { claudeBypassPermissionDialog } from "./claude-status-detectors";
5
+
6
+ export interface ClaudeConfigPromptGatePrevention {
7
+ id: string;
8
+ description: string;
9
+ apply(seed: Record<string, unknown>, context: { host?: Record<string, unknown>; config: RunnerSpawnConfig }): void;
10
+ }
11
+
12
+ export interface ClaudeLaunchPromptGatePrevention {
13
+ id: string;
14
+ description: string;
15
+ apply(config: RunnerSpawnConfig): { strictMcpConfig?: boolean };
16
+ }
17
+
18
+ type PaneGateMatch = { message?: string };
19
+
20
+ /**
21
+ * #1651 — how much authority a pane match is allowed to carry.
22
+ *
23
+ * `advisory` is the only correct value for a gate detected purely from TUI geometry. It
24
+ * produces a {@link PaneObservation} and NOTHING else: no keystroke (the concept no longer
25
+ * exists in this file), no ProviderStatusUpdate, hence no status, no `clear`, no
26
+ * providerState. #1611/#1615/#1621/#1637/#1642 each tried to make one of these gates safe by
27
+ * tightening its string matcher; the matcher was never the problem — granting a string match
28
+ * the authority to mutate state was.
29
+ *
30
+ * The retired usage-limit pane gate (#1664) is deliberately not represented here: provider
31
+ * state comes from structured provider failures, never a pane scrape.
32
+ */
33
+
34
+ interface ClaudePanePromptGate {
35
+ id: string;
36
+ label: string;
37
+ policy: string;
38
+ minDetections: number;
39
+ match(text: string): PaneGateMatch | null;
40
+ }
41
+
42
+ export interface ClaudePromptGatePaneState {
43
+ currentGateId?: string;
44
+ consecutiveDetections: number;
45
+ handledGateId?: string;
46
+ }
47
+
48
+ export interface ClaudePromptGatePaneContext {
49
+ sessionName?: string;
50
+ busy?: boolean;
51
+ nowMs?: number;
52
+ sourceId?: string;
53
+ }
54
+
55
+ export interface ClaudePromptGatePaneResult {
56
+ /** #1651 — always present when a gate reached minDetections. Diagnostic only. */
57
+ observation?: PaneObservation;
58
+ /** Kept for callers which assert absence; `never` makes a status impossible to emit. */
59
+ status?: never;
60
+ state: ClaudePromptGatePaneState;
61
+ }
62
+
63
+ export const claudeConfigPromptGatePreventions: ClaudeConfigPromptGatePrevention[] = [
64
+ {
65
+ id: "first-run-onboarding",
66
+ description: "Pre-accept Claude Code's first-run onboarding gate.",
67
+ apply(seed, { host }) {
68
+ seed.hasCompletedOnboarding = true;
69
+ if (typeof host?.lastOnboardingVersion === "string") seed.lastOnboardingVersion = host.lastOnboardingVersion;
70
+ },
71
+ },
72
+ {
73
+ id: "theme-selection",
74
+ description: "Pre-seed a theme so the first-run theme picker does not render.",
75
+ apply(seed, { host }) {
76
+ seed.theme = typeof host?.theme === "string" ? host.theme : "dark";
77
+ },
78
+ },
79
+ {
80
+ id: "bypass-permissions-mode",
81
+ description: "Pre-accept Claude's open-approval bypass-permissions modal.",
82
+ apply(seed, { config }) {
83
+ if (config.approvalMode === "open") seed.bypassPermissionsModeAccepted = true;
84
+ },
85
+ },
86
+ {
87
+ id: "workspace-trust",
88
+ description: "Trust the managed spawn cwd and clear project onboarding.",
89
+ apply(seed, { config }) {
90
+ // #2254 — seed EVERY key CC may resolve this cwd under (lexical, realpath, git
91
+ // canonical root), not just the lexical worktree path. Host-base launches, which
92
+ // never reach this seed, get the same keys written into the shared config by
93
+ // ensureClaudeWorkspaceTrust on the launch path.
94
+ const keys = claudeTrustProjectKeys(config.cwd);
95
+ if (!keys.length) return;
96
+ const projects: Record<string, unknown> = { ...(isRecord(seed.projects) ? seed.projects : {}) };
97
+ for (const key of keys) {
98
+ projects[key] = { ...(isRecord(projects[key]) ? projects[key] : {}), hasTrustDialogAccepted: true, hasCompletedProjectOnboarding: true };
99
+ }
100
+ seed.projects = projects;
101
+ },
102
+ },
103
+ ];
104
+
105
+ export const claudeLaunchPromptGatePreventions: ClaudeLaunchPromptGatePrevention[] = [
106
+ {
107
+ id: "project-mcp-discovery",
108
+ description: "Use strict controlled MCP config so Claude never prompts for ambient project .mcp.json servers.",
109
+ apply() {
110
+ return { strictMcpConfig: true };
111
+ },
112
+ },
113
+ ];
114
+
115
+ // Every wording CC has shipped for the folder-trust modal: the pre-2.1 phrasing plus the
116
+ // 2.1.233 rewrite (header, question, and affirmative option). Stateless — no /g flag.
117
+ const WORKSPACE_TRUST_PROMPT = /do you trust (?:the files in )?(?:this folder|this project|this directory)|accessing workspace|quick safety check|yes,? ?I trust this folder/i;
118
+
119
+ const panePromptGates: ClaudePanePromptGate[] = [
120
+ // #1651 — the five gates below are ADVISORY. Each used to type a key into a live pane the
121
+ // instant `claudePaneIsBusy` happened to read false, then publish a fabricated
122
+ // idle + clear:["subagent"]. Two independent hazards, both realized:
123
+ // - the four Enter gates SUBMIT whatever text is already sitting in the composer;
124
+ // - project-mcp-discovery's Escape CANCELS a live turn.
125
+ // Neither needed the matcher to be wrong. `claudePaneIsBusy` is a last-ten-lines scrape
126
+ // whose only busy witness sits on the very last budgeted line, so one extra row of bottom
127
+ // chrome (a wrapped advisory, a two-line composer, one more trailing blank) flips it to
128
+ // false ON A LIVE TURN — measured on CC 2.1.220. A persistent ready-looking footer is not
129
+ // an authorization boundary, so none of these may hold one.
130
+ // Real startup modals are already prevented structurally, before launch, by
131
+ // claudeConfigPromptGatePreventions / claudeLaunchPromptGatePreventions above; these
132
+ // entries are the backstop that NOTICES a prevention missed, not a second actuator.
133
+ {
134
+ id: "workspace-trust",
135
+ label: "Workspace trust",
136
+ policy: "accept-trusted-workspace",
137
+ minDetections: 2,
138
+ // #2254 — CC 2.1.233 reworded this modal ("Accessing workspace" / "Quick safety check:
139
+ // Is this a project you created or one you trust?" / "Yes, I trust this folder"), so the
140
+ // old "do you trust this folder" matcher never fired and the wedge was silent. Both
141
+ // wordings are matched now. Still ADVISORY (see the block comment above): the actual
142
+ // prevention is the pre-launch trust seed, this only makes a missed seed observable.
143
+ match: (text) => WORKSPACE_TRUST_PROMPT.test(text) && /\b(?:yes|proceed|trust|continue)\b/i.test(text)
144
+ ? { message: lineMatching(text, WORKSPACE_TRUST_PROMPT) }
145
+ : null,
146
+ },
147
+ {
148
+ id: "bypass-permissions-mode",
149
+ label: "Bypass permissions mode",
150
+ policy: "accept-configured-open-approval",
151
+ minDetections: 2,
152
+ match: (text) => claudeBypassPermissionDialog(text)
153
+ ? { message: lineMatching(text, /bypass permissions mode/i) }
154
+ : null,
155
+ },
156
+ {
157
+ id: "project-mcp-discovery",
158
+ label: "Project MCP discovery",
159
+ policy: "reject-ambient-project-mcp",
160
+ minDetections: 2,
161
+ match: (text) => /\bnew MCP servers? found\b/i.test(text) && /\b(?:select|enable|servers?)\b/i.test(text)
162
+ ? { message: lineMatching(text, /new MCP servers? found/i) }
163
+ : null,
164
+ },
165
+ {
166
+ id: "theme-selection",
167
+ label: "Theme selection",
168
+ policy: "accept-default-theme",
169
+ minDetections: 2,
170
+ match: (text) => /choose (?:the )?(?:text style|theme).*Claude Code/i.test(text) && /\b(?:dark|light) mode\b/i.test(text)
171
+ ? { message: lineMatching(text, /choose/i) }
172
+ : null,
173
+ },
174
+ {
175
+ id: "first-run-onboarding",
176
+ label: "First-run onboarding",
177
+ policy: "accept-onboarding",
178
+ minDetections: 2,
179
+ match: (text) => /welcome to Claude Code/i.test(text) && /\b(?:let'?s get started|continue|get started)\b/i.test(text)
180
+ ? { message: lineMatching(text, /welcome to Claude Code/i) }
181
+ : null,
182
+ },
183
+ // #1514 (review finding 3) — the "Exit anyway / Move to background / Stay"
184
+ // confirmation is deliberately NOT a live pane-watcher gate. Auto-pressing Enter
185
+ // on a session that did NOT initiate shutdown is unsafe: it could submit stray
186
+ // input or dismiss an unrelated modal on an attached/live session. This dialog is
187
+ // only ever the right thing to answer under an explicit Relay shutdown, so it is
188
+ // handled solely by the runner's shutdown grace loop (confirmClaudeExitDialogUnderGrant
189
+ // in claude-exit.ts) — which runs only after this runner sent `/exit` under a one-shot
190
+ // grant. #1647 made that structural rather than advisory: this file cannot even import
191
+ // the exit writers (claude-exit.guard.test.ts fails the build if it ever does). The
192
+ // orphan safety net for a session already wedged mid-shutdown is the orchestrator's
193
+ // wedged-session reaper, not a keypress into a live pane.
194
+ ];
195
+
196
+ export function applyClaudeConfigPromptGatePreventions(
197
+ seed: Record<string, unknown>,
198
+ host: Record<string, unknown> | undefined,
199
+ config: RunnerSpawnConfig,
200
+ ): Record<string, unknown> {
201
+ const next = { ...seed };
202
+ for (const prevention of claudeConfigPromptGatePreventions) prevention.apply(next, { host, config });
203
+ return next;
204
+ }
205
+
206
+ export function claudeLaunchPromptGateSettings(config: RunnerSpawnConfig): { strictMcpConfig: boolean } {
207
+ return {
208
+ strictMcpConfig: claudeLaunchPromptGatePreventions.some((prevention) => prevention.apply(config).strictMcpConfig === true),
209
+ };
210
+ }
211
+
212
+ export function initialClaudePromptGatePaneState(): ClaudePromptGatePaneState {
213
+ return { consecutiveDetections: 0 };
214
+ }
215
+
216
+ export function evaluateClaudePromptGatePane(
217
+ text: string,
218
+ state: ClaudePromptGatePaneState,
219
+ context: ClaudePromptGatePaneContext = {},
220
+ ): ClaudePromptGatePaneResult {
221
+ const detected = firstDetectedPaneGate(text);
222
+ if (!detected) return { state: initialClaudePromptGatePaneState() };
223
+ if (context.busy) {
224
+ return { state: { ...state, currentGateId: undefined, consecutiveDetections: 0 } };
225
+ }
226
+ if (state.handledGateId === detected.entry.id) {
227
+ return { state };
228
+ }
229
+
230
+ const consecutiveDetections = state.currentGateId === detected.entry.id ? state.consecutiveDetections + 1 : 1;
231
+ const nextState: ClaudePromptGatePaneState = {
232
+ currentGateId: detected.entry.id,
233
+ consecutiveDetections,
234
+ ...(state.handledGateId ? { handledGateId: state.handledGateId } : {}),
235
+ };
236
+ if (consecutiveDetections < detected.entry.minDetections) return { state: nextState };
237
+
238
+ const observedAt = context.nowMs ?? Date.now();
239
+ const observation: PaneObservation = {
240
+ eventType: "claude.pane_gate_candidate",
241
+ source: "pane",
242
+ detectorId: detected.entry.id,
243
+ observedAt,
244
+ ...(context.sessionName ? { sessionName: context.sessionName } : {}),
245
+ ...(detected.match.message ? { message: detected.match.message } : {}),
246
+ detail: { label: detected.entry.label, policy: detected.entry.policy, paneSource: paneSource(context) },
247
+ };
248
+ return {
249
+ observation,
250
+ state: { ...nextState, handledGateId: detected.entry.id },
251
+ };
252
+ }
253
+
254
+ function paneSource(context: ClaudePromptGatePaneContext): string {
255
+ const source = context.sourceId ?? "claude-pane";
256
+ return context.sessionName ? `${source}:${context.sessionName}` : source;
257
+ }
258
+
259
+ function firstDetectedPaneGate(text: string): { entry: ClaudePanePromptGate; match: PaneGateMatch } | null {
260
+ for (const entry of panePromptGates) {
261
+ const match = entry.match(text);
262
+ if (match) return { entry, match };
263
+ }
264
+ return null;
265
+ }
266
+
267
+ function lineMatching(text: string, pattern: RegExp): string | undefined {
268
+ return text.split(/\r?\n/).map((line) => line.trim()).find((line) => pattern.test(line));
269
+ }
@@ -0,0 +1,145 @@
1
+ // #2664 — Claude's UserPromptSubmit hook fires only when Claude accepts an Enter: at queue time
2
+ // for a busy agent and never again on dequeue, and not at all for a swallowed Enter. A matching
3
+ // hook fired after our Enter is therefore the acceptance proof; the composer read is only a fallback.
4
+
5
+ // Transport changes observed on Claude Code 2.1.284-2.1.285, and nothing else: image paths it attached
6
+ // become leading [Image #N] chips, and a long paste arrives inside a <pasted_content> wrapper, even
7
+ // a single-line one (#3081). The whitespace around the wrapper and its body varies by release, so
8
+ // both are compared trimmed.
9
+ // Image pastes also collapse blank-line runs (including the runner's attachment separator).
10
+ const LEADING_IMAGE_CHIPS = /^(?:\[Image #\d+\])+/;
11
+ const IMAGE_FILE = /\.(?:png|jpe?g|gif|webp)$/i;
12
+ const PASTED_CONTENT = /^\s*<pasted_content id="([^"]*)">([\s\S]*)<\/pasted_content id="\1">\s*$/;
13
+
14
+ function sameText(hook: string, sent: string): boolean {
15
+ return hook === sent || (hook.trim() !== "" && hook.trim() === sent.trim());
16
+ }
17
+
18
+ interface Span { start: number; end: number }
19
+
20
+ /** Every whole-token occurrence of one of OUR image attachment paths, with the one space Claude removes before it. */
21
+ function attachmentSpans(sent: string, imagePaths: readonly string[]): Span[] {
22
+ const spans: Span[] = [];
23
+ for (const path of new Set(imagePaths.filter((path) => IMAGE_FILE.test(path)))) {
24
+ for (let at = sent.indexOf(path); at >= 0; at = sent.indexOf(path, at + 1)) {
25
+ const end = at + path.length;
26
+ if ((at > 0 && !/\s/.test(sent[at - 1]!)) || (end < sent.length && !/\s/.test(sent[end]!))) continue;
27
+ spans.push({ start: at > 0 && sent[at - 1] === " " ? at - 1 : at, end });
28
+ }
29
+ }
30
+ return spans.sort((a, b) => a.start - b.start).filter((span, i, all) => i === 0 || span.start >= all[i - 1]!.end);
31
+ }
32
+
33
+ /**
34
+ * Whether dropping exactly `k` of `spans` from `sent` leaves `body`, with blank-line runs
35
+ * collapsed on both sides and the same edge trimming as sameText. A DP over
36
+ * (span, dropped, matched) instead of enumerating subsets, so the work stays polynomial.
37
+ */
38
+ function matchesWithoutSpans(body: string, sent: string, spans: Span[], k: number): boolean {
39
+ const trimmed = body.trim() !== "";
40
+ const target = (trimmed ? body.trim() : body).replace(/\n{2,}/g, "\n");
41
+ const feed = (from: number, to: number, matched: number): number => {
42
+ for (let i = from; i < to; i++) {
43
+ const ch = sent[i]!;
44
+ if (trimmed && /\s/.test(ch) && (matched === 0 || matched === target.length)) continue;
45
+ // Collapse as we feed so removing an image-only line also joins newline runs correctly.
46
+ if (ch === "\n" && matched > 0 && target[matched - 1] === "\n") continue;
47
+ if (matched >= target.length || target[matched] !== ch) return -1;
48
+ matched++;
49
+ }
50
+ return matched;
51
+ };
52
+ const seen = new Set<string>();
53
+ const walk = (index: number, from: number, dropped: number, matched: number): boolean => {
54
+ const key = `${index},${dropped},${matched}`;
55
+ if (dropped > k || seen.has(key)) return false;
56
+ seen.add(key);
57
+ const span = spans[index];
58
+ const upto = feed(from, span ? span.start : sent.length, matched);
59
+ if (upto < 0) return false;
60
+ if (!span) return dropped === k && upto === target.length;
61
+ if (walk(index + 1, span.end, dropped + 1, upto)) return true;
62
+ const kept = feed(span.start, span.end, upto);
63
+ return kept >= 0 && walk(index + 1, span.end, dropped, kept);
64
+ };
65
+ return walk(0, 0, 0, 0);
66
+ }
67
+
68
+ /**
69
+ * Raw whole-prompt equality first, then only the evidenced transport changes; never a substring.
70
+ * Only `imagePaths` (the attachments this submit carried) can become chips; literal paths in the
71
+ * text are never candidates.
72
+ */
73
+ export function claudeHookPromptMatches(hookPrompt: string, submitted: string, imagePaths: readonly string[] = []): boolean {
74
+ const hook = hookPrompt.replace(/\r\n?/g, "\n");
75
+ const sent = submitted.replace(/\r\n?/g, "\n");
76
+ if (hook.trim() === "") return false;
77
+ if (sameText(hook, sent)) return true;
78
+ const chips = LEADING_IMAGE_CHIPS.exec(hook)?.[0] ?? "";
79
+ const images = chips.split("[").length - 1;
80
+ let body = hook.slice(chips.length);
81
+ const wrapped = PASTED_CONTENT.exec(body);
82
+ if (wrapped) body = wrapped[2]!;
83
+ if (images === 0) return wrapped !== null && sameText(body, sent);
84
+ const spans = attachmentSpans(sent, imagePaths);
85
+ return images <= spans.length && matchesWithoutSpans(body, sent, spans, images);
86
+ }
87
+
88
+ export interface ClaudePromptHookExpectation {
89
+ /** Hooks count only from now on; call right before the first Enter. Idempotent. */
90
+ arm(): void;
91
+ accepted(): boolean;
92
+ /** Resolves true as soon as a matching hook arrives, or with the current state after `ms`. */
93
+ wait(ms: number): Promise<boolean>;
94
+ release(): void;
95
+ }
96
+
97
+ interface PendingPrompt { text: string; imagePaths: readonly string[]; hit: boolean; armedAt?: number; wake?: () => void }
98
+
99
+ /**
100
+ * One expectation per session at a time (submits are serialized behind the submit fence). A hook
101
+ * counts only if stamped as fired at or after arm(): a delayed earlier hook cannot vouch for our
102
+ * Enter, and an unstamped one's arrival time says nothing about when Claude accepted.
103
+ */
104
+ export class ClaudePromptHookLedger {
105
+ private pending?: PendingPrompt;
106
+ private stampedHookSeen = false;
107
+
108
+ constructor(private readonly now: () => number = Date.now) {}
109
+
110
+ /** A stamped hook has reached the runner, so the hook path works in this Claude's environment. */
111
+ get hookPathVerified(): boolean { return this.stampedHookSeen; }
112
+
113
+ /** SessionStart's stamped check-in: evidence of a working hook path before the first prompt. */
114
+ hookCheckedIn(firedAt: number | undefined): void {
115
+ if (firedAt !== undefined) this.stampedHookSeen = true;
116
+ }
117
+
118
+ /** An Enter went unconfirmed by any hook: the path may have broken, so re-verify before trusting it. */
119
+ hookPathLost(): void { this.stampedHookSeen = false; }
120
+
121
+ expect(text: string, imagePaths: readonly string[] = []): ClaudePromptHookExpectation {
122
+ const entry: PendingPrompt = { text, imagePaths, hit: false };
123
+ this.pending = entry;
124
+ return {
125
+ arm: () => { entry.armedAt ??= this.now(); },
126
+ accepted: () => entry.hit,
127
+ wait: (ms) => entry.hit ? Promise.resolve(true) : new Promise<boolean>((resolve) => {
128
+ const timer = setTimeout(() => { entry.wake = undefined; resolve(entry.hit); }, ms);
129
+ entry.wake = () => { clearTimeout(timer); entry.wake = undefined; resolve(true); };
130
+ }),
131
+ release: () => { if (this.pending === entry) this.pending = undefined; },
132
+ };
133
+ }
134
+
135
+ /** `firedAt` is when the hook ran (epoch ms). Returns "unstamped" when the hook carried none. */
136
+ observe(prompt: string, firedAt: number | undefined): "unstamped" | void {
137
+ if (firedAt === undefined) return "unstamped";
138
+ this.stampedHookSeen = true;
139
+ const entry = this.pending;
140
+ if (!entry || entry.hit || entry.armedAt === undefined) return;
141
+ if (firedAt < entry.armedAt || !claudeHookPromptMatches(prompt, entry.text, entry.imagePaths)) return;
142
+ entry.hit = true;
143
+ entry.wake?.();
144
+ }
145
+ }