@humwick/runner 0.130.5 → 0.130.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +7 -10
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +1 -1
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +1 -1
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +1 -1
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +1 -1
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/src/adapters/provider-quota-harvest.ts +0 -76
|
@@ -19,7 +19,7 @@ export const claudePermissionPromptHandler: ProviderPermissionPromptHandler = {
|
|
|
19
19
|
const RECOMMENDED_LABEL = /\(recommended\)\s*$/i;
|
|
20
20
|
const DECISION_TITLE_MAX = 200;
|
|
21
21
|
|
|
22
|
-
//
|
|
22
|
+
// AskUserQuestion → operator decision, 1:1. Claude marks its pick with a "(Recommended)" label.
|
|
23
23
|
function claudeDecisionRequest(body: Record<string, unknown>): OperatorDecisionFileInput | undefined {
|
|
24
24
|
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
25
25
|
if (body.tool_name !== "AskUserQuestion" || !Array.isArray(toolInput.questions)) return undefined;
|
|
@@ -116,7 +116,7 @@ function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput,
|
|
|
116
116
|
hookSpecificOutput: {
|
|
117
117
|
hookEventName: "PreToolUse",
|
|
118
118
|
permissionDecision: "deny",
|
|
119
|
-
permissionDecisionReason: decisionReasonMessage(decision, "Dismissed from
|
|
119
|
+
permissionDecisionReason: decisionReasonMessage(decision, "Dismissed from Humwick dashboard"),
|
|
120
120
|
},
|
|
121
121
|
};
|
|
122
122
|
}
|
|
@@ -150,7 +150,7 @@ function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput,
|
|
|
150
150
|
hookEventName,
|
|
151
151
|
decision: {
|
|
152
152
|
behavior: "deny",
|
|
153
|
-
message: decisionReasonMessage(decision, "Denied from
|
|
153
|
+
message: decisionReasonMessage(decision, "Denied from Humwick dashboard"),
|
|
154
154
|
interrupt: decision.decision === "abort",
|
|
155
155
|
},
|
|
156
156
|
},
|
|
@@ -188,7 +188,7 @@ function claudeResolvedPermissionPrompt(view: InteractivePrompt, decision: Provi
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
//
|
|
191
|
+
// AskUserQuestion's PreToolUse hook is answered with `deny` immediately (see
|
|
192
192
|
// control-server.ts) so Claude flushes its buffered reasoning instead of blocking on the human.
|
|
193
193
|
// The user's actual answer arrives afterward as a normal injected prompt, not as a hook
|
|
194
194
|
// response — this renders that prompt's text from the same view/decision shape the old
|
|
@@ -199,7 +199,7 @@ function claudeAnswerInjectionPrompt(view: InteractivePrompt, decision: Provider
|
|
|
199
199
|
const rows = resolvedQuestionRows(questions, decision.answers, "(no answer given)");
|
|
200
200
|
const qa = rows.map((row) => `Q: ${row.question}\nA: ${row.answer}`).join("\n\n");
|
|
201
201
|
return [
|
|
202
|
-
"This was answered by the user via the
|
|
202
|
+
"This was answered by the user via the Humwick dashboard — the AskUserQuestion tool call itself was intercepted and will not resolve; it will not return a result. Here is their answer:",
|
|
203
203
|
qa,
|
|
204
204
|
"Continue the task using their answer(s) above — do not call AskUserQuestion again for the same question(s), and do not wait for the original tool call to return.",
|
|
205
205
|
].join("\n\n");
|
|
@@ -18,16 +18,16 @@ export interface ClaudeLaunchPromptGatePrevention {
|
|
|
18
18
|
type PaneGateMatch = { message?: string };
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* how much authority a pane match is allowed to carry.
|
|
22
22
|
*
|
|
23
23
|
* `advisory` is the only correct value for a gate detected purely from TUI geometry. It
|
|
24
24
|
* produces a {@link PaneObservation} and NOTHING else: no keystroke (the concept no longer
|
|
25
25
|
* exists in this file), no ProviderStatusUpdate, hence no status, no `clear`, no
|
|
26
|
-
* providerState.
|
|
26
|
+
* providerState. Each tried to make one of these gates safe by
|
|
27
27
|
* tightening its string matcher; the matcher was never the problem — granting a string match
|
|
28
28
|
* the authority to mutate state was.
|
|
29
29
|
*
|
|
30
|
-
* The retired usage-limit pane gate
|
|
30
|
+
* The retired usage-limit pane gate is deliberately not represented here: provider
|
|
31
31
|
* state comes from structured provider failures, never a pane scrape.
|
|
32
32
|
*/
|
|
33
33
|
|
|
@@ -53,7 +53,7 @@ export interface ClaudePromptGatePaneContext {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface ClaudePromptGatePaneResult {
|
|
56
|
-
/**
|
|
56
|
+
/** always present when a gate reached minDetections. Diagnostic only. */
|
|
57
57
|
observation?: PaneObservation;
|
|
58
58
|
/** Kept for callers which assert absence; `never` makes a status impossible to emit. */
|
|
59
59
|
status?: never;
|
|
@@ -87,7 +87,7 @@ export const claudeConfigPromptGatePreventions: ClaudeConfigPromptGatePrevention
|
|
|
87
87
|
id: "workspace-trust",
|
|
88
88
|
description: "Trust the managed spawn cwd and clear project onboarding.",
|
|
89
89
|
apply(seed, { config }) {
|
|
90
|
-
//
|
|
90
|
+
// seed EVERY key CC may resolve this cwd under (lexical, realpath, git
|
|
91
91
|
// canonical root), not just the lexical worktree path. Host-base launches, which
|
|
92
92
|
// never reach this seed, get the same keys written into the shared config by
|
|
93
93
|
// ensureClaudeWorkspaceTrust on the launch path.
|
|
@@ -117,7 +117,7 @@ export const claudeLaunchPromptGatePreventions: ClaudeLaunchPromptGatePrevention
|
|
|
117
117
|
const WORKSPACE_TRUST_PROMPT = /do you trust (?:the files in )?(?:this folder|this project|this directory)|accessing workspace|quick safety check|yes,? ?I trust this folder/i;
|
|
118
118
|
|
|
119
119
|
const panePromptGates: ClaudePanePromptGate[] = [
|
|
120
|
-
//
|
|
120
|
+
// the five gates below are ADVISORY. Each used to type a key into a live pane the
|
|
121
121
|
// instant `claudePaneIsBusy` happened to read false, then publish a fabricated
|
|
122
122
|
// idle + clear:["subagent"]. Two independent hazards, both realized:
|
|
123
123
|
// - the four Enter gates SUBMIT whatever text is already sitting in the composer;
|
|
@@ -135,7 +135,7 @@ const panePromptGates: ClaudePanePromptGate[] = [
|
|
|
135
135
|
label: "Workspace trust",
|
|
136
136
|
policy: "accept-trusted-workspace",
|
|
137
137
|
minDetections: 2,
|
|
138
|
-
//
|
|
138
|
+
// CC 2.1.233 reworded this modal ("Accessing workspace" / "Quick safety check:
|
|
139
139
|
// Is this a project you created or one you trust?" / "Yes, I trust this folder"), so the
|
|
140
140
|
// old "do you trust this folder" matcher never fired and the wedge was silent. Both
|
|
141
141
|
// wordings are matched now. Still ADVISORY (see the block comment above): the actual
|
|
@@ -180,14 +180,14 @@ const panePromptGates: ClaudePanePromptGate[] = [
|
|
|
180
180
|
? { message: lineMatching(text, /welcome to Claude Code/i) }
|
|
181
181
|
: null,
|
|
182
182
|
},
|
|
183
|
-
//
|
|
183
|
+
// (review finding 3) — the "Exit anyway / Move to background / Stay"
|
|
184
184
|
// confirmation is deliberately NOT a live pane-watcher gate. Auto-pressing Enter
|
|
185
185
|
// on a session that did NOT initiate shutdown is unsafe: it could submit stray
|
|
186
186
|
// input or dismiss an unrelated modal on an attached/live session. This dialog is
|
|
187
187
|
// only ever the right thing to answer under an explicit Relay shutdown, so it is
|
|
188
188
|
// handled solely by the runner's shutdown grace loop (confirmClaudeExitDialogUnderGrant
|
|
189
189
|
// in claude-exit.ts) — which runs only after this runner sent `/exit` under a one-shot
|
|
190
|
-
// grant.
|
|
190
|
+
// grant. Made that structural rather than advisory: this file cannot even import
|
|
191
191
|
// the exit writers (claude-exit.guard.test.ts fails the build if it ever does). The
|
|
192
192
|
// orphan safety net for a session already wedged mid-shutdown is the orchestrator's
|
|
193
193
|
// wedged-session reaper, not a keypress into a live pane.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Claude's UserPromptSubmit hook fires only when Claude accepts an Enter: at queue time
|
|
2
2
|
// for a busy agent and never again on dequeue, and not at all for a swallowed Enter. A matching
|
|
3
3
|
// hook fired after our Enter is therefore the acceptance proof; the composer read is only a fallback.
|
|
4
4
|
|
|
5
5
|
// Transport changes observed on Claude Code 2.1.284-2.1.285, and nothing else: image paths it attached
|
|
6
6
|
// become leading [Image #N] chips, and a long paste arrives inside a <pasted_content> wrapper, even
|
|
7
|
-
// a single-line one
|
|
7
|
+
// a single-line one. The whitespace around the wrapper and its body varies by release, so
|
|
8
8
|
// both are compared trimmed.
|
|
9
9
|
// Image pastes also collapse blank-line runs (including the runner's attachment separator).
|
|
10
10
|
const LEADING_IMAGE_CHIPS = /^(?:\[Image #\d+\])+/;
|
|
@@ -31,13 +31,13 @@ const REASONING_POLL_MS = 1_200;
|
|
|
31
31
|
// Bounded the same way as control-server.ts's resolvedApprovals so a long session's dedup
|
|
32
32
|
// guard (see emittedStepKeys) can't grow without limit.
|
|
33
33
|
const EMITTED_STEP_KEY_LIMIT = 2048;
|
|
34
|
-
//
|
|
34
|
+
// a prompt still queued at a Stop was enqueued after the turn's last tool boundary, so it
|
|
35
35
|
// sits in the transcript's last entries; a bounded tail read finds it.
|
|
36
36
|
const QUEUE_SCAN_BYTES = 256 * 1024;
|
|
37
37
|
const HOOKED_PROMPT_LIMIT = 8;
|
|
38
38
|
|
|
39
39
|
export class ClaudeSessionCapture {
|
|
40
|
-
//
|
|
40
|
+
// every UserPromptSubmit hook; the proof that Claude accepted a submitted prompt.
|
|
41
41
|
readonly promptHooks = new ClaudePromptHookLedger();
|
|
42
42
|
private narrativeFlushEntryCount = 0;
|
|
43
43
|
private reasoningTail?: ReasoningTailState;
|
|
@@ -50,7 +50,7 @@ export class ClaudeSessionCapture {
|
|
|
50
50
|
// it can re-derive the exact same step signatures the earlier tail already emitted. Seeding
|
|
51
51
|
// each new tail's `seen` set from this closes that gap regardless of the exact timing.
|
|
52
52
|
private emittedStepKeys = new Set<string>();
|
|
53
|
-
//
|
|
53
|
+
// recent UserPromptSubmit prompts. Claude hooks a prompt submitted mid-turn when it queues
|
|
54
54
|
// it and never again when it runs it. Pruned at every Stop to the ones still queued.
|
|
55
55
|
private hookedPrompts: string[] = [];
|
|
56
56
|
private sessionEventCb: (event: ProviderSessionEvent) => void = () => {};
|
|
@@ -93,7 +93,7 @@ export class ClaudeSessionCapture {
|
|
|
93
93
|
const flushCount = this.narrativeFlushEntryCount;
|
|
94
94
|
this.narrativeFlushEntryCount = 0;
|
|
95
95
|
|
|
96
|
-
//
|
|
96
|
+
// the reasoning tail already mirrors each narration line as its own row, so the
|
|
97
97
|
// turn-final (and the report built from it) is the closing text only.
|
|
98
98
|
const closingOnly = ctx.chatCaptureMode === "final" || ctx.reasoningCapture !== false;
|
|
99
99
|
let body = extractHookAssistantMessage(input.lastAssistantMessage);
|
|
@@ -145,7 +145,7 @@ export class ClaudeSessionCapture {
|
|
|
145
145
|
ctx.log(input.firedAt === undefined ? "hook check-in carried no firedAt; submits stay on composer confirmation" : "hook check-in: stamped hook path verified");
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/**
|
|
148
|
+
/** whether a prompt Claude already hooked is still queued, so it runs next with no hook of its own. */
|
|
149
149
|
private async hookedPromptStillQueued(transcriptPath: string): Promise<boolean> {
|
|
150
150
|
if (!this.hookedPrompts.length) return false;
|
|
151
151
|
const queued = pendingQueuedPrompts(await readTranscriptTail(transcriptPath)).map((prompt) => prompt.trim());
|
|
@@ -174,7 +174,7 @@ export class ClaudeSessionCapture {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// runs whatever reasoningCapture says: its tool steps carry the paths area memory matches,
|
|
178
178
|
// and the session mirror drops the steps from chat when reasoningCapture is off.
|
|
179
179
|
private startReasoningTail(transcriptPath: string, ctx: ProviderSessionTurnContext): void {
|
|
180
180
|
this.stopSessionTrace();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Claude Code's intrinsic, machine-readable session probe.
|
|
2
2
|
//
|
|
3
3
|
// Claude Code ≥2.1.119 writes `$CLAUDE_CONFIG_DIR/sessions/<pid>.json` and rewrites
|
|
4
4
|
// it on every status transition. It carries an authoritative `status` field
|
|
@@ -33,7 +33,7 @@ export const CLAUDE_PROBE_MIN_VERSION = "2.1.119";
|
|
|
33
33
|
/**
|
|
34
34
|
* The probe half of an activity read: the mapped 3-valued activity (undefined when the probe is
|
|
35
35
|
* absent/old/unparseable → the caller falls back to the pane) plus `busyStale`: the busy verdict
|
|
36
|
-
* predates a turn-cancelling keystroke relay itself sent
|
|
36
|
+
* predates a turn-cancelling keystroke relay itself sent. Age never discredits it:
|
|
37
37
|
* Claude writes neither probe nor transcript during a long think, so a frozen busy is held and the
|
|
38
38
|
* notify-only prolonged-hold watchdog surfaces a turn that really died out of band.
|
|
39
39
|
*/
|
|
@@ -45,7 +45,7 @@ export function claudeProbeVerdict(
|
|
|
45
45
|
if (activity !== "busy" || !reading || opts.paneInterruptAt === undefined || reading.updatedAt === null) {
|
|
46
46
|
return { activity, busyStale: false };
|
|
47
47
|
}
|
|
48
|
-
//
|
|
48
|
+
// a transcript append after the interrupt means the cancel did not take.
|
|
49
49
|
const cancelDidNotTake = opts.lastActivityAt !== undefined && opts.lastActivityAt > opts.paneInterruptAt;
|
|
50
50
|
return { activity, busyStale: reading.updatedAt <= opts.paneInterruptAt && !cancelDidNotTake };
|
|
51
51
|
}
|
|
@@ -61,9 +61,9 @@ export interface ClaudeSessionReading {
|
|
|
61
61
|
procStart: string | null;
|
|
62
62
|
/** `updatedAt` epoch-ms of the last probe write, or null. */
|
|
63
63
|
updatedAt: number | null;
|
|
64
|
-
/** Claude's own session UUID
|
|
64
|
+
/** Claude's own session UUID — names the transcript file. Null when absent. */
|
|
65
65
|
sessionId: string | null;
|
|
66
|
-
/** The session's cwd
|
|
66
|
+
/** The session's cwd — slugified into the transcript's project dir. Null when absent. */
|
|
67
67
|
cwd: string | null;
|
|
68
68
|
/** True iff a present, parseable, version-gated probe produced `status`. */
|
|
69
69
|
available: boolean;
|
|
@@ -72,7 +72,7 @@ export interface ClaudeSessionReading {
|
|
|
72
72
|
const UNAVAILABLE: ClaudeSessionReading = { status: "unknown", waitingFor: null, version: null, procStart: null, updatedAt: null, sessionId: null, cwd: null, available: false };
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Process-liveness seam for the staleness guard
|
|
75
|
+
* Process-liveness seam for the staleness guard, injectable in tests.
|
|
76
76
|
* procStartOf — the start-time token of a LIVE pid (to compare against the probe's
|
|
77
77
|
* `procStart`), or null when the pid is dead/unreadable. Catches both a frozen
|
|
78
78
|
* probe from a SIGKILLed claude AND a sub-second pid-reuse window (a reused pid
|
|
@@ -89,7 +89,7 @@ function procStatStartTime(stat: string): string | null {
|
|
|
89
89
|
return stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/)[19] ?? null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// rows 14/15 — off-loop: the 4s reconciler / 1.5s turn-watch tick never blocks on the read.
|
|
93
93
|
async function readProcStartTimeAsync(pid: number): Promise<string | null> {
|
|
94
94
|
try {
|
|
95
95
|
return procStatStartTime(await readFile(`/proc/${pid}/stat`, "utf8"));
|
|
@@ -98,7 +98,7 @@ async function readProcStartTimeAsync(pid: number): Promise<string | null> {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
//
|
|
101
|
+
// off Linux, Claude stamps procStart as `ps -o lstart=` in UTC ("Sun Oct 25 13:01:19 2026").
|
|
102
102
|
const PS_LSTART_TIMEOUT_MS = 5_000;
|
|
103
103
|
const psLstartArgv = (pid: number): string[] => ["ps", "-o", "lstart=", "-p", String(pid)];
|
|
104
104
|
const psLstartEnv = (): Record<string, string | undefined> => ({ ...process.env, TZ: "UTC", LC_ALL: "C" });
|
|
@@ -125,9 +125,9 @@ export async function readPsLstartUtcAsync(pid: number): Promise<string | null>
|
|
|
125
125
|
// the probe on a status *transition*, so a legitimately busy turn keeps a frozen
|
|
126
126
|
// `updatedAt` for its whole duration (verified: a 90s+ busy turn never rewrote it).
|
|
127
127
|
// A staleness threshold would force a long busy turn back to the pane-scrape and
|
|
128
|
-
// reintroduce the
|
|
128
|
+
// reintroduce the idle-while-working bug; pid-liveness + procStart cover the
|
|
129
129
|
// SIGKILL-frozen and pid-reuse cases without that hazard. `available` is not
|
|
130
|
-
// time-gated, and neither is the busy verdict
|
|
130
|
+
// time-gated, and neither is the busy verdict.
|
|
131
131
|
/** The off-loop probe liveness check for `platform`: `/proc` on Linux, UTC `ps` lstart elsewhere. */
|
|
132
132
|
export function claudeProbeLivenessAsync(
|
|
133
133
|
platform: string = process.platform,
|
|
@@ -208,7 +208,7 @@ export function parseClaudeSessionProbe(raw: string, minVersion = CLAUDE_PROBE_M
|
|
|
208
208
|
/**
|
|
209
209
|
* Read + parse the probe for `pid` under `configHome`. Missing file (older CC, a config home the
|
|
210
210
|
* runner can't resolve, or claude not yet started) → unavailable, so the caller falls back to the
|
|
211
|
-
* pane-scrape. Off-loop
|
|
211
|
+
* pane-scrape. Off-loop: every caller is a timer tick or the async `/exit` correlator.
|
|
212
212
|
*/
|
|
213
213
|
export async function readClaudeSessionStatusAsync(
|
|
214
214
|
configHome: string,
|
|
@@ -223,7 +223,7 @@ export async function readClaudeSessionStatusAsync(
|
|
|
223
223
|
}
|
|
224
224
|
const reading = parseClaudeSessionProbe(raw, opts.minVersion ?? CLAUDE_PROBE_MIN_VERSION);
|
|
225
225
|
if (!reading.available) return reading;
|
|
226
|
-
//
|
|
226
|
+
// P1-B: a frozen probe from a SIGKILLed claude (or a reused pid) would report a stale
|
|
227
227
|
// busy/shell forever. Verify the pid still belongs to the same process.
|
|
228
228
|
const live = opts.liveness ?? DEFAULT_LIVENESS_ASYNC;
|
|
229
229
|
if (reading.procStart) {
|
|
@@ -252,7 +252,7 @@ export function claudeProbeArgsFor(process: ManagedProcess, tmuxSession: string,
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
255
|
+
* a clear-commit probe reading: Claude's own session UUID together with the pid it was
|
|
256
256
|
* read for. A committed `/clear` mints a NEW session, so a rotation of `sessionId` OBSERVED FOR
|
|
257
257
|
* THE SAME PID is machine-readable proof the loaded conversation is gone.
|
|
258
258
|
*/
|
|
@@ -261,7 +261,7 @@ export interface ClaudeClearSessionReading { sessionId: string; pid: number }
|
|
|
261
261
|
/**
|
|
262
262
|
* Read one, or null for every "could not find out" outcome: no resolvable pid, no probe file, a
|
|
263
263
|
* version-gated-out or liveness-failed reading (`available === false`, which is what rules out a
|
|
264
|
-
* SIGKILL-frozen probe and a reused pid —
|
|
264
|
+
* SIGKILL-frozen probe and a reused pid — P1-B), or a probe carrying no sessionId at all.
|
|
265
265
|
* Deliberately total (never throws, never distinguishes flavours of absence): its only consumer
|
|
266
266
|
* treats anything short of a confirmed same-pid rotation as no evidence.
|
|
267
267
|
*/
|
|
@@ -276,7 +276,7 @@ export async function readClaudeClearSessionAsync(args: ManagedClaudeProbeArgs):
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
*
|
|
279
|
+
* bind a clear-commit reader to ONE pane pid, resolved up front.
|
|
280
280
|
*
|
|
281
281
|
* The barrier polls ~50 times inside its 10s budget. Re-resolving the pane pid per poll spawned a
|
|
282
282
|
* `tmux display-message` each time (the pid is undefined for a tmux launch), and — worse — let the
|
|
@@ -299,7 +299,7 @@ export async function bindClaudeClearSessionReader(
|
|
|
299
299
|
export type ClaudeClearSessionVerdict = "rotated" | "unchanged" | "unknown";
|
|
300
300
|
|
|
301
301
|
/**
|
|
302
|
-
*
|
|
302
|
+
* compare a pre-clear reading against a current one. Both must be present AND describe the
|
|
303
303
|
* SAME pid; anything else is `unknown`, never a verdict. A `rotated` verdict is positive proof the
|
|
304
304
|
* clear committed; an `unchanged` one is positive proof it has NOT (yet) — the probe is
|
|
305
305
|
* authoritative in both directions whenever it is readable on both sides.
|
|
@@ -327,10 +327,10 @@ export async function readManagedClaudeStatusAsync(
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
/**
|
|
330
|
-
* The transcript JSONL path Claude Code writes for a session
|
|
330
|
+
* The transcript JSONL path Claude Code writes for a session. Claude stores
|
|
331
331
|
* per-cwd transcripts at `<configHome>/projects/<cwd-slug>/<sessionId>.jsonl`, where
|
|
332
332
|
* the slug replaces every non-alphanumeric char in the absolute cwd with `-` (verified
|
|
333
|
-
* live on CC 2.1.205: `/home/u/p/.
|
|
333
|
+
* live on CC 2.1.205: `/home/u/p/.humwick/x` → `-home-u-p--humwick-x`). This is
|
|
334
334
|
* the hookless path the monitorless turn-watch feeds to the transcript reasoning tail so
|
|
335
335
|
* isolated/lean Claude workers (no Stop/UserPromptSubmit hooks) still mirror chat.
|
|
336
336
|
*/
|
|
@@ -344,16 +344,16 @@ export function claudeTranscriptPath(configHome: string, cwd: string, sessionId:
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
/**
|
|
347
|
-
*
|
|
347
|
+
* the transcript's last-modified time in epoch-ms, or undefined when it can't be read
|
|
348
348
|
* (absent/old probe with no sessionId+cwd, or the file not yet created). Claude appends to this
|
|
349
349
|
* JSONL on every turn event, so its mtime is the runner's hookless "events still flowing"
|
|
350
350
|
* heartbeat — the signal that keeps an actively-streaming turn's frozen busy probe from being
|
|
351
351
|
* aged out to the pane. A failed stat is silence, never a claim the turn ended: undefined leaves
|
|
352
|
-
*
|
|
352
|
+
* untouched.
|
|
353
353
|
*
|
|
354
|
-
*
|
|
354
|
+
* rows 14/15 — the only caller (the busy reconciler / turn watch, via activityFromProbeOrPane)
|
|
355
355
|
* is now off the event loop, so this is the sole variant and uses `fs/promises`. (The synchronous
|
|
356
|
-
* version was removed in the
|
|
356
|
+
* version was removed in the round-3 review: it had no production callers once the hot path
|
|
357
357
|
* went async — see the critical-loop ratchet note for claude-session-probe.ts.)
|
|
358
358
|
*/
|
|
359
359
|
export async function claudeTranscriptMtimeMsAsync(
|
|
@@ -371,7 +371,7 @@ export async function claudeTranscriptMtimeMsAsync(
|
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
/**
|
|
374
|
-
* Resolve the live transcript path for the managed claude via its session probe
|
|
374
|
+
* Resolve the live transcript path for the managed claude via its session probe: the
|
|
375
375
|
* probe carries `sessionId` + `cwd`, and the runner knows the config home. undefined when the
|
|
376
376
|
* probe is absent/old or the pid can't be resolved (graceful degrade: no native capture).
|
|
377
377
|
*/
|
|
@@ -389,13 +389,13 @@ export async function resolveManagedClaudeTranscriptPathAsync(args: {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
* Map the intrinsic probe status to the 3-valued activity. The whole point
|
|
392
|
+
* Map the intrinsic probe status to the 3-valued activity. The whole point:
|
|
393
393
|
* busy → busy — a turn is running, foreground tools included; the old
|
|
394
394
|
* pane-scrape misread it as idle and force-cleared.
|
|
395
395
|
* idle/shell → idle — `shell` is Claude's `idle` while a background local_bash task
|
|
396
396
|
* is alive (2.1.310: `status==="idle" && hasLiveLocalBash`).
|
|
397
397
|
* Relay's own monitor is one, so a managed session at rest
|
|
398
|
-
* reads `shell`, including right after an ESC
|
|
398
|
+
* reads `shell`, including right after an ESC.
|
|
399
399
|
* waiting → unknown — a permission/input dialog is a parked (not done) turn, so
|
|
400
400
|
* we neither assert compute-busy nor let the backstop clear
|
|
401
401
|
* the live turn. `waitingFor` is surfaced separately for
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (maybe) exit gracefully. Split out of ./claude at the
|
|
1
|
+
// the Claude tmux shutdown sequence: observe, correlate, and only then
|
|
2
|
+
// (maybe) exit gracefully. Split out of ./claude at the ratchet; the adapter
|
|
3
3
|
// keeps the watcher teardown and delegates the rest here.
|
|
4
4
|
//
|
|
5
5
|
// The policy itself lives in ./claude-exit. This module is the caller that samples
|
|
6
6
|
// live state and drives the loop.
|
|
7
|
-
import {
|
|
7
|
+
import { prepareTmuxPaneStop, prepareTmuxStop, tmuxHasSession, type PreparedTmuxStop } from "@humwick/sdk/tmux-utils";
|
|
8
8
|
import { tmuxOwnerIdFromEnv } from "../config";
|
|
9
9
|
import type { ManagedProcess, ShutdownOutcome } from "../adapter";
|
|
10
10
|
import { claudeProbeActivity, claudeProbeArgsFor, readManagedClaudeStatusAsync, type ClaudeProbeLivenessAsync } from "./claude-session-probe";
|
|
@@ -43,10 +43,10 @@ export async function observeClaudeExitState(
|
|
|
43
43
|
fallbackEpoch: number,
|
|
44
44
|
seams: { liveness?: ClaudeProbeLivenessAsync; capturePane?: typeof captureTmuxPaneAsync } = {},
|
|
45
45
|
): Promise<ClaudeExitObservation> {
|
|
46
|
-
//
|
|
46
|
+
// the shared off-loop reader, so a macOS `ps` lstart procStart binds this reading too.
|
|
47
47
|
const managed = await readManagedClaudeStatusAsync(claudeProbeArgsFor(process, sessionName, socketName), seams.liveness);
|
|
48
48
|
const reading = managed?.reading;
|
|
49
|
-
//
|
|
49
|
+
// a managed Claude at rest reads `shell` (Relay's monitor is a live background task).
|
|
50
50
|
const probeIdle = reading?.available === true && claudeProbeActivity(reading.status) === "idle";
|
|
51
51
|
let turn: ClaudeTurnState = probeIdle ? "ended" : reading?.available === true ? "live" : "unknown";
|
|
52
52
|
if (turn === "ended") {
|
|
@@ -75,7 +75,7 @@ export interface ClaudeShutdownDeps {
|
|
|
75
75
|
observe?: (process: ManagedProcess, sessionName: string, socketName?: string) => ClaudeExitObservation | Promise<ClaudeExitObservation>;
|
|
76
76
|
submitExit?: (grant: ClaudeExitGrant) => Promise<unknown>;
|
|
77
77
|
confirmExit?: (grant: ClaudeExitGrant) => unknown;
|
|
78
|
-
//
|
|
78
|
+
// the reap now returns a terminal outcome (it verifies death, it does not merely
|
|
79
79
|
// fire a kill-session). Tests that only care that the reap ran may still return void.
|
|
80
80
|
reap?: (sessionName: string, socketName?: string) => ShutdownOutcome | void | Promise<ShutdownOutcome | void>;
|
|
81
81
|
sleep?: (ms: number) => Promise<unknown>;
|
|
@@ -119,7 +119,7 @@ export async function shutdownClaudeTmux(
|
|
|
119
119
|
// Session disappearance still requires verification of the captured descendants.
|
|
120
120
|
if (!hasSession(sessionName, socketName)) return stop();
|
|
121
121
|
if (!grant) {
|
|
122
|
-
//
|
|
122
|
+
// the guard that would have saved the coordinator. A turn that is live
|
|
123
123
|
// (or that we cannot positively confirm has ended) authorizes nothing, so we
|
|
124
124
|
// keep waiting and let the out-of-band reap below terminate the process if the
|
|
125
125
|
// turn never settles.
|
|
@@ -134,7 +134,7 @@ export async function shutdownClaudeTmux(
|
|
|
134
134
|
}
|
|
135
135
|
} else {
|
|
136
136
|
// `/exit` while a background task is running raises Claude's "Exit anyway /
|
|
137
|
-
// Move to background / Stay" confirmation
|
|
137
|
+
// Move to background / Stay" confirmation. Answering it is allowed ONLY
|
|
138
138
|
// through the same grant that submitted that `/exit`, and only for a dialog that
|
|
139
139
|
// was not already on screen beforehand.
|
|
140
140
|
confirmExit(grant);
|
|
@@ -143,9 +143,9 @@ export async function shutdownClaudeTmux(
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// The out-of-band backstop, and the path
|
|
146
|
+
// The out-of-band backstop, and the path prefers: it terminates the process
|
|
147
147
|
// without typing anything into the pane, so it is always reached when the guard above
|
|
148
|
-
// declines to authorize a graceful exit.
|
|
148
|
+
// declines to authorize a graceful exit. This is now a VERIFIED, escalating
|
|
149
149
|
// force-kill that returns a terminal outcome, not a fire-and-forget kill-session.
|
|
150
150
|
return stop();
|
|
151
151
|
}
|
|
@@ -156,7 +156,7 @@ async function prepareClaudeStop(sessionName: string, socketName: string | undef
|
|
|
156
156
|
at: new Date().toISOString(), name, socket, ...failure,
|
|
157
157
|
})),
|
|
158
158
|
})))(sessionName, socketName);
|
|
159
|
-
const preparePane = async () => socketName
|
|
159
|
+
const preparePane = async () => socketName !== undefined
|
|
160
160
|
? (deps.preparePaneStop ?? prepareTmuxPaneStop)({ name: sessionName, socket: socketName, owner: tmuxOwnerIdFromEnv() }).catch(() => undefined)
|
|
161
161
|
: undefined;
|
|
162
162
|
let fallback = await preparePane();
|