@humwick/runner 0.0.0-stage → 0.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +46 -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,373 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # POST a JSON body over stdin. A curl argv argument is capped by the kernel (~128 KiB), and a
4
+ # prompt or assistant message can exceed it; curl then never starts and the report is lost (#2664).
5
+ relay_post_json() {
6
+ local port="$1" path="$2" max_time="${3:-}"
7
+ curl -fsS ${max_time:+--max-time "$max_time"} -X POST "http://127.0.0.1:${port}${path}" \
8
+ -H 'Content-Type: application/json' --data-binary @-
9
+ }
10
+
11
+ relay_post_status() {
12
+ local status="${1:-}"
13
+ local reason="${2:-}"
14
+ local id="${3:-}"
15
+ local label="${4:-}"
16
+ local role="${5:-}"
17
+ local parent_id="${6:-}"
18
+ local clear_kind="${7:-}"
19
+ local timeline_status="${8:-}"
20
+ # Raw JSON object (e.g. '{"backgroundTasks":[...]}'), not a string field — the caller is
21
+ # responsible for producing valid JSON (see relay_background_task_metadata). Empty/unset
22
+ # omits the field entirely so ordinary status posts are unaffected.
23
+ local metadata_json="${9:-}"
24
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
25
+ [ -z "$port" ] && return 0
26
+ [ -z "$status" ] && return 0
27
+ local body="{\"status\":\"$(relay_json_escape "$status")\""
28
+ [ -n "${AGENT_RELAY_PROVIDER_SESSION_ID:-}" ] && body="${body},\"providerSessionId\":\"$(relay_json_escape "$AGENT_RELAY_PROVIDER_SESSION_ID")\""
29
+ [ -n "$reason" ] && body="${body},\"reason\":\"$(relay_json_escape "$reason")\""
30
+ [ -n "$clear_kind" ] && body="${body},\"clear\":[\"$(relay_json_escape "$clear_kind")\"]"
31
+ [ -n "$timeline_status" ] && body="${body},\"timeline\":{\"status\":\"$(relay_json_escape "$timeline_status")\"}"
32
+ [ -n "$id" ] && body="${body},\"id\":\"$(relay_json_escape "$id")\""
33
+ [ -n "$label" ] && body="${body},\"label\":\"$(relay_json_escape "$label")\""
34
+ [ -n "$role" ] && body="${body},\"role\":\"$(relay_json_escape "$role")\""
35
+ [ -n "$parent_id" ] && body="${body},\"parentId\":\"$(relay_json_escape "$parent_id")\""
36
+ [ -n "$metadata_json" ] && body="${body},\"metadata\":${metadata_json}"
37
+ body="${body}}"
38
+ printf '%s' "$body" | relay_post_json "$port" /status >/dev/null || true
39
+ }
40
+
41
+ relay_post_status_clearing_subagents() {
42
+ relay_post_status "$1" "${2:-}" "${3:-}" "${4:-}" "${5:-}" "${6:-}" subagent
43
+ }
44
+
45
+ # #502/#539: count user background shells still running at turn end.
46
+ # Claude's Stop payload carries a `background_tasks` array (CC v2.1.157+) listing
47
+ # shells launched with run_in_background that have not yet finished. It can also
48
+ # include the long-lived Humwick monitor; that monitor is notification
49
+ # plumbing, not agent work, so exclude it before driving `background-script`
50
+ # busy state. Older Claude builds omit the field → count 0 → graceful no-op.
51
+ relay_background_task_count() {
52
+ local input="${1:-}"
53
+ [ -z "$input" ] && { printf '0'; return 0; }
54
+ if command -v jq >/dev/null 2>&1; then
55
+ local n
56
+ n="$(printf '%s' "$input" | jq -r '
57
+ (.background_tasks // [])
58
+ | map(select(
59
+ (.name // "") != "agent-relay-runner-monitor"
60
+ and ((.command // "") | contains("relay-monitor.ts") | not)
61
+ and ((.command // "") | contains("agent-relay-runner-monitor") | not)
62
+ ))
63
+ | length
64
+ ' 2>/dev/null)"
65
+ case "$n" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$n" ;; esac
66
+ return 0
67
+ fi
68
+ # jq-less fallback: avoid a false busy for the common "only relay monitor" case.
69
+ local compact
70
+ compact="$(printf '%s' "$input" | tr -d '[:space:]')"
71
+ if printf '%s' "$compact" | grep -q '"background_tasks":\[{'; then
72
+ if printf '%s' "$compact" | grep -qE '("name":"agent-relay-runner-monitor"|relay-monitor\.ts)' \
73
+ && ! printf '%s' "$compact" | grep -q '},{'; then
74
+ printf '0'
75
+ return 0
76
+ fi
77
+ printf '1'
78
+ else
79
+ printf '0'
80
+ fi
81
+ }
82
+
83
+ # #2034 (#2031 L3): cap on the per-task detail list — bounds task count and per-command
84
+ # length so a pathological Stop payload (a runaway number of concurrent background shells,
85
+ # or an absurdly long command string) can never bloat the persisted work record.
86
+ # relay_background_task_count above (the busy/label signal) stays uncapped; only this
87
+ # richer detail list is bounded.
88
+ RELAY_BACKGROUND_TASK_LIST_MAX=20
89
+ RELAY_BACKGROUND_TASK_COMMAND_MAX=200
90
+
91
+ # #2034 (#2031 L3): shape Claude's Stop-payload background_tasks[] into the compact
92
+ # {id, command} detail list the runner keeps as the background-script work record's
93
+ # `metadata.backgroundTasks` — the per-task identity a watchdog can name directly instead
94
+ # of just a count. The runner (not this hook) stamps startedAt: each hook invocation is a
95
+ # fresh process with no memory of when a task first appeared, so age tracking has to live
96
+ # in the long-lived runner process, keyed by task id across turns.
97
+ # Same monitor-exclusion as relay_background_task_count above — must stay in sync.
98
+ # No jq -> "[]" (graceful degradation; the coarse count/busy path above is unaffected).
99
+ relay_background_task_list() {
100
+ local input="${1:-}"
101
+ [ -z "$input" ] && { printf '[]'; return 0; }
102
+ command -v jq >/dev/null 2>&1 || { printf '[]'; return 0; }
103
+ printf '%s' "$input" | jq -c \
104
+ --argjson max "$RELAY_BACKGROUND_TASK_LIST_MAX" \
105
+ --argjson cmdmax "$RELAY_BACKGROUND_TASK_COMMAND_MAX" '
106
+ (.background_tasks // [])
107
+ | map(select(
108
+ (.name // "") != "agent-relay-runner-monitor"
109
+ and ((.command // "") | contains("relay-monitor.ts") | not)
110
+ and ((.command // "") | contains("agent-relay-runner-monitor") | not)
111
+ ))
112
+ | map({
113
+ id: ((.id // "") | tostring),
114
+ command: ((.command // "") | if length > $cmdmax then .[0:$cmdmax] + "…" else . end)
115
+ })
116
+ | map(select(.id != ""))
117
+ | .[0:$max]
118
+ ' 2>/dev/null || printf '[]'
119
+ }
120
+
121
+ # Wrap a capped task list into the `metadata` object relay_post_status expects, or print
122
+ # nothing when there is nothing to report (no task list, empty list, or jq-less host) so an
123
+ # ordinary background-script status post is byte-for-byte what it was before #2034.
124
+ relay_background_task_metadata() {
125
+ local list_json="${1:-}"
126
+ case "$list_json" in
127
+ ''|'[]') printf '' ;;
128
+ *) printf '{"backgroundTasks":%s}' "$list_json" ;;
129
+ esac
130
+ }
131
+
132
+ # #502: end the provider turn (idle) clearing any stale subagent work — the
133
+ # critical path (#199) — then report the live background-script snapshot so an
134
+ # agent whose turn ended while a `run_in_background` shell keeps running stays
135
+ # busy instead of falsely reading idle. A single full-snapshot post per turn-end:
136
+ # >0 → busy(background-script); 0 → idle(background-script) which clears any prior.
137
+ relay_post_turn_end_status() {
138
+ local bg_count="${1:-0}"
139
+ # #2034: the capped {id, command} detail list (see relay_background_task_list) — only
140
+ # relevant on the busy branch; a bg_count of 0 always resolves to idle regardless.
141
+ local bg_tasks_json="${2:-[]}"
142
+ local bg_metadata
143
+ bg_metadata="$(relay_background_task_metadata "$bg_tasks_json")"
144
+ relay_post_status_clearing_subagents idle
145
+ case "$bg_count" in
146
+ ''|0|*[!0-9]*) relay_post_status idle background-script background-tasks ;;
147
+ 1) relay_post_status busy background-script background-tasks "1 background task" "" "" "" "" "$bg_metadata" ;;
148
+ *) relay_post_status busy background-script background-tasks "${bg_count} background tasks" "" "" "" "" "$bg_metadata" ;;
149
+ esac
150
+ }
151
+
152
+ # Report a provider usage/rate-limit hold to the runner (#286). The runner turns
153
+ # this into a `providerState: blocked` (reason rate_limit), posts a chat notice,
154
+ # and auto-resumes the agent once the window resets. reset_time (unix seconds,
155
+ # only "if available" per CC StopFailure docs) drives the resume; the runner falls
156
+ # back to a poll window when it is absent. Fire-and-forget like the other reports.
157
+ relay_post_rate_limit() {
158
+ local error_type="${1:-}"
159
+ local reset_time="${2:-}"
160
+ local error_message="${3:-}"
161
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
162
+ [ -z "$port" ] && return 0
163
+ local body="{\"errorType\":\"$(relay_json_escape "$error_type")\""
164
+ case "$reset_time" in ''|*[!0-9]*) ;; *) body="${body},\"resetTime\":${reset_time}" ;; esac
165
+ [ -n "$error_message" ] && body="${body},\"errorMessage\":\"$(relay_json_escape "$error_message")\""
166
+ body="${body}}"
167
+ printf '%s' "$body" | relay_post_json "$port" /rate-limit >/dev/null 2>&1 || true
168
+ }
169
+
170
+ relay_post_timeline_status() {
171
+ relay_post_status "$1" "${2:-provider-turn}" "" "" "" "" "${3:-}" "$4"
172
+ }
173
+
174
+ relay_post_session_turn() {
175
+ # Phase 1 live-session lane: hand the transcript path to the runner so it can
176
+ # capture the assistant turn for the dashboard chat. Synchronous on purpose —
177
+ # the captured turn clears the reply obligation before the stop decision check
178
+ # below, so the agent is not nagged to /reply (and does not re-emit).
179
+ local transcript_path="${1:-}"
180
+ local last_assistant_message="${2:-}"
181
+ local prompt_id="${3:-}"
182
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
183
+ [ -z "$port" ] && return 0
184
+ # transcript_path is the primary capture source, but the Stop payload can omit it
185
+ # (known CC edge case). last_assistant_message is the fallback for exactly that —
186
+ # POST with whichever we have; only bail when both are missing.
187
+ [ -z "$transcript_path" ] && [ -z "$last_assistant_message" ] && return 0
188
+ local body="{"
189
+ [ -n "$transcript_path" ] && body="${body}\"transcriptPath\":\"$(relay_json_escape "$transcript_path")\""
190
+ if [ -n "$last_assistant_message" ]; then
191
+ [ "$body" != "{" ] && body="${body},"
192
+ body="${body}\"lastAssistantMessage\":${last_assistant_message}"
193
+ fi
194
+ # prompt_id (#1086): Claude's native turn identifier — the stable id the runner
195
+ # uses instead of a minted random UUID, and the deterministic key it slices the
196
+ # transcript by for full-mode enrichment.
197
+ if [ -n "$prompt_id" ]; then
198
+ [ "$body" != "{" ] && body="${body},"
199
+ body="${body}\"promptId\":\"$(relay_json_escape "$prompt_id")\""
200
+ fi
201
+ body="${body}}"
202
+ printf '%s' "$body" | relay_post_json "$port" /session-turn >/dev/null 2>&1 || true
203
+ }
204
+
205
+ # Forward a hook's raw stdin JSON unchanged, stamped with when the hook ran (#2664). No jq: a
206
+ # missing jq must not silence the prompt hook the runner confirms submits by.
207
+ relay_forward_hook_payload() {
208
+ local path="${1:-}" payload="${2:-}" fired_at="${3:-}"
209
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
210
+ [ -z "$port" ] && return 0
211
+ case "$fired_at" in ''|*[!0-9]*) fired_at="" ;; esac
212
+ printf '%s' "$payload" | relay_post_json "$port" "${path}${fired_at:+?firedAt=${fired_at}}" 3 >/dev/null 2>&1 || true
213
+ }
214
+
215
+ relay_post_user_prompt() {
216
+ # Mirror a prompt the human typed directly into this Claude session (web
217
+ # terminal / TUI) into the dashboard chat, and hand over the transcript path so
218
+ # the runner can tail reasoning/tool steps for the turn. The runner dedups
219
+ # prompts it injected itself, and uses the stamp as submit evidence.
220
+ # Fire-and-forget; never blocks or fails the turn.
221
+ [ -z "${1:-}" ] && return 0
222
+ relay_forward_hook_payload /user-prompt "$1" "${2:-}"
223
+ }
224
+
225
+ relay_post_session_boundary() {
226
+ # Pre-destroy seam (#183): tell the runner a context reset or termination is imminent
227
+ # (compact/clear/logout/…) so it can run end-of-session work — the #184 context-gathering
228
+ # ratio capture — from the full transcript before the invasive op. Fire-and-forget; the
229
+ # transcript path is optional (the runner falls back to the last path it saw).
230
+ local transcript_path="${1:-}"
231
+ local reason="${2:-}"
232
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
233
+ [ -z "$port" ] && return 0
234
+ local body="{"
235
+ [ -n "$transcript_path" ] && body="${body}\"transcriptPath\":\"$(relay_json_escape "$transcript_path")\""
236
+ if [ -n "$reason" ]; then
237
+ [ "$body" != "{" ] && body="${body},"
238
+ body="${body}\"reason\":\"$(relay_json_escape "$reason")\""
239
+ fi
240
+ body="${body}}"
241
+ printf '%s' "$body" | relay_post_json "$port" /session-boundary 3 >/dev/null 2>&1 || true
242
+ }
243
+
244
+ # --- Hook FATAL surfacing (#198) -------------------------------------------
245
+ # A hook that dies unexpectedly must never be silent. relay_install_hook_guard
246
+ # arms an ERR trap that reports the failure FATAL to the runner control port,
247
+ # which logs it to the dashboard-surfaced per-agent log. Best-effort and bounded
248
+ # (--max-time 2) so the report itself can never blow the hook's timeout budget.
249
+ relay_hook_fatal_report() {
250
+ local hook="${1:-unknown}" detail="${2:-}"
251
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
252
+ [ -z "$port" ] && return 0
253
+ local body="{\"hook\":\"$(relay_json_escape "$hook")\",\"error\":\"$(relay_json_escape "$detail")\"}"
254
+ printf '%s' "$body" | relay_post_json "$port" /hook-fatal 2 >/dev/null 2>&1 || true
255
+ }
256
+
257
+ relay_install_hook_guard() {
258
+ RELAY_HOOK_NAME="${1:-unknown}"
259
+ # The pipe adapter owns native turn boundaries and final-result reporting.
260
+ # Permission and safety hooks still run; TUI lifecycle hooks must not compete.
261
+ if [ "${AGENT_RELAY_CLAUDE_TRANSPORT:-}" = "stream-json" ]; then
262
+ case "$RELAY_HOOK_NAME" in
263
+ user-prompt-submit|stop|stop-failure|session-end|pre-compact|post-compact|subagent-start|subagent-stop) exit 0 ;;
264
+ esac
265
+ fi
266
+ # Fires on any unhandled failure under `set -e`/`set -u`/pipefail in the hook's
267
+ # main body, just before the shell exits. Reports, then lets the exit proceed.
268
+ # (ERR is not inherited into functions without `set -E`; this covers the top-level
269
+ # flow, which is where a silent death actually wedges a turn.)
270
+ trap 'relay_hook_err_rc=$?; relay_hook_fatal_report "${RELAY_HOOK_NAME:-unknown}" "exit ${relay_hook_err_rc}: ${BASH_COMMAND}"' ERR
271
+ }
272
+
273
+ relay_pending_reply_stop_decision() {
274
+ local port="${AGENT_RELAY_RUNNER_PORT:-}"
275
+ [ -z "$port" ] && return 0
276
+ local response
277
+ # --max-time guards the Claude Stop hook's 5s budget: a slow runner/server (e.g. an
278
+ # un-indexed obligation query) must never block past the timeout, or Claude SIGKILLs
279
+ # the hook before it clears the turn -> stuck "busy" (#199). On timeout: no block.
280
+ response="$(curl -fsS --max-time 2 "http://127.0.0.1:${port}/reply-obligations/claude-stop" 2>/dev/null || true)"
281
+ case "$response" in
282
+ *'"decision":"block"'*|*'"decision": "block"'*) ;;
283
+ *) return 0 ;;
284
+ esac
285
+ printf '%s' "$response"
286
+ }
287
+
288
+ relay_json_string_field() {
289
+ local field="${1:-}"
290
+ local input="${2:-}"
291
+ [ -z "$field" ] && return 0
292
+ printf '%s' "$input" | sed -nE 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p' | head -1
293
+ }
294
+
295
+ relay_json_number_field() {
296
+ local field="${1:-}"
297
+ local input="${2:-}"
298
+ [ -z "$field" ] && return 0
299
+ printf '%s' "$input" | sed -nE 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' | head -1
300
+ }
301
+
302
+ relay_json_bool_field() {
303
+ local field="${1:-}"
304
+ local input="${2:-}"
305
+ [ -z "$field" ] && return 0
306
+ printf '%s' "$input" | sed -nE 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*(true|false).*/\1/p' | head -1
307
+ }
308
+
309
+ # Wall-clock epoch milliseconds, or nothing when no source can tell (never whole seconds).
310
+ relay_now_ms() {
311
+ local ms=""
312
+ if [ -n "${EPOCHREALTIME:-}" ]; then
313
+ local t="${EPOCHREALTIME/,/.}"
314
+ local frac="${t#*.}000"
315
+ ms="${t%.*}${frac:0:3}"
316
+ fi
317
+ case "$ms" in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) ;; *) ms="$(date +%s%3N 2>/dev/null || true)" ;; esac
318
+ case "$ms" in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) ;; *) ms="$(perl -MTime::HiRes=time -e 'printf "%d", time() * 1000' 2>/dev/null || true)" ;; esac
319
+ case "$ms" in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) printf '%s' "$ms" ;; esac
320
+ }
321
+
322
+ relay_json_escape() {
323
+ local text="${1:-}" code octal char escaped
324
+ text="${text//\\/\\\\}"
325
+ text="${text//\"/\\\"}"
326
+ # Bash strings cannot hold NUL. Escape every other JSON control byte, with no subprocesses.
327
+ for ((code = 1; code < 32; code++)); do
328
+ printf -v octal '%03o' "$code"
329
+ printf -v char '%b' "\\$octal"
330
+ printf -v escaped '\\u%04x' "$code"
331
+ text="${text//"$char"/$escaped}"
332
+ done
333
+ printf '%s' "$text"
334
+ }
335
+
336
+ # Print a short "request-review when done" reminder to stdout IFF this agent owns
337
+ # an isolated workspace whose branch has committed work not yet integrated into
338
+ # base. Prints nothing otherwise — so plain chat, shared-mode, and no-change
339
+ # sessions never pay context/token cost. Reads the workspace from
340
+ # AGENT_RELAY_WORKSPACE_JSON (set by the orchestrator at spawn) and does a local,
341
+ # network-free git count. Always returns 0 (never aborts its caller).
342
+ relay_review_reminder_text() {
343
+ local ws="${AGENT_RELAY_WORKSPACE_JSON:-}"
344
+ [ -z "$ws" ] && return 0
345
+ local mode worktree base id branch ahead
346
+ mode="$(relay_json_string_field mode "$ws")"
347
+ [ "$mode" = "isolated" ] || return 0
348
+ worktree="$(relay_json_string_field worktreePath "$ws")"
349
+ base="$(relay_json_string_field baseSha "$ws")"
350
+ [ -z "$base" ] && base="$(relay_json_string_field baseRef "$ws")"
351
+ id="$(relay_json_string_field id "$ws")"
352
+ branch="$(relay_json_string_field branch "$ws")"
353
+ [ -n "$worktree" ] || return 0
354
+ [ -n "$base" ] || return 0
355
+ [ -n "$id" ] || return 0
356
+ ahead="$(git -C "$worktree" rev-list --count "${base}..HEAD" 2>/dev/null || echo 0)"
357
+ case "$ahead" in ''|*[!0-9]*) ahead=0 ;; esac
358
+ [ "$ahead" -gt 0 ] || return 0
359
+ printf '[agent-relay] You have %s committed change(s) on `%s` that are not yet integrated into base. If your task is complete, request review so Humwick can auto-land it: POST /api/workspaces/%s/actions with {"action":"request-review"}. If you are still working, ignore this — it only appears while there is unmerged committed work.' \
360
+ "$ahead" "${branch:-this branch}" "$id"
361
+ return 0
362
+ }
363
+
364
+ # Wrap reminder text in the Claude Code additionalContext envelope for a given
365
+ # hook event. Emits nothing when the text is empty.
366
+ relay_emit_additional_context() {
367
+ local event="${1:-}" text="${2:-}"
368
+ [ -z "$event" ] && return 0
369
+ [ -z "$text" ] && return 0
370
+ printf '{"hookSpecificOutput":{"hookEventName":"%s","additionalContext":"%s"}}' \
371
+ "$event" "$(relay_json_escape "$text")"
372
+ return 0
373
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
4
+ relay_install_hook_guard session-end
5
+
6
+ payload="$(cat || true)"
7
+ reason="$(relay_json_string_field reason "$payload")"
8
+ transcript_path="$(relay_json_string_field transcript_path "$payload")"
9
+
10
+ case "$reason" in
11
+ clear)
12
+ relay_post_timeline_status idle provider-turn subagent clearing-context
13
+ # /clear wipes the context — the most common work boundary for a long-lived agent.
14
+ # Capture this segment's Insights (#183/#184) before it's gone.
15
+ relay_post_session_boundary "$transcript_path" clear
16
+ ;;
17
+ resume)
18
+ relay_post_status_clearing_subagents idle
19
+ ;;
20
+ logout|prompt_input_exit|bypass_permissions_disabled|other|*)
21
+ relay_post_status_clearing_subagents offline
22
+ # Real session termination: capture end-of-session Insights (#183/#184). Order after the
23
+ # status post is arbitrary — the runner reads the transcript file regardless.
24
+ relay_post_session_boundary "$transcript_path" "$reason"
25
+ ;;
26
+ esac
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
4
+ relay_install_hook_guard session-start
5
+ # #2664 — a stamped check-in proves the hook path works before the first prompt is submitted.
6
+ relay_forward_hook_payload /hook-check "{}" "$(relay_now_ms)"
7
+
8
+ payload="$(cat || true)"
9
+ source_kind="$(relay_json_string_field source "$payload")"
10
+ continuation=""
11
+
12
+ case "$source_kind" in
13
+ clear)
14
+ # A queued /clear can commit after the runner's confirmation timed out. Restore
15
+ # its continuation in THIS fresh context, even if the backstop hit the old one.
16
+ if [ -n "${AGENT_RELAY_RUNNER_PORT:-}" ]; then
17
+ continuation="$(printf '{"source":"clear","providerSessionId":"%s"}' "$(relay_json_escape "${AGENT_RELAY_PROVIDER_SESSION_ID:-}")" | relay_post_json "$AGENT_RELAY_RUNNER_PORT" /session-start 3 || true)"
18
+ fi
19
+ relay_post_timeline_status idle provider-turn subagent context-cleared
20
+ ;;
21
+ *)
22
+ ;;
23
+ esac
24
+
25
+ # Re-prime the request-review reminder when a session (re)starts — crucially on
26
+ # source=="compact", which is how it survives a context compaction (PreCompact
27
+ # cannot inject post-compact context; SessionStart can). No-op on a fresh startup
28
+ # with no committed work, and silent for non-isolated/no-change sessions.
29
+ reminder="$(relay_review_reminder_text || true)"
30
+ if [ -n "$continuation" ]; then reminder="${reminder:0:1000}"; fi
31
+ relay_emit_additional_context SessionStart "${continuation}${continuation:+${reminder:+$'\n\n'}}${reminder}"
@@ -0,0 +1,28 @@
1
+ // Shared "is this plain simple commands?" pre-filter for the PreToolUse guards (#2834, #2993).
2
+ // Heredocs, comments, functions, control-flow keywords, continuations, substitutions and
3
+ // per-line unbalanced quotes (a string spanning lines) all mean "unsure": the caller allows.
4
+ // `do`/`done` are deliberately not keywords here: they never appear without a header keyword
5
+ // (`for`/`while`/`until`/`select`), and `echo done` tripped the filter.
6
+ const SYNTAX = String.raw`<<|\\$|(^|\s)#|\$\(|` + "`" + String.raw`|\(\)|^\s*[{}]|\{\s*$`;
7
+ export const CONTROL_KEYWORDS = ["if", "then", "elif", "else", "fi", "for", "while", "until", "case", "esac", "select", "function"] as const;
8
+
9
+ const patterns = new Map<string, RegExp>();
10
+ function unsurePattern(keywords: readonly string[]): RegExp {
11
+ const key = keywords.join("|");
12
+ let pattern = patterns.get(key);
13
+ if (!pattern) {
14
+ pattern = new RegExp(`${SYNTAX}|(^|[\\s;&|(])(${key})\\b`, "m");
15
+ patterns.set(key, pattern);
16
+ }
17
+ return pattern;
18
+ }
19
+ const countOf = (line: string, char: string): number => line.split(char).length - 1;
20
+
21
+ /** `;`-joined non-blank lines, or null when the text is not plainly simple commands. `keywords` are the control
22
+ * words that mean "unsure"; a caller that parses a construct itself (a `while` loop) drops it from the list. */
23
+ export function trustedFlatCommand(command: string, keywords: readonly string[] = CONTROL_KEYWORDS): string | null {
24
+ if (unsurePattern(keywords).test(command)) return null;
25
+ const lines = command.split(/\r?\n/);
26
+ if (lines.some((line) => countOf(line, "'") % 2 === 1 || countOf(line, '"') % 2 === 1)) return null;
27
+ return lines.filter((line) => line.trim()).join("; ");
28
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
4
+ relay_install_hook_guard stop-failure
5
+
6
+ payload="$(cat || true)"
7
+ # CC sends the error class as `error_type` (StopFailure docs, verified 2026-06);
8
+ # fall back to legacy `error` so older payloads / tests still match.
9
+ error="$(relay_json_string_field error_type "$payload")"
10
+ [ -z "$error" ] && error="$(relay_json_string_field error "$payload")"
11
+ reset_time="$(relay_json_number_field reset_time "$payload")"
12
+ error_message="$(relay_json_string_field error_message "$payload")"
13
+ [ -z "$error_message" ] && error_message="$(relay_json_string_field error_details "$payload")"
14
+ [ -z "$error_message" ] && error_message="$(relay_json_string_field last_assistant_message "$payload")"
15
+
16
+ # #286: a usage/rate-limit stall is transient and time-bounded — hold the agent
17
+ # (providerState blocked) and auto-resume at reset, instead of silently going idle.
18
+ # The disambiguator for billing_error is reset_time: a subscription rolling-window
19
+ # limit carries one (and is auto-resumable); true credit exhaustion does not (fatal).
20
+ # #3577: a refused proxy credential (ERR_PROXY_TUNNEL) also posts here; the runner classifies it.
21
+ case "$error" in
22
+ rate_limit|overloaded|server_error|api_error)
23
+ relay_post_rate_limit "$error" "$reset_time" "$error_message"
24
+ ;;
25
+ unknown)
26
+ case "$error_message" in
27
+ *"529"*|*"Overloaded"*|*"overloaded"*|*"temporar"*|*"server-side"*|*"server error"*|*"ERR_PROXY_TUNNEL"*)
28
+ relay_post_rate_limit "$error" "$reset_time" "$error_message"
29
+ ;;
30
+ *)
31
+ relay_post_status_clearing_subagents idle
32
+ ;;
33
+ esac
34
+ ;;
35
+ billing_error)
36
+ case "$reset_time" in
37
+ ''|*[!0-9]*) relay_post_status_clearing_subagents error ;;
38
+ *) relay_post_rate_limit "$error" "$reset_time" "$error_message" ;;
39
+ esac
40
+ ;;
41
+ authentication_failed|oauth_org_not_allowed|model_not_found)
42
+ relay_post_status_clearing_subagents error
43
+ ;;
44
+ *)
45
+ relay_post_status_clearing_subagents idle
46
+ ;;
47
+ esac
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks/relay-status.sh"
4
+ relay_install_hook_guard stop
5
+
6
+ # Clearing the turn's busy state is the critical path (#199). Register it on EXIT
7
+ # so it runs even if a side-call below fails or times out under `set -e`. The one
8
+ # exception is the reply-obligation block path, which deliberately keeps the agent
9
+ # busy to answer — it opts out via the flag before exiting.
10
+ _relay_clear_idle_on_exit=1
11
+ # bg_count/bg_tasks_json are read from the payload below and consumed by the trap at exit
12
+ # time (#502): the turn-end status carries the live background-script snapshot so an agent
13
+ # whose turn ended while a run_in_background shell keeps running stays busy instead of
14
+ # falsely reading idle. bg_tasks_json (#2034) is the per-task {id, command} detail behind
15
+ # that snapshot — the ledger the runner enriches with startedAt and a watchdog can read.
16
+ bg_count=0
17
+ bg_tasks_json="[]"
18
+ trap '[ "${_relay_clear_idle_on_exit:-0}" = "1" ] && relay_post_turn_end_status "${bg_count:-0}" "${bg_tasks_json:-[]}"' EXIT
19
+
20
+ payload="$(cat || true)"
21
+ stop_hook_active="$(relay_json_bool_field stop_hook_active "$payload")"
22
+ bg_count="$(relay_background_task_count "$payload")"
23
+ bg_tasks_json="$(relay_background_task_list "$payload")"
24
+
25
+ # Session-mirror capture is orthogonal to reply-obligation gating (#332): a recursive Stop
26
+ # (stop_hook_active=true) must NOT suppress capturing this turn's response, or the live mirror
27
+ # silently drops frames. Always post the turn; only the reply-obligation decision is gated below.
28
+ last_assistant_msg="$(echo "$payload" | jq -c '.last_assistant_message // empty' 2>/dev/null || true)"
29
+ # prompt_id (#1086): Claude's own native turn identifier, correlating this Stop with
30
+ # the same UUID stamped on every transcript entry of the turn (and the id the
31
+ # UserPromptSubmit hook reported at turn start) — the stable replacement for a
32
+ # runner-minted random UUID.
33
+ prompt_id="$(relay_json_string_field prompt_id "$payload")"
34
+ relay_post_session_turn "$(relay_json_string_field transcript_path "$payload")" "$last_assistant_msg" "$prompt_id"
35
+
36
+ if [ "$stop_hook_active" != "true" ]; then
37
+ # `|| true`: under `set -e`, a non-zero from the obligation check must never abort
38
+ # the hook before the idle-clear — clearing the turn is the critical path (#199).
39
+ stop_decision="$(relay_pending_reply_stop_decision || true)"
40
+ if [ -n "$stop_decision" ]; then
41
+ _relay_clear_idle_on_exit=0
42
+ printf '%s\n' "$stop_decision"
43
+ exit 0
44
+ fi
45
+ fi
46
+
47
+ # Normal turn end → the EXIT trap posts idle (always, even on an unexpected abort above).
48
+ exit 0
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bun
2
+ // Native model forcing exempts forks; resolve skills at execution, after lazy discovery.
3
+ const fs = require('node:fs');
4
+ (async () => {
5
+ const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
6
+ const input = payload.tool_input || {};
7
+ const event = payload.hook_event_name;
8
+ let reason;
9
+ if (require('./subagent-skill-policy.cjs').harnessSubagentsDisabled() && ['Agent', 'Task', 'TeamCreate', 'TeamDelete', 'SendMessage'].includes(payload.tool_name)) {
10
+ reason = 'Harness sub-agents are disabled. Start a Relay workflow or ask your coordinator';
11
+ } else if (event === 'UserPromptSubmit') {
12
+ if (/^\s*\/(fork|subtask|remote|agents|team|teams|workflow|autocompact)(?:\s|$)/i.test(payload.prompt || '')) reason = 'native fork, remote, team and workflow commands are disabled';
13
+ } else if (event === 'UserPromptExpansion' || payload.tool_name === 'Skill') {
14
+ if (payload.expansion_type !== 'mcp_prompt') {
15
+ reason = await require('./subagent-skill-policy.cjs').skillDenial(payload, event === 'UserPromptExpansion' ? payload.command_name : input.skill);
16
+ }
17
+ } else if (['TeamCreate', 'TeamDelete', 'SendMessage', 'Workflow'].includes(payload.tool_name) || input.subagent_type === 'fork' || input.isolation === 'remote' || input.team_name || input.resume) {
18
+ reason = 'native forks, remote agents, teams, workflows and resumed children are disabled; use an ordinary Agent child or Relay spawn';
19
+ }
20
+ if (reason) {
21
+ reason = `Relay child policy denied execution: ${reason}. Start a Relay workflow or ask your coordinator.`;
22
+ console.log(JSON.stringify(event === 'PreToolUse'
23
+ ? { hookSpecificOutput: { hookEventName: event, permissionDecision: 'deny', permissionDecisionReason: reason } }
24
+ : { decision: 'block', reason }));
25
+ }
26
+ })().catch((error) => { console.error(`Relay child policy cannot validate execution: ${error.message}`); process.exit(2); });