@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
package/src/control-server.ts
CHANGED
|
@@ -13,22 +13,22 @@ import { bestEffortDeletePendingPermissionRequest, bestEffortListPendingPermissi
|
|
|
13
13
|
// decision before the provider abandons the hook.
|
|
14
14
|
const PERMISSION_WAIT_MS = 880_000;
|
|
15
15
|
// Distinct from a human dismissal ("Dismissed/Denied from Humwick dashboard") so the
|
|
16
|
-
// agent can tell "nobody answered in time" (retriable) from "the human said no"
|
|
16
|
+
// agent can tell "nobody answered in time" (retriable) from "the human said no".
|
|
17
17
|
function permissionTimeoutReason(view?: Pick<InteractivePrompt, "toolName" | "title">): string {
|
|
18
18
|
const tool = view?.toolName?.trim() || view?.title?.replace(/^Approve\s+/i, "").trim() || "provider tool";
|
|
19
19
|
return `Permission request for ${tool} timed out after ~15 minutes without a coordinator decision; the tool was denied (timeout, not a dismissal). Re-ask if you still need it.`;
|
|
20
20
|
}
|
|
21
21
|
const PERMISSION_STOP_REASON = "The runner restarted while this permission request was pending; the original hook is no longer connected.";
|
|
22
22
|
const RESOLVED_APPROVAL_LIMIT = 1024;
|
|
23
|
-
//
|
|
23
|
+
// the immediate PreToolUse response for a "questions" prompt (see the deny-then-hold
|
|
24
24
|
// branch in handlePermissionRequest below). Bridges the hand-off so the model doesn't read a
|
|
25
25
|
// bare denial and re-ask — the real answer follows shortly as an injected user prompt.
|
|
26
26
|
const PENDING_QUESTION_DENY_REASON =
|
|
27
|
-
"The user is answering this question directly via
|
|
27
|
+
"The user is answering this question directly via Humwick, not through this tool call — do not re-ask it. Their answer will follow as your next message; wait for it and continue from there.";
|
|
28
28
|
|
|
29
29
|
// A hook that failed in a way it could not handle itself reports here so the
|
|
30
|
-
// failure is never silent
|
|
31
|
-
// log; Phase 2
|
|
30
|
+
// failure is never silent. Phase 1 logs it FATAL to the per-agent
|
|
31
|
+
// log; Phase 2 will additionally route it through the runner outbox to the
|
|
32
32
|
// server.
|
|
33
33
|
export interface HookFatalReport {
|
|
34
34
|
hook: string;
|
|
@@ -58,7 +58,7 @@ export interface ControlServer {
|
|
|
58
58
|
export interface ControlServerOptions {
|
|
59
59
|
onStatus(status: ProviderStatusEvent): void;
|
|
60
60
|
onTerminalAttachSpec?(): Promise<TerminalAttachSpec>;
|
|
61
|
-
//
|
|
61
|
+
// the CURRENT non-secret attachment generation (derived from the provider's active
|
|
62
62
|
// session id), read by the orchestrator before terminal input and on stream connect/
|
|
63
63
|
// reconnect to detect a session rotation. Returns ONLY that digest — never the session id,
|
|
64
64
|
// never a credential. Absent handler ⇒ endpoint reports no generation, which the
|
|
@@ -72,25 +72,25 @@ export interface ControlServerOptions {
|
|
|
72
72
|
onSessionTurn?(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean; toolName?: string; toolInput?: unknown }): Promise<{ reasoningSettled: boolean; toolUseOccurredAt?: number } | void>;
|
|
73
73
|
onPermissionResolved?(input: ResolvedPermissionPrompt): Promise<void> | void;
|
|
74
74
|
onPermissionAbandoned?(input: PendingPermissionRequestRecord & { reason: string; decisionReason: ProviderPermissionDecisionReason }): Promise<void> | void;
|
|
75
|
-
//
|
|
75
|
+
// a "questions" (AskUserQuestion) prompt's own PreToolUse hook is answered with `deny`
|
|
76
76
|
// immediately (see the branch in handlePermissionRequest) instead of held open for the human,
|
|
77
77
|
// so there is no live HTTP request left to answer once the decision resolves. This delivers
|
|
78
78
|
// that resolution instead: `text` is the prompt to inject as a normal user turn.
|
|
79
79
|
onAnswerInject?(input: { approvalId: string; text?: string }): Promise<void> | void;
|
|
80
|
-
//
|
|
80
|
+
// files a "questions" prompt as a durable operator decision; undefined = not filed, so the
|
|
81
81
|
// question falls back to the live deny-then-hold relay above.
|
|
82
82
|
onQuestionDecision?(input: OperatorDecisionFileInput): Promise<{ id: number } | undefined>;
|
|
83
83
|
// A provider user-prompt hook hands over text typed directly into the session
|
|
84
84
|
// so the runner can mirror it into dashboard chat.
|
|
85
85
|
onUserPrompt?(input: { prompt: string; transcriptPath?: string; promptId?: string; firedAt?: number }): Promise<void>;
|
|
86
|
-
// A session-start hook checks in with its fire time, proving the hook path works
|
|
86
|
+
// A session-start hook checks in with its fire time, proving the hook path works.
|
|
87
87
|
onHookCheck?(input: { firedAt?: number }): void;
|
|
88
88
|
onToolUse?(input: unknown, signal: AbortSignal): Promise<string>;
|
|
89
89
|
// A provider session-boundary hook signals an imminent context reset or
|
|
90
90
|
// termination so the runner can run end-of-session work before the operation.
|
|
91
91
|
onSessionBoundary?(input: { reason?: string; transcriptPath?: string }): Promise<void>;
|
|
92
92
|
onSessionStart?(input: { source?: string; providerSessionId?: string }): string | undefined;
|
|
93
|
-
// Phase 1 observability
|
|
93
|
+
// Phase 1 observability: a hook reporting an unhandled failure. The
|
|
94
94
|
// control server already logs it FATAL; this is the seam for Phase 2 to also
|
|
95
95
|
// surface it to the server via the runner outbox.
|
|
96
96
|
onHookFatal?(report: HookFatalReport): void;
|
|
@@ -100,7 +100,7 @@ export interface ControlServerOptions {
|
|
|
100
100
|
instanceId?: string;
|
|
101
101
|
pendingPermissionStore?: PendingPermissionStore;
|
|
102
102
|
permissionPromptHandler?: ProviderPermissionPromptHandler;
|
|
103
|
-
//
|
|
103
|
+
// the agent runs in `open` approval mode: answer every permission request at once.
|
|
104
104
|
autoApprovePermissions?: boolean;
|
|
105
105
|
providerSourceId?: string;
|
|
106
106
|
stopObligationPath?: string;
|
|
@@ -115,7 +115,7 @@ export function startControlServer(options: ControlServerOptions): ControlServer
|
|
|
115
115
|
const pendingPermissionRequests = new Map<string, PendingPermissionWaiter>();
|
|
116
116
|
// approvalIds already resolved, so a duplicate decision (e.g. bus replay after a
|
|
117
117
|
// reconnect, or both the PreToolUse and PermissionRequest hooks racing one answer)
|
|
118
|
-
// is an idempotent no-op instead of a silent command failure
|
|
118
|
+
// is an idempotent no-op instead of a silent command failure.
|
|
119
119
|
const resolvedApprovals = new Set<string>();
|
|
120
120
|
let server!: Server<MonitorSocketData>;
|
|
121
121
|
recoverStoredPermissionRequests(options);
|
|
@@ -133,7 +133,7 @@ export function startControlServer(options: ControlServerOptions): ControlServer
|
|
|
133
133
|
if (url.pathname === "/terminal/attach-spec" && req.method === "GET") {
|
|
134
134
|
if (!options.onTerminalAttachSpec) return Response.json({ error: "terminal attach is unavailable" }, { status: 404 });
|
|
135
135
|
return options.onTerminalAttachSpec()
|
|
136
|
-
//
|
|
136
|
+
// round 1 (review A) — strip any credential-bearing env record from the body:
|
|
137
137
|
// credentials travel ONLY by runner-staged env file (spec.envFile), so nothing
|
|
138
138
|
// secret ever serializes into this HTTP response.
|
|
139
139
|
.then((spec) => {
|
|
@@ -144,7 +144,7 @@ export function startControlServer(options: ControlServerOptions): ControlServer
|
|
|
144
144
|
.catch((error) => Response.json({ error: errMessage(error) }, { status: 409 }));
|
|
145
145
|
}
|
|
146
146
|
if (url.pathname === "/terminal/generation" && req.method === "GET") {
|
|
147
|
-
//
|
|
147
|
+
// deliberately returns ONLY the non-secret generation digest. No session id,
|
|
148
148
|
// no password, no attach detail: this endpoint exists so the orchestrator can detect
|
|
149
149
|
// staleness, and anything more would widen the secret surface for no benefit.
|
|
150
150
|
if (!options.onTerminalGeneration) return Response.json({});
|
|
@@ -279,7 +279,7 @@ export function startControlServer(options: ControlServerOptions): ControlServer
|
|
|
279
279
|
const pending = pendingPermissionRequests.get(input.approvalId);
|
|
280
280
|
if (!pending) {
|
|
281
281
|
// Idempotent: a duplicate decision for an already-resolved approval is a
|
|
282
|
-
// benign no-op, not a dropped answer that fails the command
|
|
282
|
+
// benign no-op, not a dropped answer that fails the command.
|
|
283
283
|
return resolvedApprovals.has(input.approvalId);
|
|
284
284
|
}
|
|
285
285
|
clearTimeout(pending.timer);
|
|
@@ -323,14 +323,14 @@ function replyObligationSummary(obligations: ReplyObligation[]): Record<string,
|
|
|
323
323
|
pending: true,
|
|
324
324
|
count: obligations.length,
|
|
325
325
|
obligation,
|
|
326
|
-
reason: `Pending
|
|
326
|
+
reason: `Pending Humwick response for message #${obligation.messageId} from ${obligation.from}${subject}. If it still needs an answer, use ${relayReplyActionText(obligation.messageId)}. If multiple delivered messages are part of one current request, answer once to the latest relevant message. If the useful response was already delivered through Relay, do not send a separate status-only confirmation.`,
|
|
327
327
|
};
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
function replyObligationStopDecision(obligations: ReplyObligation[]): Record<string, unknown> {
|
|
331
|
-
//
|
|
331
|
+
// never block the turn to nag for a `user` obligation: the operator already receives
|
|
332
332
|
// this turn via the session mirror, so an explicit /reply double-delivers. Only agent↔agent /
|
|
333
|
-
// spawner obligations
|
|
333
|
+
// spawner obligations gate the turn here; the mirror's replyTo-linking clears the user
|
|
334
334
|
// one. (The /reply-obligations summary endpoint is left unfiltered — the obligation IS pending
|
|
335
335
|
// until the mirror lands, so observability stays accurate.)
|
|
336
336
|
const enforceable = obligations.filter(obligationNagEligible);
|
|
@@ -402,12 +402,12 @@ async function handlePermissionRequest(
|
|
|
402
402
|
const { decision } = await awaitPermissionDecision({
|
|
403
403
|
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
404
404
|
blockedReason: "permissionRequest",
|
|
405
|
-
recommendedAction: "Review the permission request in
|
|
405
|
+
recommendedAction: "Review the permission request in Humwick.",
|
|
406
406
|
});
|
|
407
407
|
return Response.json(handler.buildHookResponse(decision, body));
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
//
|
|
410
|
+
// bypass mode does not cover every provider check (Claude's dangerous-rm check on a variable
|
|
411
411
|
// target still asks). An open agent has no one to answer, so the request is approved, not held.
|
|
412
412
|
const OPEN_MODE_APPROVAL_REASON = "Auto-approved: the agent runs in open approval mode.";
|
|
413
413
|
|
|
@@ -442,7 +442,7 @@ async function fileQuestionDecision(options: ControlServerOptions, handler: Prov
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
function decisionFiledReason(id: number): string {
|
|
445
|
-
return `
|
|
445
|
+
return `Humwick filed this as operator decision #${id}; the user answers it on the Relay dashboard. Do not re-ask it and do not wait in this turn: the answer arrives later as a Relay message ("Decision #${id} answered"). Continue other work or end your turn. Next time, file questions with humwick_decision directly.`;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
function holdQuestionWithImmediateDeny(params: {
|
|
@@ -457,7 +457,7 @@ function holdQuestionWithImmediateDeny(params: {
|
|
|
457
457
|
toolInput?: unknown;
|
|
458
458
|
}): Response {
|
|
459
459
|
const { options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput } = params;
|
|
460
|
-
//
|
|
460
|
+
// a fresh "questions" prompt (the provider's AskUserQuestion) must not block
|
|
461
461
|
// its own hook response while the human answers. Denying immediately makes the provider persist
|
|
462
462
|
// the buffered turn plus rejected tool_result, so the pre-flush harvest below can publish the
|
|
463
463
|
// reasoning before the dashboard form appears. The eventual answer is injected as a normal prompt.
|
|
@@ -468,21 +468,21 @@ function holdQuestionWithImmediateDeny(params: {
|
|
|
468
468
|
void awaitPermissionDecision({
|
|
469
469
|
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
470
470
|
blockedReason: "pendingQuestion",
|
|
471
|
-
recommendedAction: "Answer the question in
|
|
471
|
+
recommendedAction: "Answer the question in Humwick.",
|
|
472
472
|
suppressResolvedStatus: true,
|
|
473
473
|
}).then(({ decision, view }) => decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length
|
|
474
474
|
? handler.buildAnswerInjectionPrompt!(view, decision, body)
|
|
475
475
|
: pendingQuestionDismissalPrompt(decision),
|
|
476
476
|
).then(
|
|
477
477
|
(text) => options.onAnswerInject?.({ approvalId, text }),
|
|
478
|
-
//
|
|
478
|
+
// the inject is the only release of the runner's question hold — never skip it.
|
|
479
479
|
() => options.onAnswerInject?.({ approvalId }),
|
|
480
480
|
).catch(() => {});
|
|
481
481
|
return response;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
// Shared by both the traditional block-until-decided flow and the
|
|
485
|
-
// pre-flushes narrative
|
|
484
|
+
// Shared by both the traditional block-until-decided flow and the deny-then-hold flow:
|
|
485
|
+
// pre-flushes narrative, builds+persists the interactive view, publishes the
|
|
486
486
|
// blocked providerState, and resolves once a decision arrives (or the wait times out). Callers
|
|
487
487
|
// differ only in what happens with the resolved decision (build a hook response vs. inject an
|
|
488
488
|
// answer prompt), so that part is left to them.
|
|
@@ -501,12 +501,12 @@ async function awaitPermissionDecision(params: {
|
|
|
501
501
|
suppressResolvedStatus?: boolean;
|
|
502
502
|
}): Promise<{ decision: ProviderPermissionDecisionInput; view: InteractivePrompt }> {
|
|
503
503
|
const { options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput, blockedReason, recommendedAction, suppressResolvedStatus } = params;
|
|
504
|
-
//
|
|
504
|
+
// flush any un-mirrored narrative before the blocking form appears in
|
|
505
505
|
// chat. The adapter owns how to settle its capture source; the transport only
|
|
506
506
|
// forwards the hook payload as an anchor and stamps the returned truth value.
|
|
507
507
|
let reasoningSettled = true;
|
|
508
508
|
// Defaults to the hook's own wall-clock receipt time; refined below to the transcript's own
|
|
509
|
-
// tool_use event time when the pre-flush harvest can resolve it
|
|
509
|
+
// tool_use event time when the pre-flush harvest can resolve it — the hook can receive
|
|
510
510
|
// before the provider flushes/timestamps that entry, which otherwise sorts the resolved prompt
|
|
511
511
|
// ahead of the narration that led up to it.
|
|
512
512
|
let resolvedOccurredAt = occurredAt;
|
|
@@ -584,7 +584,7 @@ async function handleSessionTurn(req: Request, options: ControlServerOptions): P
|
|
|
584
584
|
const promptId = isRecord(body) && typeof body.promptId === "string" && body.promptId ? body.promptId : undefined;
|
|
585
585
|
// transcriptPath is the primary source, but it can be absent (known CC Stop edge case).
|
|
586
586
|
// lastAssistantMessage is the fallback for exactly that — accept either; reject only when
|
|
587
|
-
// both are missing so the turn still reaches the dashboard chat
|
|
587
|
+
// both are missing so the turn still reaches the dashboard chat.
|
|
588
588
|
if (!transcriptPath && lastAssistantMessage === undefined) {
|
|
589
589
|
return Response.json({ ok: false, reason: "transcriptPath or lastAssistantMessage required" }, { status: 400 });
|
|
590
590
|
}
|
|
@@ -599,7 +599,7 @@ async function handleSessionTurn(req: Request, options: ControlServerOptions): P
|
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
/** `?firedAt=<epoch ms>`: when the hook ran, stamped by the hook itself
|
|
602
|
+
/** `?firedAt=<epoch ms>`: when the hook ran, stamped by the hook itself. */
|
|
603
603
|
function hookFiredAt(url: URL): { firedAt?: number } {
|
|
604
604
|
const raw = url.searchParams.get("firedAt") ?? "";
|
|
605
605
|
const firedAt = /^\d{1,16}$/.test(raw) ? Number(raw) : NaN;
|
|
@@ -664,7 +664,7 @@ async function handleStatus(req: Request, options: ControlServerOptions): Promis
|
|
|
664
664
|
const isWorkKind = (item: unknown): item is "provider-turn" | "subagent" | "background-script" =>
|
|
665
665
|
item === "provider-turn" || item === "subagent" || item === "background-script";
|
|
666
666
|
const clear = Array.isArray(body?.clear) ? body.clear.filter(isWorkKind) : undefined;
|
|
667
|
-
//
|
|
667
|
+
// optional pids the runner reconciles against process liveness so a tracked
|
|
668
668
|
// command/shell killed out-of-band clears the busy marker instead of stranding it.
|
|
669
669
|
const pidsInput = Array.isArray((body as { pids?: unknown })?.pids) ? (body as { pids: unknown[] }).pids : undefined;
|
|
670
670
|
const pids = pidsInput
|
|
@@ -688,7 +688,7 @@ async function handleStatus(req: Request, options: ControlServerOptions): Promis
|
|
|
688
688
|
return Response.json({ ok: true, ...update });
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
-
//
|
|
691
|
+
// the stop-failure hook reports a usage/rate-limit stall here. Build the
|
|
692
692
|
// provider-neutral `blocked` state (reason rate_limit) so the dashboard shows it
|
|
693
693
|
// distinctly (never as idle/available) and the relay's resume sweep can lift it
|
|
694
694
|
// at reset. The agent stays `idle` underneath (the turn truly ended); the runner
|
package/src/delivery-result.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function normalizeDeliverResult(
|
|
|
8
8
|
): { acknowledgedIds: number[]; terminalFailureIds: number[]; awaitingConsumptionIds: number[]; providerHeldIds: number[] } {
|
|
9
9
|
if (result === undefined) return { acknowledgedIds: deliverable.map((message) => message.id), terminalFailureIds: [], awaitingConsumptionIds: [], providerHeldIds: [] };
|
|
10
10
|
if (Array.isArray(result)) return { acknowledgedIds: result, terminalFailureIds: [], awaitingConsumptionIds: [], providerHeldIds: [] };
|
|
11
|
-
//
|
|
11
|
+
// round 4 review (F5) — adapter.ts documents terminalFailureIds as a subset of the ids NOT
|
|
12
12
|
// in acknowledgedIds, but that's only true because every adapter today always reports both
|
|
13
13
|
// together. Enforce it here too: a future adapter that reports only terminalFailureIds (omitting
|
|
14
14
|
// acknowledgedIds) must not have this default silently ack its own failures.
|
|
@@ -18,7 +18,7 @@ export function normalizeDeliverResult(
|
|
|
18
18
|
acknowledgedIds: result.acknowledgedIds ?? deliverable.filter((message) => !terminalFailureIds.includes(message.id) && !providerHeldIds.includes(message.id)).map((message) => message.id),
|
|
19
19
|
providerHeldIds,
|
|
20
20
|
terminalFailureIds,
|
|
21
|
-
//
|
|
21
|
+
// Phase B — accepted-but-unconsumed soft-steer ids: recorded, not settled (see settleMessageDelivery).
|
|
22
22
|
awaitingConsumptionIds: result.awaitingConsumptionIds ?? [],
|
|
23
23
|
};
|
|
24
24
|
}
|
package/src/effort-command.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ManagedProcess, ProviderAdapter } from "./adapter";
|
|
|
3
3
|
import { fireAndForget } from "./async-guard";
|
|
4
4
|
import type { ProviderInputTicket } from "./provider-input-queue";
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/** this process can switch effort: the adapter can switch and its launch allows it. */
|
|
7
7
|
export function runnerEffortSwitch(adapter: ProviderAdapter, process: ManagedProcess | undefined): boolean {
|
|
8
8
|
return Boolean(adapter.setEffort) && process?.meta?.effortSwitch === true;
|
|
9
9
|
}
|
|
@@ -44,7 +44,7 @@ export function isEffortNotice(eventType: string, data: unknown): boolean {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* converges the provider on Relay's desired effort version between turns. Triggered by the
|
|
48
48
|
* runner's existing edges (adoption, registration, the HTTP liveness pass, a bus notice, turn end);
|
|
49
49
|
* a failed read or report leaves the version pending for the next trigger. No timers of its own.
|
|
50
50
|
*/
|
package/src/empty-turn.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// an empty provider turn is a failure, not a clean idle.
|
|
2
2
|
//
|
|
3
|
-
// Observed live (
|
|
3
|
+
// Observed live (worker-b, OpenCode via OpenRouter): the spawn prompt was delivered and
|
|
4
4
|
// persisted, the model was dispatched a real turn, and the assistant message came back
|
|
5
5
|
// `finish=stop` with NO text part, NO tool part and ZERO tokens. Twice in a row. The runner logged
|
|
6
6
|
// `turn ended via provider idle` and left the agent reporting healthy and idle, so the spawning
|
|
@@ -68,6 +68,7 @@ export interface EmptyTurnVerdict {
|
|
|
68
68
|
empty: boolean;
|
|
69
69
|
/** How many empty turns in a row, counting this one. */
|
|
70
70
|
consecutiveEmpty: number;
|
|
71
|
+
inputOfferedAt?: number;
|
|
71
72
|
turnAgeMs: number;
|
|
72
73
|
/** Deliver the nudge now. At most once per user-originated input. */
|
|
73
74
|
nudge: boolean;
|
|
@@ -240,7 +241,7 @@ export class EmptyTurnLedger {
|
|
|
240
241
|
*
|
|
241
242
|
* KNOWN RESIDUAL, argued not fixed: this drops an obligation armed by input that arrived DURING
|
|
242
243
|
* the interrupted/restarted turn, which is a discharge by something other than the input's own
|
|
243
|
-
* turn — the shape every blocking finding on
|
|
244
|
+
* turn — the shape every blocking finding on has had. It is kept because a post-restart
|
|
244
245
|
* ledger cannot honestly judge a pre-restart obligation, and because the outcome is not silent:
|
|
245
246
|
* the interrupt command and forceClearProviderTurn both route `agent.turn_interrupted` to the
|
|
246
247
|
* spawn parent. The auto-restart path is weaker — `restartProvider` emits a
|
|
@@ -295,7 +296,7 @@ export class EmptyTurnLedger {
|
|
|
295
296
|
* seams on purpose: a turn that only ran tools, or one whose narration is suppressed by
|
|
296
297
|
* `reasoningCapture: false`, must never be misread as empty.
|
|
297
298
|
*
|
|
298
|
-
* Claude's transcript tail runs whatever `reasoningCapture` says
|
|
299
|
+
* Claude's transcript tail runs whatever `reasoningCapture` says, so its tool and
|
|
299
300
|
* narration steps count here even when the mirror keeps them out of chat.
|
|
300
301
|
*/
|
|
301
302
|
noteOutput(turnId?: string, occurredAt?: number, isWork = false): EmptyTurnOutput {
|
|
@@ -351,7 +352,7 @@ export class EmptyTurnLedger {
|
|
|
351
352
|
this.turn = undefined;
|
|
352
353
|
const turnAgeMs = Math.max(0, turn.endedAt - turn.startedAt);
|
|
353
354
|
if (turn.outputs > 0) {
|
|
354
|
-
// A turn already in flight can consume coalesced input without starting a new turn
|
|
355
|
+
// A turn already in flight can consume coalesced input without starting a new turn.
|
|
355
356
|
if (this.answers(turn)) {
|
|
356
357
|
this.dischargeOutput();
|
|
357
358
|
}
|
|
@@ -395,7 +396,7 @@ export class EmptyTurnLedger {
|
|
|
395
396
|
* still the reason the input has not been picked up — provider input is buffered behind the live
|
|
396
397
|
* turn — so escalating on it would page the parent about an agent that is visibly working. This
|
|
397
398
|
* defers; it never discharges. A turn that is wedged forever is the provider-turn watchdog's
|
|
398
|
-
*
|
|
399
|
+
* to report, not this one's.
|
|
399
400
|
*/
|
|
400
401
|
escalateOnTimeout(): "escalate" | "retry" | "done" {
|
|
401
402
|
if (this.undischargedInputAt === undefined || this.escalated) return "done";
|
|
@@ -404,7 +405,7 @@ export class EmptyTurnLedger {
|
|
|
404
405
|
return "escalate";
|
|
405
406
|
}
|
|
406
407
|
|
|
407
|
-
/** In-flight work needs correlated consumption, or stamped tool/response evidence
|
|
408
|
+
/** In-flight work needs correlated consumption, or stamped tool/response evidence. */
|
|
408
409
|
private answers(turn: TurnRecord): boolean {
|
|
409
410
|
const evidence = this.evidence;
|
|
410
411
|
const inputAt = evidence?.offeredAt;
|
|
@@ -431,6 +432,10 @@ export class EmptyTurnLedger {
|
|
|
431
432
|
this.ladderExempt = false;
|
|
432
433
|
}
|
|
433
434
|
|
|
435
|
+
inputOfferedAt(): number | undefined {
|
|
436
|
+
return this.evidence?.offeredAt;
|
|
437
|
+
}
|
|
438
|
+
|
|
434
439
|
/** Whether input is still outstanding — nothing has produced output since it was delivered. */
|
|
435
440
|
hasUndischargedInput(): boolean {
|
|
436
441
|
return this.undischargedInputAt !== undefined;
|
|
@@ -454,19 +459,19 @@ export class EmptyTurnLedger {
|
|
|
454
459
|
* original spawn prompt the moment a human typed "hello" at it hours later. This says only "your
|
|
455
460
|
* last turn produced nothing, go look at what you already have".
|
|
456
461
|
*
|
|
457
|
-
* Prefixed with `[
|
|
462
|
+
* Prefixed with `[humwick` so `isRunnerInjectedPrompt` suppresses its prompt echo and it is
|
|
458
463
|
* never mirrored into chat as if a human had typed it.
|
|
459
464
|
*/
|
|
460
465
|
export const EMPTY_TURN_NUDGE =
|
|
461
|
-
"[
|
|
466
|
+
"[humwick] Your previous turn ended without producing any output — no text, no tool calls. " +
|
|
462
467
|
"That was a provider-side empty completion, not an instruction to stop. Nothing was lost: the " +
|
|
463
468
|
"work you were given is already in this session's history above. Re-read it and carry on now; do " +
|
|
464
469
|
"not wait for it to be re-sent.";
|
|
465
470
|
|
|
466
471
|
/**
|
|
467
|
-
*
|
|
472
|
+
* the announcement for a turn that ended clean and produced nothing.
|
|
468
473
|
*
|
|
469
|
-
* Emitted (not silently logged) for the same reason
|
|
474
|
+
* Emitted (not silently logged) for the same reason emits its reconcile: `idle` is exactly
|
|
470
475
|
* the signal a coordinator uses to decide the agent is fine, so an empty turn that only ever
|
|
471
476
|
* reached a log file leaves the coordinator reading health it does not have. Pure builder so the
|
|
472
477
|
* payload the parent's notice derives from is unit-testable in isolation.
|
|
@@ -474,6 +479,7 @@ export const EMPTY_TURN_NUDGE =
|
|
|
474
479
|
export function emptyTurnTimelineEvent(input: {
|
|
475
480
|
turnId: string;
|
|
476
481
|
consecutiveEmpty: number;
|
|
482
|
+
inputOfferedAt?: number;
|
|
477
483
|
turnAgeMs: number;
|
|
478
484
|
nudged: boolean;
|
|
479
485
|
escalated: boolean;
|
|
@@ -510,6 +516,7 @@ export function emptyTurnTimelineEvent(input: {
|
|
|
510
516
|
metadata: {
|
|
511
517
|
eventType: PROVIDER_TURN_EMPTY_TIMELINE_STATUS,
|
|
512
518
|
emptyKind: stalled ? "input-stalled" : "empty-turn",
|
|
519
|
+
inputOfferedAt: input.inputOfferedAt,
|
|
513
520
|
turnId: input.turnId,
|
|
514
521
|
consecutiveEmptyTurns: input.consecutiveEmpty,
|
|
515
522
|
turnAgeMs: input.turnAgeMs,
|
|
@@ -596,7 +603,7 @@ export class EmptyTurnWatch {
|
|
|
596
603
|
}
|
|
597
604
|
|
|
598
605
|
/**
|
|
599
|
-
*
|
|
606
|
+
* arming ALSO starts the timed backstop, not just the strike ladder. Without this the
|
|
600
607
|
* ladder is the only detector, and it can only ever fire on a turn that actually ran: an input
|
|
601
608
|
* that provokes no turn at all, and every watch-only continuation, would go undetected forever.
|
|
602
609
|
*/
|
|
@@ -653,9 +660,9 @@ export class EmptyTurnWatch {
|
|
|
653
660
|
|
|
654
661
|
/**
|
|
655
662
|
* Evidence the model produced something. A `prompt` event is the user's own text echoing back.
|
|
656
|
-
* `diagnostic: true`
|
|
663
|
+
* `diagnostic: true` is an adapter narrating its OWN retry/failure mechanics —
|
|
657
664
|
* e.g. OpenCode's "retrying in Ns…" hold — never the model's own output; crediting it would let a
|
|
658
|
-
* retry storm that ends in terminal failure cancel the
|
|
665
|
+
* retry storm that ends in terminal failure cancel the escalation that wakes the spawn
|
|
659
666
|
* parent for a turn that never actually produced anything.
|
|
660
667
|
*/
|
|
661
668
|
noteOutput(event?: { type?: string; body?: string; turnId?: string; occurredAt?: number; diagnostic?: boolean }): void {
|
|
@@ -788,6 +795,7 @@ export class EmptyTurnWatch {
|
|
|
788
795
|
this.ports.log(`no output ${Math.round(deadlineMs / 1000)}s after ${nudged ? "the empty-turn nudge" : "input was delivered"} — escalating to spawn parent`);
|
|
789
796
|
this.ports.publishTimeline(emptyTurnTimelineEvent({
|
|
790
797
|
turnId,
|
|
798
|
+
inputOfferedAt: this.ledger.inputOfferedAt(),
|
|
791
799
|
consecutiveEmpty: this.ledger.consecutiveEmptyTurns(),
|
|
792
800
|
turnAgeMs: 0,
|
|
793
801
|
nudged,
|
package/src/free-port.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// the ONE free-loopback-port allocator for provider launches.
|
|
2
2
|
//
|
|
3
3
|
// A provider serve port must never be derived from another port: the runner control port
|
|
4
4
|
// comes from the OS (`bind :0`), and `controlPort + 1000` overflows 65535 whenever the OS
|
package/src/gate-run-config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//
|
|
1
|
+
// resolve the repo's CONFIGURED gate commands for a runner session's cwd.
|
|
2
2
|
//
|
|
3
3
|
// The backstop sweep used to classify a gate run by argv shape alone, which is why a foreign repo's
|
|
4
|
-
// own runner (`pnpm tier:run static fast` — observed live on
|
|
5
|
-
// already knows every repo's real gate commands from `.
|
|
4
|
+
// own runner (`pnpm tier:run static fast` — observed live on worker-a) was invisible to it. Relay
|
|
5
|
+
// already knows every repo's real gate commands from `.humwick/land-gates.json`; this is the
|
|
6
6
|
// runner-side reader for them. Mirrors orchestrator/src/gate-run-guard.ts's `gateCommandsForCwd` —
|
|
7
7
|
// the two workspaces cannot import each other, and the shared piece that matters (the matching
|
|
8
8
|
// rule) lives in sdk/src/gate-run-classify.ts, so only this four-line projection is repeated.
|
package/src/host-asset-plan.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { CLAUDE_OWNED_PLUGIN_IDS, selectClaudePluginDirs } from "./provider-integration";
|
|
2
|
-
|
|
3
|
-
// #3387 — the host half of a launch plan. Every runner-managed profiled launch runs in a Relay-owned
|
|
1
|
+
import { canonicalIntegrationAssetName, CLAUDE_OWNED_PLUGIN_IDS, selectClaudePluginDirs } from "./provider-integration";
|
|
2
|
+
// the host half of a launch plan. Every runner-managed profiled launch runs in a Relay-owned
|
|
4
3
|
// provider home; this module decides, per host asset class, what that home receives from the real
|
|
5
4
|
// host home, and materializes exactly that (removing what an opted-out class left behind on reuse).
|
|
6
5
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
7
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
-
import { effectiveHostSources, isRecord, type HostSourceClass, type HostSourceSelection
|
|
8
|
+
import { effectiveHostSources, isRecord, type HostSourceClass, type HostSourceSelection } from "@humwick/sdk";
|
|
10
9
|
import type { RunnerSpawnConfig } from "./adapter";
|
|
11
10
|
|
|
12
11
|
export type HostSourceDisposition = "applied" | "selected-empty" | "disabled" | "unsupported";
|
|
@@ -90,7 +89,7 @@ export interface ClaudeHostPluginRegistry {
|
|
|
90
89
|
* Project), their host enablement (a disabled plugin stays disabled) and their marketplaces.
|
|
91
90
|
* Payload paths stay absolute; only the registry files are private copies.
|
|
92
91
|
*/
|
|
93
|
-
export function selectClaudeHostPlugins(sourceHome: string, hostSettings: Record<string, unknown> | undefined, bridge?: {
|
|
92
|
+
export function selectClaudeHostPlugins(sourceHome: string, hostSettings: Record<string, unknown> | undefined, bridge?: { ownedProvided: boolean }): ClaudeHostPluginRegistry {
|
|
94
93
|
const installedPath = join(sourceHome, "plugins", "installed_plugins.json");
|
|
95
94
|
const knownPath = join(sourceHome, "plugins", "known_marketplaces.json");
|
|
96
95
|
const installed = readSelectedJson("plugins", installedPath);
|
|
@@ -108,7 +107,7 @@ export function selectClaudeHostPlugins(sourceHome: string, hostSettings: Record
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
if (bridge) {
|
|
111
|
-
const preferred =
|
|
110
|
+
const preferred = CLAUDE_OWNED_PLUGIN_IDS[1]!;
|
|
112
111
|
const candidates = [preferred, ...CLAUDE_OWNED_PLUGIN_IDS].filter(id => keptPlugins[id]);
|
|
113
112
|
const selected = bridge.ownedProvided ? undefined : candidates.find(id => enabled[id] === true) ?? candidates[0];
|
|
114
113
|
for (const id of CLAUDE_OWNED_PLUGIN_IDS) if (id !== selected) delete keptPlugins[id];
|
|
@@ -305,8 +304,8 @@ export function claudeHostPlan(config: RunnerSpawnConfig, sourceHome: string, pr
|
|
|
305
304
|
const selection = hostSourcesFor(config);
|
|
306
305
|
const settingsClass: HostSourceClass | undefined = selection.plugins ? "plugins" : selection.settings ? "settings" : undefined;
|
|
307
306
|
const hostSettings = settingsClass ? claudeHostSettingsSnapshot(config, sourceHome, settingsClass) : undefined;
|
|
308
|
-
const ownedProvided = !config.agentProfile || Boolean(config.agentProfile.provisioning?.plugins.some(plugin => plugin.name === "
|
|
309
|
-
const registry = selection.plugins ? selectClaudeHostPlugins(sourceHome, hostSettings, {
|
|
307
|
+
const ownedProvided = !config.agentProfile || Boolean(config.agentProfile.provisioning?.plugins.some(plugin => canonicalIntegrationAssetName(plugin.name) === "humwick-claude-plugin"));
|
|
308
|
+
const registry = selection.plugins ? selectClaudeHostPlugins(sourceHome, hostSettings, { ownedProvided }) : undefined;
|
|
310
309
|
const files = [
|
|
311
310
|
...(registry?.installed ? [{ target: join("plugins", "installed_plugins.json"), content: JSON.stringify(registry.installed, null, 2) }] : []),
|
|
312
311
|
...(registry?.known ? [{ target: join("plugins", "known_marketplaces.json"), content: JSON.stringify(registry.known, null, 2) }] : []),
|
|
@@ -317,7 +316,7 @@ export function claudeHostPlan(config: RunnerSpawnConfig, sourceHome: string, pr
|
|
|
317
316
|
files,
|
|
318
317
|
settings: { ...(selection.settings ? claudeHostSettingsFor(hostSettings, "settings") : {}), ...(registry?.settings ?? {}) },
|
|
319
318
|
pluginIds: registry?.ids ?? [],
|
|
320
|
-
pluginDirs: selection.plugins ? selectClaudePluginDirs(providerConfig.pluginDirs,
|
|
319
|
+
pluginDirs: selection.plugins ? selectClaudePluginDirs(providerConfig.pluginDirs, ownedProvided || Boolean(registry?.ids.some(id => CLAUDE_OWNED_PLUGIN_IDS.includes(id) && (registry.settings.enabledPlugins as Record<string, unknown> | undefined)?.[id] === true))) : [],
|
|
321
320
|
};
|
|
322
321
|
}
|
|
323
322
|
|
|
@@ -366,7 +365,7 @@ export interface CodexHostPlan {
|
|
|
366
365
|
settings: Record<string, unknown>;
|
|
367
366
|
}
|
|
368
367
|
|
|
369
|
-
export function codexHostPlan(config: RunnerSpawnConfig, sourceHome: string, userHome = homedir()): CodexHostPlan {
|
|
368
|
+
export function codexHostPlan(config: RunnerSpawnConfig, sourceHome: string, userHome = process.env.HOME || homedir()): CodexHostPlan {
|
|
370
369
|
const selection = hostSourcesFor(config);
|
|
371
370
|
const ambient = codexAmbientSkillRoots(userHome).flatMap(skillDirsUnder);
|
|
372
371
|
let settings: Record<string, unknown> = {};
|
|
@@ -404,15 +403,23 @@ export function codexHostReport(plan: CodexHostPlan, applied: { mcp: boolean; ho
|
|
|
404
403
|
};
|
|
405
404
|
}
|
|
406
405
|
|
|
407
|
-
const CODEX_SETTINGS_BEGIN = "# >>>
|
|
408
|
-
const CODEX_SETTINGS_END = "# <<<
|
|
406
|
+
const CODEX_SETTINGS_BEGIN = "# >>> humwick host settings (generated; edits are replaced)";
|
|
407
|
+
const CODEX_SETTINGS_END = "# <<< humwick host settings";
|
|
409
408
|
|
|
410
409
|
function withoutCodexSettingsRegion(text: string): string {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
410
|
+
// recorded homes can retain the previous generated region until reuse.
|
|
411
|
+
for (const [beginMarker, endMarker] of [
|
|
412
|
+
[CODEX_SETTINGS_BEGIN, CODEX_SETTINGS_END],
|
|
413
|
+
["# >>> agent-relay host settings (generated; edits are replaced)", "# <<< agent-relay host settings"],
|
|
414
|
+
] as const) {
|
|
415
|
+
let begin: number;
|
|
416
|
+
while ((begin = text.indexOf(beginMarker)) >= 0) {
|
|
417
|
+
const end = text.indexOf(endMarker, begin);
|
|
418
|
+
if (end < 0) break;
|
|
419
|
+
text = (text.slice(0, begin) + text.slice(end + endMarker.length)).replace(/^\n+/, "");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return text;
|
|
416
423
|
}
|
|
417
424
|
|
|
418
425
|
/**
|
|
@@ -12,12 +12,12 @@ const task: OffloadWorkerTask<{ path: string; kind?: "config" | "settings" }, Re
|
|
|
12
12
|
try {
|
|
13
13
|
parsed = JSON.parse(await file.text());
|
|
14
14
|
} catch (error) {
|
|
15
|
-
//
|
|
15
|
+
// a selected host class must fail loudly on a malformed settings.json, never launch as if empty.
|
|
16
16
|
if (kind === "settings") return { [READ_ERROR]: (error as Error).message };
|
|
17
17
|
throw error;
|
|
18
18
|
}
|
|
19
19
|
if (!isRecord(parsed)) return kind === "settings" ? { [READ_ERROR]: "not a JSON object" } : undefined;
|
|
20
|
-
// settings.json is small; the launch partitions it per host class
|
|
20
|
+
// settings.json is small; the launch partitions it per host class.
|
|
21
21
|
if (kind === "settings") return parsed;
|
|
22
22
|
// Keep only the three host-level launch inputs. Returning Claude's entire history/session
|
|
23
23
|
// object would transfer tens of MB back to the runner and recreate the same event-loop stall.
|
package/src/host-sources.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { claudeHostSettingsSnapshot } from "./host-asset-plan";
|
|
|
6
6
|
import { providerSourceHome } from "./profile-home";
|
|
7
7
|
|
|
8
8
|
/** Host Claude hooks are stored in the host settings.json, not .claude.json; a host that set
|
|
9
|
-
* `disableAllHooks` carries none, so a managed home never re-enables them
|
|
10
|
-
*
|
|
9
|
+
* `disableAllHooks` carries none, so a managed home never re-enables them.
|
|
10
|
+
* keyed by provider id so a dedicatedHome claude-family provider (claude-b)
|
|
11
11
|
* reads ITS OWN source home's settings, never ~/.claude's. */
|
|
12
12
|
export function hostClaudeHooks(config: Pick<RunnerSpawnConfig, "hostClaudeSettings">, provider: string = "claude"): ProvisioningHookSet {
|
|
13
13
|
const settings = claudeHostSettingsSnapshot(config, providerSourceHome(provider), "hooks");
|