@humwick/runner 0.130.4 → 0.130.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -167
- 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 +8 -11
- 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.provisioned.mjs +1 -1
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +2 -2
- 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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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/plugins/claude/skills/disconnect/SKILL.md +0 -16
- package/plugins/claude/skills/pair/SKILL.md +0 -30
- package/plugins/codex/skills/disconnect/SKILL.md +0 -15
- package/plugins/codex/skills/pair/SKILL.md +0 -29
- package/src/adapters/provider-quota-harvest.ts +0 -76
|
@@ -3,7 +3,7 @@ import type { PaneObservation, ProviderStatusUpdate } from "../adapter";
|
|
|
3
3
|
import type { ClaudeClearSessionVerdict, ClaudeProbeActivity } from "./claude-session-probe";
|
|
4
4
|
import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "../rate-limit";
|
|
5
5
|
|
|
6
|
-
// LEGACY PANE-SCRAPE FALLBACK
|
|
6
|
+
// LEGACY PANE-SCRAPE FALLBACK — busy/idle is now driven primarily by
|
|
7
7
|
// Claude Code's intrinsic session probe (readClaudeSessionStatusAsync, ./claude-session-probe):
|
|
8
8
|
// `$CLAUDE_CONFIG_DIR/sessions/<pid>.json` carries a machine-readable `status` field
|
|
9
9
|
// on CC ≥2.1.119. These two pane heuristics are the SOLE fallback for pre-2.1.119 /
|
|
@@ -61,7 +61,7 @@ export function claudeBypassDialogDisplayGate(sessionName: string, observedAt: n
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// The live spinner: "✶ Perambulating… (2m 17s · ↓ 8.7k tokens)", "… (", optional hook or
|
|
64
|
-
// tool status
|
|
64
|
+
// tool status, then "[Nh ][Nm ]Ns". Anchored on the
|
|
65
65
|
// gerund ellipsis so it can't match the "… +N lines (ctrl+o to expand)" truncation marker,
|
|
66
66
|
// the idle input box, or the "/btw … without interrupting Claude's current work" hint.
|
|
67
67
|
const CLAUDE_BUSY_SPINNER_RE = /…\s*\((?:[^()\n]*·\s*)?(?:\d+h\s+)?(?:\d+m\s+)?\d+s\b/;
|
|
@@ -88,7 +88,7 @@ interface ClaudePaneRegions {
|
|
|
88
88
|
activity?: string[];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
//
|
|
91
|
+
// find the composer instead of counting rows from the bottom. Inline rendering
|
|
92
92
|
// (CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN) pads the capture with blank rows below it, and a
|
|
93
93
|
// completed "Thought for …" row can sit within any fixed window above it.
|
|
94
94
|
function claudePaneRegions(text: string): ClaudePaneRegions {
|
|
@@ -120,7 +120,7 @@ function claudeRegionsAreBusy({ footer, activity }: ClaudePaneRegions): boolean
|
|
|
120
120
|
// the pane veto works across those formats.
|
|
121
121
|
if (footer.includes("esc to interrupt")) return true;
|
|
122
122
|
if (!activity) return CLAUDE_BUSY_SPINNER_RE.test(footer) || CLAUDE_BUSY_THOUGHT_RE.test(footer);
|
|
123
|
-
// Rows rejoin so a counter Claude wrapped at a narrow width still reads
|
|
123
|
+
// Rows rejoin so a counter Claude wrapped at a narrow width still reads. A thinking
|
|
124
124
|
// row is live only as the block's head: a completed one stays above "⎿ Interrupted" or output.
|
|
125
125
|
return CLAUDE_BUSY_SPINNER_RE.test(activity.join(" ")) || CLAUDE_BUSY_THOUGHT_RE.test(claudeActivityHead(activity));
|
|
126
126
|
}
|
|
@@ -131,7 +131,7 @@ function claudeActivityHead(activity: string[]): string {
|
|
|
131
131
|
return (tip >= 0 ? activity.slice(0, tip) : activity).at(-1) ?? "";
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//
|
|
134
|
+
// persistent TUI chrome that is present at the prompt regardless of whether a
|
|
135
135
|
// conversation is loaded, so it must NEVER be picked as a pre-clear conversation anchor.
|
|
136
136
|
// (Includes the claudePaneLooksReady footer markers plus the box/hint chrome around them.)
|
|
137
137
|
const CLAUDE_CHROME_MARKERS = [
|
|
@@ -148,14 +148,14 @@ const CLAUDE_CHROME_MARKERS = [
|
|
|
148
148
|
"Tips for getting started",
|
|
149
149
|
];
|
|
150
150
|
|
|
151
|
-
//
|
|
151
|
+
// pick a distinctive line from the PRE-clear pane that belongs to the loaded
|
|
152
152
|
// conversation body (not the persistent chrome/footer), so its DISAPPEARANCE after `/clear`
|
|
153
153
|
// is positive evidence the conversation was actually wiped. claudePaneLooksReady alone can't
|
|
154
154
|
// tell "before clear" from "after clear" — its footer markers are present in both — so the
|
|
155
155
|
// self-resume barrier (waitForClaudeClearCommitted) keys off this anchor instead. Returns the
|
|
156
156
|
// longest content-rich, chrome-free line, or undefined when the pane has no such line (e.g. an
|
|
157
157
|
// already-empty conversation) — in which case the clear can't be positively confirmed and the
|
|
158
|
-
// caller falls to continuationDelivered:false so the
|
|
158
|
+
// caller falls to continuationDelivered:false so the backstop covers it.
|
|
159
159
|
export function claudeConversationAnchor(pane: string): string | undefined {
|
|
160
160
|
let best: string | undefined;
|
|
161
161
|
for (const raw of claudeConversationRows(pane)) {
|
|
@@ -175,7 +175,7 @@ const CLAUDE_BANNER_RE = /Claude Code v\d/;
|
|
|
175
175
|
// A turn opens with the user's prompt or an assistant, tool or event bullet.
|
|
176
176
|
const CLAUDE_TURN_ROW_RE = /^\s*[❯>⏺●]/;
|
|
177
177
|
|
|
178
|
-
//
|
|
178
|
+
// the rows `/clear` wipes: none of the startup header (banner, model, cwd, tip), which it
|
|
179
179
|
// repaints, and nothing from the composer down, whose status rows change on their own.
|
|
180
180
|
function claudeConversationRows(pane: string): string[] {
|
|
181
181
|
const lines = pane.trimEnd().split("\n");
|
|
@@ -186,32 +186,32 @@ function claudeConversationRows(pane: string): string[] {
|
|
|
186
186
|
return start < 0 ? [] : lines.slice(start, end);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// CLEAR-AWARE completion check for the self-resume barrier. The `/clear` keystroke is
|
|
190
190
|
// async in the Claude TUI: submitTextToTmux returns as soon as the keys are typed, and the
|
|
191
191
|
// persistent footer (claudePaneLooksReady) matches the STILL-UNCLEARED pane, so accepting the
|
|
192
|
-
// first ready match let the continuation race the async clear and get wiped
|
|
192
|
+
// first ready match let the continuation race the async clear and get wiped.
|
|
193
193
|
// A clear is treated as COMMITTED only with positive evidence: the pane is (a) not busy (the
|
|
194
194
|
// clear/redraw finished processing), (b) back at the input-ready footer, and (c) no longer shows
|
|
195
195
|
// the pre-clear conversation anchor (the loaded conversation is gone). Any of these missing →
|
|
196
196
|
// NOT committed → keep waiting (and, on timeout, the caller reports continuationDelivered:false
|
|
197
197
|
// so the backstop re-injects — uncertainty resolves toward the backstop, never toward a wipe).
|
|
198
198
|
//
|
|
199
|
-
//
|
|
199
|
+
// the anchor test above is the FALLBACK signal, not the primary one, and `currentPane`
|
|
200
200
|
// MUST be a VISIBLE-SCREEN capture (captureTmuxVisiblePaneAsync). Claude's `/clear` repaints only
|
|
201
201
|
// the visible screen, so an anchor that had already scrolled into tmux history stays in a
|
|
202
202
|
// scrollback capture forever and the barrier can never confirm a clear that plainly committed.
|
|
203
203
|
export interface ClaudeClearEvidence {
|
|
204
204
|
/**
|
|
205
|
-
*
|
|
205
|
+
* the intrinsic probe's verdict on Claude's session id (./claude-session-probe), ALREADY
|
|
206
206
|
* SETTLED by the caller across a poll interval. Default `unknown` (no probe / pre-2.1.119 /
|
|
207
207
|
* unreadable / liveness-failed / pid changed) keeps the pure anchor behavior.
|
|
208
208
|
*/
|
|
209
209
|
session?: ClaudeClearSessionVerdict;
|
|
210
|
-
/**
|
|
210
|
+
/** pre-clear pane captured AND anchorless: vacuous clear, so idle+ready
|
|
211
211
|
* suffices. Never set from a failed capture — that stays fail-closed. */
|
|
212
212
|
preClearEmpty?: boolean;
|
|
213
|
-
/**
|
|
214
|
-
* A ready pane can predate the /clear commit
|
|
213
|
+
/** the poller saw a CLAUDE_VACUOUS_CLEAR_READY_POLLS idle+ready streak.
|
|
214
|
+
* A ready pane can predate the /clear commit, and with no probe verdict this
|
|
215
215
|
* streak is the only commit evidence. Set by the poller. */
|
|
216
216
|
settledReady?: boolean;
|
|
217
217
|
}
|
|
@@ -223,7 +223,7 @@ export function claudeClearCommitted(
|
|
|
223
223
|
): boolean {
|
|
224
224
|
if (claudePaneIsBusy(currentPane)) return false;
|
|
225
225
|
if (!claudePaneLooksReady(currentPane)) return false;
|
|
226
|
-
//
|
|
226
|
+
// PRIMARY signal — machine-readable and version-robust: committing `/clear` mints a NEW
|
|
227
227
|
// Claude session (fresh sessionId + fresh transcript file), so a same-pid session rotation is
|
|
228
228
|
// direct proof the loaded conversation is gone, independent of any TUI chrome.
|
|
229
229
|
//
|
|
@@ -231,31 +231,31 @@ export function claudeClearCommitted(
|
|
|
231
231
|
// a relaxation. `unchanged` means the probe positively says the clear has not committed, so it
|
|
232
232
|
// must VETO — never let the (visible-screen-only) anchor heuristic confirm over it, e.g. when
|
|
233
233
|
// unrelated output has merely scrolled the anchor off the screen. The busy/ready gates above
|
|
234
|
-
// still gate both verdicts, so the
|
|
234
|
+
// still gate both verdicts, so the ordering property (never inject while the clear is
|
|
235
235
|
// still processing) is untouched. `unknown` is no evidence: it falls through to the anchor test,
|
|
236
|
-
// whose failure still resolves to false → the
|
|
236
|
+
// whose failure still resolves to false → the backstop re-injects.
|
|
237
237
|
if (evidence.session === "rotated") return true;
|
|
238
238
|
if (evidence.session === "unchanged") return false;
|
|
239
|
-
//
|
|
240
|
-
// refuses, and a lone ready poll may be the pre-clear footer
|
|
241
|
-
// settledReady (a long consecutive-ready streak
|
|
239
|
+
// vacuous clear, BELOW the session veto: an unchanged session still
|
|
240
|
+
// refuses, and a lone ready poll may be the pre-clear footer, so both
|
|
241
|
+
// settledReady (a long consecutive-ready streak) and preClearEmpty are required.
|
|
242
242
|
if (evidence.preClearEmpty === true && evidence.settledReady === true) return true;
|
|
243
243
|
if (!preClearAnchor) return false;
|
|
244
244
|
return !currentPane.includes(preClearAnchor);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
export interface ClaudeActivityReadOptions {
|
|
248
|
-
/**
|
|
248
|
+
/** the probe's busy assertion predates an interrupt relay sent (claudeProbeVerdict),
|
|
249
249
|
* so it may describe a turn that was cancelled. */
|
|
250
250
|
probeBusyStale?: boolean;
|
|
251
251
|
/**
|
|
252
|
-
*
|
|
252
|
+
* U2 — when the intrinsic session probe is UNAVAILABLE (pre-2.1.119 / unresolvable
|
|
253
253
|
* config-home), may a bare input-ready-looking pane be read as `idle`?
|
|
254
254
|
* true (default) — legacy pane-scrape turn-boundary detection (the monitorless turn-watch):
|
|
255
255
|
* an input-ready footer ends the turn.
|
|
256
256
|
* false — the busy BACKSTOP path: an input-ready-looking footer is NOT positive evidence a
|
|
257
257
|
* turn ended — a long foreground tool renders the exact same prompt box while fully
|
|
258
|
-
* alive
|
|
258
|
+
* alive — so with no probe to corroborate it is SILENCE, reported as
|
|
259
259
|
* `unknown` (the backstop holds the turn) rather than `idle` (which force-cancels it).
|
|
260
260
|
* This is what stops the `no-busy-observed` false-cancel from ever being reached.
|
|
261
261
|
*/
|
|
@@ -272,7 +272,7 @@ export function claudeActivityFromProbeAndPane(
|
|
|
272
272
|
if (claudeRegionsAreBusy(regions)) return "busy";
|
|
273
273
|
const ready = claudePaneLooksReady(regions.footer);
|
|
274
274
|
if (probe === "busy") {
|
|
275
|
-
// Probe-busy is authoritative
|
|
275
|
+
// Probe-busy is authoritative. Only after relay's own interrupt may a
|
|
276
276
|
// positively input-ready footer overrule it; a shelled-out or unreadable pane keeps it busy.
|
|
277
277
|
return opts.probeBusyStale && ready ? "idle" : "busy";
|
|
278
278
|
}
|
|
@@ -280,14 +280,14 @@ export function claudeActivityFromProbeAndPane(
|
|
|
280
280
|
// input-ready pane corroborates it, an ambiguous pane holds as unknown.
|
|
281
281
|
if (probe === "idle") return ready ? "idle" : "unknown";
|
|
282
282
|
// probe is undefined/unknown here (unavailable, or Claude `waiting`): the pane-scrape fallback.
|
|
283
|
-
//
|
|
283
|
+
// U2 — an input-ready footer is only trustworthy as turn-end when the caller opts in
|
|
284
284
|
// (paneFallbackIsIdle, the turn-boundary path). For the busy backstop (opt-out) it is silence,
|
|
285
285
|
// held as `unknown`, so a live foreground tool the probe cannot see is never force-cancelled.
|
|
286
286
|
if (ready && (opts.paneFallbackIsIdle ?? true)) return "idle";
|
|
287
287
|
return "unknown";
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
//
|
|
290
|
+
// detect the subscription usage-limit modal in the pane and turn it into the
|
|
291
291
|
// provider-neutral rate-limit hold (idle + blocked) that the relay's resume sweep lifts
|
|
292
292
|
// at reset. Unlike the pane model-rejection observation below, it remains a legacy status path.
|
|
293
293
|
export function claudeRateLimitStatus(text: string, sessionName?: string): ProviderStatusUpdate | null {
|
|
@@ -305,7 +305,7 @@ export function claudeRateLimitStatus(text: string, sessionName?: string): Provi
|
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
//
|
|
308
|
+
// detect the API connection-retry banner in the live terminal footer. Three stable
|
|
309
309
|
// anchors from the banner text: "Unable to connect to API", "Retrying in <N>s",
|
|
310
310
|
// "attempt <N>/<M>". Gated on the LAST N lines so agent output that merely QUOTES these
|
|
311
311
|
// strings (e.g. quoting this comment, or a discussion of retry logic) doesn't false-positive —
|
|
@@ -314,7 +314,7 @@ export function claudeRateLimitStatus(text: string, sessionName?: string): Provi
|
|
|
314
314
|
// fast fix once the new footer wording is known, and one with no correctness consequence now
|
|
315
315
|
// that this is advisory.
|
|
316
316
|
//
|
|
317
|
-
//
|
|
317
|
+
// this returns a DIAGNOSTIC, not a status. It used to return
|
|
318
318
|
// `status: "idle"` + `clear: ["subagent"]` + a blocked `conn_retry` providerState. The runner
|
|
319
319
|
// treats an idle provider-turn as the turn ENDING (runner-core setProviderStatus), which
|
|
320
320
|
// clears the work record and calls completeObservedTaskClaims(): retry chrome rendered in a
|
|
@@ -362,14 +362,14 @@ export function claudeConnectionRetryClearedObservation(sessionName?: string, no
|
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
//
|
|
365
|
+
// this returns a DIAGNOSTIC, not a status. It used to return `status: "error"` +
|
|
366
366
|
// `clear: ["provider-turn", "subagent"]` + a TERMINAL failed provider state + suppressed
|
|
367
367
|
// restart from a single pane match — reproduced live: the harmless sentence "The test fixture
|
|
368
368
|
// says: not an available model" triggered every one of those, from a pane, where the matched
|
|
369
369
|
// text may simply be an agent quoting a fixture, a code comment, or this very file.
|
|
370
370
|
//
|
|
371
371
|
// A pane match is never terminal. Only a correlated StopFailure whose native error type
|
|
372
|
-
// explicitly denotes model-not-found/unavailable (not yet wired —
|
|
372
|
+
// explicitly denotes model-not-found/unavailable (not yet wired — the Direction
|
|
373
373
|
// section) may end a turn this way. This detector only notices.
|
|
374
374
|
//
|
|
375
375
|
// Scoped to the trailing modal/composer structural block: the same positional technique
|
|
@@ -15,22 +15,22 @@ export function claudeSubagentPolicy(config: Pick<RunnerSpawnConfig, "harnessSub
|
|
|
15
15
|
|
|
16
16
|
export function claudeSubagentEnv(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile">): Record<string, string> {
|
|
17
17
|
const policy = claudeSubagentPolicy(config);
|
|
18
|
-
const enabled = writeChildEnv({}, {
|
|
18
|
+
const enabled = writeChildEnv({}, { HUMWICK_HARNESS_SUBAGENTS: harnessSubagentsEnabled(config) ? "1" : "0" }, []);
|
|
19
19
|
return policy ? writeChildEnv({}, { ...enabled,
|
|
20
20
|
CLAUDE_CODE_SUBAGENT_MODEL: policy.model,
|
|
21
21
|
CLAUDE_CODE_SUBAGENT_MODEL_FORCE: "1",
|
|
22
22
|
CLAUDE_CODE_FORK_SUBAGENT: "0",
|
|
23
23
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "0",
|
|
24
24
|
CLAUDE_CODE_DISABLE_WORKFLOWS: "1",
|
|
25
|
-
|
|
26
|
-
}) : writeChildEnv({}, { ...enabled,
|
|
25
|
+
HUMWICK_CLAUDE_SUBAGENT_CAP: "1",
|
|
26
|
+
}, []) : writeChildEnv({}, { ...enabled, HUMWICK_CLAUDE_SUBAGENT_CAP: "0" }, []);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function claudeSubagentSettings(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile" | "model">): Record<string, unknown> {
|
|
30
30
|
const policy = claudeSubagentPolicy(config);
|
|
31
31
|
if (!policy) return harnessSubagentsEnabled(config) ? {} : { disableAllHooks: false };
|
|
32
32
|
const mainFamily = claudeModelFamily(config.model);
|
|
33
|
-
// Model settings apply to both parent and children, including canonicalized aliases
|
|
33
|
+
// Model settings apply to both parent and children, including canonicalized aliases.
|
|
34
34
|
const modelSettings = mainFamily && mainFamily !== policy.model
|
|
35
35
|
? { [policy.model]: { maxEffortLevel: policy.effort, autoCompactWindow: policy.contextTokens } } : undefined;
|
|
36
36
|
return { disableAllHooks: false, ...(modelSettings ? { modelSettings } : {}), autoCompactEnabled: true };
|
|
@@ -83,11 +83,11 @@ export function configureClaudeSubagentLaunch(config: Pick<RunnerSpawnConfig, "h
|
|
|
83
83
|
if (!sources.includes("user")) throw new Error("Capped Claude profiles require user settings to load mandatory hooks");
|
|
84
84
|
const settings = extractCallerSettings([launch.args], launch.cwd)?.settings ?? {};
|
|
85
85
|
const callerEnv = settings.env as Record<string, unknown> | undefined;
|
|
86
|
-
const protectedEnv = [...Object.keys(claudeSubagentEnv(config)), "CLAUDE_CODE_SIMPLE", "CLAUDE_CODE_SAFE_MODE", "CLAUDE_CODE_AUTO_COMPACT_WINDOW", "DISABLE_AUTO_COMPACT", "
|
|
86
|
+
const protectedEnv = [...Object.keys(claudeSubagentEnv(config)), "CLAUDE_CODE_SIMPLE", "CLAUDE_CODE_SAFE_MODE", "CLAUDE_CODE_AUTO_COMPACT_WINDOW", "DISABLE_AUTO_COMPACT", "HUMWICK_CLAUDE_SKILL_SOURCES"];
|
|
87
87
|
if (callerEnv && protectedEnv.some((key) => key in callerEnv)) throw new Error("Capped Claude profiles cannot override child policy through --settings env");
|
|
88
|
-
writeChildEnv(launch.env, {
|
|
88
|
+
writeChildEnv(launch.env, { HUMWICK_CLAUDE_SKILL_SOURCES: JSON.stringify({ cwd: launch.cwd,
|
|
89
89
|
sources,
|
|
90
90
|
directories: values("--add-dir", true), plugins: values("--plugin-dir", true),
|
|
91
91
|
settings: { enabledPlugins: settings.enabledPlugins, skillOverrides: settings.skillOverrides },
|
|
92
|
-
}) });
|
|
92
|
+
}) }, []);
|
|
93
93
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A tmux command exiting 0 only proves that tmux accepted a key. Claude can still
|
|
2
2
|
// have the pasted prompt in its composer (for example when Enter lands in copy mode).
|
|
3
3
|
// Claude's UserPromptSubmit hook proves acceptance; the composer only decides whether
|
|
4
|
-
// another Enter is safe, and confirms alone only on hookless launches
|
|
4
|
+
// another Enter is safe, and confirms alone only on hookless launches.
|
|
5
5
|
import { ProviderSubmitFailure } from "../provider-submit-failure";
|
|
6
6
|
import type { ClaudePromptHookExpectation } from "./claude-prompt-hook";
|
|
7
7
|
import { claudeHookPromptMatches } from "./claude-prompt-hook";
|
|
@@ -11,7 +11,7 @@ import { IncrementalClaudeTranscriptTail, type IncrementalTranscriptPollResult }
|
|
|
11
11
|
|
|
12
12
|
export const CLAUDE_SUBMIT_MAX_ENTERS = 3;
|
|
13
13
|
export const CLAUDE_SUBMIT_HOOK_WAIT_MS = 3_000;
|
|
14
|
-
/** Extra hook wait once our Enter left the composer: a slow hook is still proof
|
|
14
|
+
/** Extra hook wait once our Enter left the composer: a slow hook is still proof. */
|
|
15
15
|
export const CLAUDE_SUBMIT_LATE_HOOK_WAIT_MS = 10_000;
|
|
16
16
|
const PASTE_RENDER_TIMEOUT_MS = 3_000;
|
|
17
17
|
const SUBMIT_SETTLE_MS = 1_200;
|
|
@@ -83,7 +83,7 @@ export class ClaudeSubmitTranscriptProbe {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async confirms(text: string, offeredAt: number, imagePaths: readonly string[] = []): Promise<boolean> {
|
|
86
|
-
// A consumption check must leave its read available to submit recovery
|
|
86
|
+
// A consumption check must leave its read available to submit recovery.
|
|
87
87
|
const result = this.recoveryPoll = (await this.poll()) ?? undefined;
|
|
88
88
|
return result?.userPrompts.some((prompt, i) =>
|
|
89
89
|
(result.userPromptOccurredAts[i] ?? -Infinity) >= offeredAt && claudeHookPromptMatches(prompt, text, imagePaths)) === true;
|
|
@@ -96,7 +96,7 @@ export class ClaudeSubmitFence {
|
|
|
96
96
|
|
|
97
97
|
async run(session: string, submit: (markPasteStarted: () => void) => Promise<unknown>, recovery: (turnStarted: boolean) => Promise<boolean>): Promise<void> {
|
|
98
98
|
// Probe here too: a dashboard prompt reaches the fence directly, and the turn that would
|
|
99
|
-
// otherwise trigger recovery may have started before the failure was recorded
|
|
99
|
+
// otherwise trigger recovery may have started before the failure was recorded.
|
|
100
100
|
if (this.uncertain.has(session) && !await this.recover(session)) {
|
|
101
101
|
throw new ProviderSubmitFailure("Claude prompt submit remains unconfirmed from an earlier attempt; inspect the TUI before retrying", false);
|
|
102
102
|
}
|
|
@@ -140,7 +140,7 @@ export interface ClaudeComposerReadDeps {
|
|
|
140
140
|
now?: () => number;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
// Hookless (degraded) composer acceptance
|
|
143
|
+
// Hookless (degraded) composer acceptance: empty only after a readable empty composer
|
|
144
144
|
// has lasted SUBMIT_SETTLE_MS from its FIRST empty frame, with no input re-shown meanwhile.
|
|
145
145
|
// A torn frame neither confirms nor withdraws, but the window must end on a readable empty
|
|
146
146
|
// frame; an unsettled or unreadable end is "unreadable", so no key goes into a dialog.
|
|
@@ -174,7 +174,7 @@ async function readComposer(deps: ClaudeComposerReadDeps, timeoutMs: number): Pr
|
|
|
174
174
|
|
|
175
175
|
// Claude renders composer placeholders ("Press up to edit queued messages" after a
|
|
176
176
|
// mid-turn submit, "Try …") dim; typed and pasted input never is. Reading a styled
|
|
177
|
-
// capture keeps a placeholder from looking like an unsubmitted prompt
|
|
177
|
+
// capture keeps a placeholder from looking like an unsubmitted prompt.
|
|
178
178
|
const TERMINAL_ESCAPE = /\x1b\[([0-9;:]*)m|\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
179
179
|
|
|
180
180
|
function sgrDim(params: string, dim: boolean): boolean {
|
|
@@ -2,7 +2,7 @@ import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
|
2
2
|
import { envPipeSource, prepareEnvPipe, providerSecretEnvKeys } from "@humwick/sdk/child-env";
|
|
3
3
|
import { PROVIDER_SCRATCH_ENV_KEYS } from "@humwick/sdk/provider-scratch";
|
|
4
4
|
// Claude tmux transport plumbing (extracted from ./claude for the file-size
|
|
5
|
-
// ratchet
|
|
5
|
+
// ratchet): session/socket naming, pane capture, input-ready gating,
|
|
6
6
|
// paste/submit injection, launcher-script materialization, and env-key selection.
|
|
7
7
|
// The adapter (./claude) keeps the lifecycle decisions; this module owns the
|
|
8
8
|
// mechanical tmux interactions they ride on. Public symbols are re-exported from
|
|
@@ -43,7 +43,7 @@ export function seedClaudeInitialPrompt(args: string[], prompt: string, options:
|
|
|
43
43
|
if (!options.headless || options.lifecycle !== "transient") {
|
|
44
44
|
throw new Error("oversized Claude initial prompt requires a transient headless launch; refusing a racy or role-changing fallback");
|
|
45
45
|
}
|
|
46
|
-
if (!options.scratchDir) throw new Error("oversized Claude initial prompt requires
|
|
46
|
+
if (!options.scratchDir) throw new Error("oversized Claude initial prompt requires HUMWICK_SCRATCH_DIR");
|
|
47
47
|
const promptFile = join(options.scratchDir, "claude-initial-assignment.md");
|
|
48
48
|
const exitCodeFile = join(options.scratchDir, "claude-initial-exit-code");
|
|
49
49
|
args.push("-p", "--input-format", "text");
|
|
@@ -60,7 +60,7 @@ export function tmuxSessionName(prefix: string, instanceId: string, label?: stri
|
|
|
60
60
|
// separates the session/window/pane portions of a target). All later `-t` lookups
|
|
61
61
|
// must use the rendered name or `has-session` misses the session tmux just created.
|
|
62
62
|
// Delegates to the shared SDK renderer so the runner and orchestrator (which must match
|
|
63
|
-
// the rendered name when resolving a shutdown target,
|
|
63
|
+
// the rendered name when resolving a shutdown target, RC-1) can never diverge.
|
|
64
64
|
export function tmuxSessionTargetName(sessionName: string): string {
|
|
65
65
|
return renderTmuxSessionTarget(sessionName);
|
|
66
66
|
}
|
|
@@ -76,7 +76,7 @@ export function tmuxSocketName(sessionName: string): string {
|
|
|
76
76
|
return socketName;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Previous socket selector, used only for one-way cleanup when a runner first
|
|
80
80
|
// starts after upgrading. Do not assert its path length: an overlong legacy
|
|
81
81
|
// name is exactly what the new fixed-size selector is designed to escape.
|
|
82
82
|
export function legacyTmuxSocketName(sessionName: string): string {
|
|
@@ -88,7 +88,7 @@ export function createOrReuseTmuxSession(sessionName: string, socketName: string
|
|
|
88
88
|
spawnSyncBounded(tmuxCommand(socketName, "kill-session", "-t", targetSessionName), {
|
|
89
89
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
90
90
|
});
|
|
91
|
-
// Upgrade cleanup: a
|
|
91
|
+
// Upgrade cleanup: a previous runner used the readable session name as its
|
|
92
92
|
// selector. Best-effort removal prevents a legacy pane surviving beside its
|
|
93
93
|
// replacement; an overlong legacy selector can fail here harmlessly.
|
|
94
94
|
spawnSyncBounded(tmuxCommand(legacyTmuxSocketName(sessionName), "kill-session", "-t", targetSessionName), {
|
|
@@ -105,7 +105,7 @@ export function createOrReuseTmuxSession(sessionName: string, socketName: string
|
|
|
105
105
|
throw new Error(`tmux session creation failed: ${stderr || (result.signalCode ? `signal ${result.signalCode}` : `exit code ${result.exitCode}`)}`);
|
|
106
106
|
}
|
|
107
107
|
if (result.exitCode === 0) promptSubmitFence.clear(promptSubmitSessionKey(targetSessionName, socketName));
|
|
108
|
-
//
|
|
108
|
+
// stamp the owning orchestrator's state-home id (passed to this runner via
|
|
109
109
|
// env) onto the session so the orchestrator's wedged-session reaper reaps ONLY the
|
|
110
110
|
// sessions it positively owns, never a co-tenant orchestrator's on the same host.
|
|
111
111
|
setTmuxSessionOwner(targetSessionName, tmuxOwnerIdFromEnv(), socketName);
|
|
@@ -135,7 +135,7 @@ export function tmuxSessionCreationFailureIsReusable(
|
|
|
135
135
|
&& hasSession(tmuxSessionTargetName(sessionName), socketName);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
//
|
|
138
|
+
// stamp the owning orchestrator's state-home id onto the tmux session as a
|
|
139
139
|
// user-option. The orchestrator's wedged-session reaper reads it back and reaps ONLY
|
|
140
140
|
// sessions whose owner id matches its own, so it can never reap a session belonging
|
|
141
141
|
// to a different orchestrator that happens to share the UID + tmux prefix on this
|
|
@@ -166,7 +166,7 @@ export async function confirmTmuxSessionGone(
|
|
|
166
166
|
sessionName: string,
|
|
167
167
|
socketName?: string,
|
|
168
168
|
delayMs = 250,
|
|
169
|
-
//
|
|
169
|
+
// row 12 — off the event loop via the tri-state `tmuxSessionPresence` (review
|
|
170
170
|
// HIGH-2). Death is confirmed ONLY when BOTH the immediate and the delayed read are the
|
|
171
171
|
// positive `absent` (tmux exited 1); a `present` OR an `unreadable` (timeout/abort on a
|
|
172
172
|
// wedged-but-live server) at either point means NOT gone, so a slow read can never be
|
|
@@ -195,15 +195,15 @@ export function captureTmuxPane(sessionName: string, socketName?: string): strin
|
|
|
195
195
|
return result.stdout.toString();
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
//
|
|
198
|
+
// exit-dialog confirmation used to live here as
|
|
199
199
|
// `confirmClaudeExitDialogIfPresent(sessionName, socketName)` — an Enter writer that
|
|
200
|
-
// any caller holding a session name could invoke.
|
|
200
|
+
// any caller holding a session name could invoke. Removed it: taking a session
|
|
201
201
|
// name meant nothing in the type system stopped a pane detector from calling it, and
|
|
202
202
|
// the production kill was exactly that Enter answering an "Exit anyway" modal on a
|
|
203
203
|
// live pane. It now lives in ./claude-exit, where both it and the `/exit` writer take
|
|
204
204
|
// an unforgeable one-shot ClaudeExitGrant instead.
|
|
205
205
|
|
|
206
|
-
//
|
|
206
|
+
// review BLOCKER + round-3 MED — this readiness poller runs on the hot delivery path
|
|
207
207
|
// (every deliver / deliverInitialPrompt / monitor self-resume settle) and used to call the
|
|
208
208
|
// SYNCHRONOUS tmuxHasSession + captureTmuxPane every 200ms; a single wedged tmux call froze the
|
|
209
209
|
// whole runner event loop for the full 10s deadline. Both reads now go OFF the loop and use the
|
|
@@ -243,11 +243,11 @@ export async function waitForClaudeInputReady(sessionName: string, timeoutMs = C
|
|
|
243
243
|
|
|
244
244
|
export const CLAUDE_CLEAR_COMMIT_POLL_MS = 200;
|
|
245
245
|
|
|
246
|
-
/**
|
|
246
|
+
/** consecutive idle+ready polls (~1s) a vacuous clear needs when the probe can't attest. */
|
|
247
247
|
export const CLAUDE_VACUOUS_CLEAR_READY_POLLS = 5;
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
|
-
*
|
|
250
|
+
* the capture scope the clear-commit barrier reads, pinned as a named export so the
|
|
251
251
|
* invariant is assertable. It MUST be the visible-screen capture: `/clear` repaints only the
|
|
252
252
|
* visible screen (no `ESC[3J`), so a scrollback capture keeps returning the wiped conversation —
|
|
253
253
|
* and with it the anchor — for the entire timeout, making a committed clear unconfirmable.
|
|
@@ -260,23 +260,23 @@ export interface ClaudeClearCommitDeps {
|
|
|
260
260
|
capture?: (sessionName: string, socketName?: string, opts?: TmuxReadOpts) => string | Promise<string>;
|
|
261
261
|
presence?: (sessionName: string, socketName?: string, opts?: TmuxReadOpts) => TmuxSessionPresence | Promise<TmuxSessionPresence>;
|
|
262
262
|
sleep?: (ms: number) => Promise<unknown>;
|
|
263
|
-
/**
|
|
263
|
+
/** the probe reading taken immediately BEFORE the `/clear` submit (null = unavailable). */
|
|
264
264
|
preClearSession?: ClaudeClearSessionReading | null;
|
|
265
|
-
/**
|
|
265
|
+
/** re-read it each poll, pid-pinned by the caller. Absent → anchor-only (legacy). */
|
|
266
266
|
readSession?: () => Promise<ClaudeClearSessionReading | null>;
|
|
267
|
-
/**
|
|
267
|
+
/** true only when `preClearPane` is a successful capture (not the "" fallback
|
|
268
268
|
* left by a throw). Lets a provably-empty pre-clear pane confirm via idle+ready. */
|
|
269
269
|
preClearCaptured?: boolean;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
//
|
|
272
|
+
// CLEAR-AWARE settle barrier for the self-resume clear+inject coupling. Unlike
|
|
273
273
|
// waitForClaudeInputReady (which returns on the first persistent-footer match — present BOTH
|
|
274
274
|
// before and after `/clear`, the F2 race), this requires POSITIVE evidence the `/clear` actually
|
|
275
275
|
// committed: it snapshots a distinctive pre-clear conversation anchor and only reports success
|
|
276
276
|
// once the pane is idle, input-ready, AND that anchor is gone (the loaded conversation was wiped).
|
|
277
277
|
// Returns a BOOLEAN rather than throwing: `true` = clear positively confirmed committed; `false` =
|
|
278
278
|
// timed out / session vanished / capture failed / no anchor derivable (uncertain). The caller MUST
|
|
279
|
-
// treat `false` as "not confirmed" and report continuationDelivered:false so the
|
|
279
|
+
// treat `false` as "not confirmed" and report continuationDelivered:false so the backstop
|
|
280
280
|
// re-injects — never as a wipe. A settle timeout is therefore surfaced, not swallowed.
|
|
281
281
|
export async function waitForClaudeClearCommitted(
|
|
282
282
|
sessionName: string,
|
|
@@ -285,12 +285,12 @@ export async function waitForClaudeClearCommitted(
|
|
|
285
285
|
socketName?: string,
|
|
286
286
|
deps: ClaudeClearCommitDeps = {},
|
|
287
287
|
): Promise<boolean> {
|
|
288
|
-
//
|
|
288
|
+
// review — same off-loop + tri-state + ABSOLUTE-deadline treatment as
|
|
289
289
|
// waitForClaudeInputReady (this poller also ran sync captureTmuxPane on the clear path, and had
|
|
290
290
|
// the same between-calls-only deadline, round-3 MED). A positive `absent` returns false (clear
|
|
291
|
-
// not confirmed → the
|
|
291
|
+
// not confirmed → the backstop re-injects, the safe outcome); an `unreadable` read or a
|
|
292
292
|
// transient capture failure retries until the deadline instead of blocking or giving up early.
|
|
293
|
-
//
|
|
293
|
+
// the VISIBLE screen, never the `-S -80` scrollback window: `/clear` repaints only the
|
|
294
294
|
// visible screen, so a scrollback capture keeps returning the wiped conversation (and with it the
|
|
295
295
|
// anchor) for the whole timeout. That single mismatch made a committed clear unconfirmable on any
|
|
296
296
|
// pane with history — proven live: the clear-inject at 22:15:10.185Z minted its new Claude
|
|
@@ -302,18 +302,18 @@ export async function waitForClaudeClearCommitted(
|
|
|
302
302
|
const sleep = deps.sleep ?? Bun.sleep;
|
|
303
303
|
const anchor = claudeConversationAnchor(preClearPane);
|
|
304
304
|
const { preClearSession, readSession } = deps;
|
|
305
|
-
//
|
|
305
|
+
// a positively-captured pane with no anchor held no conversation, so the
|
|
306
306
|
// clear is vacuous. A failed capture ("") also yields no anchor but must stay
|
|
307
307
|
// fail-closed (preClearCaptured false). Confirmation still needs the settle below.
|
|
308
308
|
const preClearEmpty = deps.preClearCaptured === true && anchor === undefined;
|
|
309
|
-
//
|
|
309
|
+
// with no probe verdict a ready pane may predate the commit,
|
|
310
310
|
// so a vacuous clear needs a CLAUDE_VACUOUS_CLEAR_READY_POLLS streak. A failed capture,
|
|
311
311
|
// a busy/non-ready pane or an `unchanged` probe reading resets it.
|
|
312
312
|
let readyStreak = 0;
|
|
313
|
-
//
|
|
313
|
+
// the new session is minted ~112ms BEFORE the TUI finishes repainting (measured on the
|
|
314
314
|
// live repro), and inside that window an un-cleared pane still reads non-busy + input-ready. So
|
|
315
315
|
// a rotation only counts once it has been observed on TWO CONSECUTIVE polls, which lands the
|
|
316
|
-
// confirmation after the repaint rather than inside the
|
|
316
|
+
// confirmation after the repaint rather than inside the injection window. One poll
|
|
317
317
|
// interval (200ms) out of a 10s budget.
|
|
318
318
|
let rotationSeen = false;
|
|
319
319
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -331,7 +331,7 @@ export async function waitForClaudeClearCommitted(
|
|
|
331
331
|
pane = await capture(sessionName, socketName, readOpts);
|
|
332
332
|
} catch {
|
|
333
333
|
readyStreak = 0;
|
|
334
|
-
//
|
|
334
|
+
// round-4 MED — clamp the poll sleep to the remaining budget so a wait started
|
|
335
335
|
// with < CLAUDE_CLEAR_COMMIT_POLL_MS left can't overshoot the absolute deadline.
|
|
336
336
|
await sleep(Math.min(CLAUDE_CLEAR_COMMIT_POLL_MS, Math.max(0, deadline - Date.now())));
|
|
337
337
|
continue;
|
|
@@ -344,7 +344,7 @@ export async function waitForClaudeClearCommitted(
|
|
|
344
344
|
readyStreak = verdict !== "unchanged" && !claudePaneIsBusy(pane) && claudePaneLooksReady(pane) ? readyStreak + 1 : 0;
|
|
345
345
|
const settledReady = readyStreak >= CLAUDE_VACUOUS_CLEAR_READY_POLLS;
|
|
346
346
|
if (claudeClearCommitted(anchor, pane, { session: settled, preClearEmpty, settledReady })) return true;
|
|
347
|
-
//
|
|
347
|
+
// round-4 MED — same clamp on the steady-state poll.
|
|
348
348
|
await sleep(Math.min(CLAUDE_CLEAR_COMMIT_POLL_MS, Math.max(0, deadline - Date.now())));
|
|
349
349
|
}
|
|
350
350
|
return false;
|
|
@@ -353,7 +353,7 @@ export async function waitForClaudeClearCommitted(
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
//
|
|
356
|
+
// row 16 — the provider input transport (up to 50 spawns per delivered payload).
|
|
357
357
|
// Wave 2a moves every tmux subprocess here OFF the event loop via the offload runner, and the
|
|
358
358
|
// payload rides on STDIN (`load-buffer -`) rather than in argv.
|
|
359
359
|
//
|
|
@@ -367,7 +367,7 @@ export async function waitForClaudeClearCommitted(
|
|
|
367
367
|
// User prompts opt into visible-composer confirmation. Context commands retain their own
|
|
368
368
|
// command-specific commit barriers; the tmux transport still sends those once.
|
|
369
369
|
async function deleteTmuxBuffer(bufferName: string, socketName?: string): Promise<void> {
|
|
370
|
-
// Best-effort: a failed delete must never fail (or mask) the delivery outcome.
|
|
370
|
+
// Best-effort: a failed delete must never fail (or mask) the delivery outcome. Review
|
|
371
371
|
// LOW — always runs, so a buffer loaded-but-not-cleaned (e.g. a paste timeout) can't leak.
|
|
372
372
|
await runTmuxInput("tmux-delete-buffer", tmuxCommand(socketName, "delete-buffer", "-b", bufferName)).catch(() => {});
|
|
373
373
|
}
|
|
@@ -387,7 +387,7 @@ export function releaseClaudeSubmitFence(sessionName: string, socketName?: strin
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
export async function submitTextToTmux(sessionName: string, text: string, socketName?: string, options: { confirmSubmit?: boolean; transcriptPath?: string; resolveTranscriptPath?: () => Promise<string | undefined>; promptHooks?: ClaudePromptHookLedger; imagePaths?: readonly string[]; inputObserver?: ProviderInputObserver } = {}): Promise<void> {
|
|
390
|
-
const bufferName = `
|
|
390
|
+
const bufferName = `humwick-${crypto.randomUUID()}`;
|
|
391
391
|
const paste = async () => {
|
|
392
392
|
try {
|
|
393
393
|
// Load via stdin so large prompts never hit the platform argv limit.
|
|
@@ -451,7 +451,7 @@ export async function submitTextToTmux(sessionName: string, text: string, socket
|
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
453
453
|
// Submit the pasted input with a SINGLE send-keys C-m: one send, throw-on-failure, no retry —
|
|
454
|
-
// delivery-acknowledgment semantics identical to main (see the header note +
|
|
454
|
+
// delivery-acknowledgment semantics identical to main (see the header note +).
|
|
455
455
|
await paste();
|
|
456
456
|
await Bun.sleep(CLAUDE_TMUX_SUBMIT_DELAY_MS);
|
|
457
457
|
options.inputObserver?.offered(Date.now(), false);
|
|
@@ -461,7 +461,7 @@ export async function submitTextToTmux(sessionName: string, text: string, socket
|
|
|
461
461
|
// The script keeps the provider command out of tmux argv. Environment values
|
|
462
462
|
// arrive through a private FIFO; only the recovery identity is written here.
|
|
463
463
|
// `gateFifo`, when given, is read from (blocking) before the real command execs
|
|
464
|
-
//
|
|
464
|
+
// the caller creates the FIFO before the tmux session exists, attaches
|
|
465
465
|
// `pipe-pane` once the pane is up, then writes to the FIFO to release the gate.
|
|
466
466
|
// That guarantees no provider output can be produced before pipe-pane is capturing
|
|
467
467
|
// it — a process that dies in milliseconds previously produced a 0-byte log because
|
|
@@ -473,10 +473,10 @@ export function writeLauncherScript(sessionName: string, env: Record<string, str
|
|
|
473
473
|
const scriptPath = launcherScriptPathForSession(sessionName);
|
|
474
474
|
const launcherDir = dirname(scriptPath); mkdirSync(launcherDir, { recursive: true, mode: 0o700 }); chmodSync(launcherDir, 0o700);
|
|
475
475
|
const exports = Object.entries(env)
|
|
476
|
-
.filter(([key]) => key === "
|
|
476
|
+
.filter(([key]) => key === "HUMWICK_ID")
|
|
477
477
|
.map(([key, value]) => `export ${shellEnvKey(key)}=${shellQuote(value)}`)
|
|
478
478
|
.join("\n");
|
|
479
|
-
//
|
|
479
|
+
// exports only ADD to the tmux server's inherited environment; a key that must
|
|
480
480
|
// not reach the pane (an ambient credential a second-subscription launch must shed)
|
|
481
481
|
// needs an explicit unset, absence from `env` is not enough.
|
|
482
482
|
const unsets = unsetKeys.map((key) => `unset ${shellEnvKey(key)}`).join("\n");
|
|
@@ -484,7 +484,7 @@ export function writeLauncherScript(sessionName: string, env: Record<string, str
|
|
|
484
484
|
const stdin = stdinFile ? `exec < ${shellQuote(stdinFile)}\n` : "";
|
|
485
485
|
const codeFile = exitCodeFile ?? `${scriptPath}.exit-code`;
|
|
486
486
|
// Interactive Claude must own pane_pid for its session probe. execfail lets a
|
|
487
|
-
// failed exec return to EXIT; print mode keeps a parent to record completion
|
|
487
|
+
// failed exec return to EXIT; print mode keeps a parent to record completion.
|
|
488
488
|
const recordExit = `trap ${shellQuote(`exit_code=$?; printf '%s' "$exit_code" > ${shellQuote(codeFile)}; exit "$exit_code"`)} EXIT`;
|
|
489
489
|
const command = exitCodeFile ? `( ${shellCmd} )` : `exec ${shellCmd}`;
|
|
490
490
|
writeFileSync(scriptPath, `#!/usr/bin/env bash\numask 077\nshopt -s execfail\nrm -f -- ${shellQuote(codeFile)} ${shellQuote(`${codeFile}.stderr`)}\n${recordExit}\nexec 2> >(tee ${shellQuote(`${codeFile}.stderr`)} >&2)\n${exports ? `${exports}\n` : ""}${Object.keys(env).length ? `${envPipeSource()}\n` : ""}${unsets ? `${unsets}\n` : ""}rm -f -- "$0"\n${gate}${stdin}${command}\n`, { mode: 0o700 });
|
|
@@ -517,7 +517,7 @@ export async function releaseLauncherGate(gateFifo: string, timeoutMs = 5_000):
|
|
|
517
517
|
try { proc.kill(); } catch {}
|
|
518
518
|
}
|
|
519
519
|
} catch {
|
|
520
|
-
// best-effort — a stuck launcher gate is no worse than the
|
|
520
|
+
// best-effort — a stuck launcher gate is no worse than the previous race.
|
|
521
521
|
} finally {
|
|
522
522
|
try { rmSync(gateFifo, { force: true }); } catch {}
|
|
523
523
|
}
|
|
@@ -529,14 +529,14 @@ function shellEnvKey(key: string): string {
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
export function tmuxEnvKeys(env: Record<string, string>, providerEnv: Record<string, string>, provider = "claude"): string[] {
|
|
532
|
-
//
|
|
532
|
+
// PATH is always carried through, not just HUMWICK_/CLAUDE_ vars, so the
|
|
533
533
|
// launcher script's exported session PATH (built by buildSpawnArgs) reaches
|
|
534
534
|
// the tmux pane instead of whatever ambient PATH the tmux server happened to inherit.
|
|
535
|
-
//
|
|
535
|
+
// likewise the agent's private TMPDIR/TMP/TEMP: without them the pane got the tmux
|
|
536
536
|
// server's host-shared TMPDIR while only CLAUDE_CODE_TMPDIR was per-agent.
|
|
537
537
|
const keys = new Set<string>(["PATH", ...PROVIDER_SCRATCH_ENV_KEYS, ...providerSecretEnvKeys(provider)]);
|
|
538
538
|
for (const key of Object.keys(env)) {
|
|
539
|
-
if (
|
|
539
|
+
if (key.startsWith("HUMWICK_") || key.startsWith("CLAUDE_")) {
|
|
540
540
|
keys.add(key);
|
|
541
541
|
}
|
|
542
542
|
}
|
|
@@ -558,12 +558,12 @@ export function buildClaudeTmuxArgs(config: RunnerSpawnConfig, spawnArgs: SpawnA
|
|
|
558
558
|
|
|
559
559
|
const envKeys = tmuxEnvKeys(spawnArgs.env, config.providerConfig.env, provider);
|
|
560
560
|
const env: Record<string, string> = {};
|
|
561
|
-
for (const key of envKeys) if (relayEnv(spawnArgs.env, key) !== undefined) writeChildEnv(env, { [key]: relayEnv(spawnArgs.env, key)! });
|
|
561
|
+
for (const key of envKeys) if (relayEnv(spawnArgs.env, key) !== undefined) writeChildEnv(env, { [key]: relayEnv(spawnArgs.env, key)! }, []);
|
|
562
562
|
|
|
563
563
|
// Only gate the launch behind pipe-pane attachment when there's a log file to
|
|
564
|
-
// protect
|
|
565
|
-
// read
|
|
566
|
-
const gateFifo = relayEnv(spawnArgs.env, "
|
|
564
|
+
// protect — no point serializing dev/interactive spawns that never
|
|
565
|
+
// read HUMWICK_LOG_FILE.
|
|
566
|
+
const gateFifo = relayEnv(spawnArgs.env, "HUMWICK_LOG_FILE") ? launcherGatePathForSession(sessionName) : undefined;
|
|
567
567
|
|
|
568
568
|
// tmux new-session has a hard command-length limit that includes every argv item,
|
|
569
569
|
// including `-e KEY=value`. The script holds the command; its private FIFO
|