@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.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- 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); });
|