@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.
Files changed (217) hide show
  1. package/LICENSE +661 -167
  2. package/agent-bin/humwick +2 -2
  3. package/agent-bin/humwick-browser +4 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
  5. package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
  6. package/package.json +8 -11
  7. package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
  8. package/plugins/claude/.claude-plugin/plugin.json +3 -3
  9. package/plugins/claude/claude-statusline-probe.cjs +26 -16
  10. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
  11. package/plugins/claude/hooks/gate-run-guard.sh +3 -3
  12. package/plugins/claude/hooks/gate-run-guard.ts +5 -5
  13. package/plugins/claude/hooks/kill-guard.provisioned.mjs +1 -1
  14. package/plugins/claude/hooks/kill-guard.sh +4 -4
  15. package/plugins/claude/hooks/kill-guard.ts +2 -2
  16. package/plugins/claude/hooks/permission-request.sh +1 -1
  17. package/plugins/claude/hooks/post-compact.sh +1 -1
  18. package/plugins/claude/hooks/post-tool-use.sh +3 -3
  19. package/plugins/claude/hooks/pre-compact.sh +2 -2
  20. package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
  21. package/plugins/claude/hooks/relay-status.sh +32 -32
  22. package/plugins/claude/hooks/session-end.sh +2 -2
  23. package/plugins/claude/hooks/session-start.sh +3 -3
  24. package/plugins/claude/hooks/shell-trust.ts +1 -1
  25. package/plugins/claude/hooks/stop-failure.sh +2 -2
  26. package/plugins/claude/hooks/stop.sh +6 -6
  27. package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
  28. package/plugins/claude/hooks/subagent-stop.sh +3 -3
  29. package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
  30. package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
  31. package/plugins/claude/monitors/monitors.json +2 -2
  32. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
  33. package/plugins/claude/monitors/relay-monitor.ts +5 -5
  34. package/plugins/claude/skills/label/SKILL.md +3 -3
  35. package/plugins/claude/skills/message/SKILL.md +5 -5
  36. package/plugins/claude/skills/react/SKILL.md +1 -1
  37. package/plugins/claude/skills/read-message/SKILL.md +4 -4
  38. package/plugins/claude/skills/reply/SKILL.md +5 -5
  39. package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
  40. package/plugins/claude/skills/slice-first/SKILL.md +5 -3
  41. package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
  42. package/plugins/claude/skills/status/SKILL.md +3 -3
  43. package/plugins/claude/skills/tags/SKILL.md +5 -5
  44. package/plugins/claude/skills/task/SKILL.md +10 -10
  45. package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
  47. package/plugins/codex/skills/guide/SKILL.md +3 -3
  48. package/plugins/codex/skills/label/SKILL.md +3 -3
  49. package/plugins/codex/skills/message/SKILL.md +5 -5
  50. package/plugins/codex/skills/react/SKILL.md +1 -1
  51. package/plugins/codex/skills/read-message/SKILL.md +4 -4
  52. package/plugins/codex/skills/reply/SKILL.md +5 -5
  53. package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
  54. package/plugins/codex/skills/slice-first/SKILL.md +5 -3
  55. package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
  56. package/plugins/codex/skills/status/SKILL.md +3 -3
  57. package/plugins/codex/skills/tags/SKILL.md +5 -5
  58. package/plugins/codex/skills/task/SKILL.md +11 -10
  59. package/src/adapter.ts +95 -92
  60. package/src/adapters/claude-api.ts +8 -11
  61. package/src/adapters/claude-context-command.ts +1 -1
  62. package/src/adapters/claude-credentials.ts +7 -3
  63. package/src/adapters/claude-delivery-lanes.ts +2 -2
  64. package/src/adapters/claude-delivery.ts +16 -16
  65. package/src/adapters/claude-effort.ts +2 -2
  66. package/src/adapters/claude-exit.ts +2 -2
  67. package/src/adapters/claude-headless-mirror.ts +18 -2
  68. package/src/adapters/claude-headless-refusal.ts +3 -3
  69. package/src/adapters/claude-headless.ts +11 -10
  70. package/src/adapters/claude-launch-exit.ts +1 -1
  71. package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
  72. package/src/adapters/claude-permission.ts +5 -5
  73. package/src/adapters/claude-prompt-gates.ts +9 -9
  74. package/src/adapters/claude-prompt-hook.ts +2 -2
  75. package/src/adapters/claude-session-capture.ts +6 -6
  76. package/src/adapters/claude-session-probe.ts +25 -25
  77. package/src/adapters/claude-shutdown.ts +11 -11
  78. package/src/adapters/claude-status-detectors.ts +31 -31
  79. package/src/adapters/claude-subagents.ts +7 -7
  80. package/src/adapters/claude-submit-confirmation.ts +6 -6
  81. package/src/adapters/claude-tmux.ts +43 -43
  82. package/src/adapters/claude-token-usage-capture.ts +5 -5
  83. package/src/adapters/claude-tool-policy.ts +7 -7
  84. package/src/adapters/claude-transcript.ts +24 -24
  85. package/src/adapters/claude.ts +100 -100
  86. package/src/adapters/codex-agent-message-capture.ts +26 -63
  87. package/src/adapters/codex-app-server.ts +1 -1
  88. package/src/adapters/codex-client.ts +9 -9
  89. package/src/adapters/codex-effective-config.ts +1 -1
  90. package/src/adapters/codex-launch-config.ts +5 -5
  91. package/src/adapters/codex-rollout-context.ts +2 -2
  92. package/src/adapters/codex-skill-discovery.ts +1 -1
  93. package/src/adapters/codex-token-usage-capture.ts +16 -16
  94. package/src/adapters/codex-tool-output.ts +4 -4
  95. package/src/adapters/codex-version.ts +3 -3
  96. package/src/adapters/codex.ts +64 -64
  97. package/src/adapters/opencode-api.ts +1 -1
  98. package/src/adapters/opencode-context.ts +20 -22
  99. package/src/adapters/opencode-model-registry.ts +7 -7
  100. package/src/adapters/opencode-provider.ts +8 -8
  101. package/src/adapters/opencode-provisioning.ts +30 -0
  102. package/src/adapters/opencode-server-retry.ts +1 -1
  103. package/src/adapters/opencode-soft-steer.ts +5 -5
  104. package/src/adapters/opencode-spawn.ts +18 -14
  105. package/src/adapters/opencode-token-usage-capture.ts +12 -12
  106. package/src/adapters/opencode-tool-events.ts +15 -15
  107. package/src/adapters/opencode.ts +232 -232
  108. package/src/adapters/project-shell.ts +11 -11
  109. package/src/adapters/service-access.ts +6 -6
  110. package/src/adapters/tmux-offload.ts +9 -9
  111. package/src/adapters/tool-summary.ts +5 -5
  112. package/src/agent-redact.ts +1 -1
  113. package/src/agent-runner/commands/dispatch.ts +10 -10
  114. package/src/agent-runner/commands/lifecycle.ts +2 -2
  115. package/src/agent-runner/commands/prompt.ts +19 -19
  116. package/src/agent-runner/credentials.ts +5 -5
  117. package/src/agent-runner/inbox.ts +1 -1
  118. package/src/agent-runner/lifecycle-milestones.ts +3 -3
  119. package/src/agent-runner/lifecycle-phase.ts +2 -2
  120. package/src/agent-runner/liveness-exchange.ts +4 -10
  121. package/src/agent-runner/registration.ts +8 -8
  122. package/src/agent-runner/runner-context.ts +9 -14
  123. package/src/agent-runner/session-mirror.ts +12 -12
  124. package/src/agent-runner/task-claims.ts +2 -2
  125. package/src/agent-runner/token-usage-coverage.ts +1 -1
  126. package/src/agent-runner/turn-report.ts +12 -12
  127. package/src/api-budget-accounting.ts +3 -3
  128. package/src/area-memory.ts +6 -6
  129. package/src/artifact-path-materialize.ts +1 -1
  130. package/src/async-guard.ts +5 -5
  131. package/src/attachment-cache.ts +17 -22
  132. package/src/background-script-reconcile.ts +6 -6
  133. package/src/bounded-log.ts +1 -1
  134. package/src/busy-reconciler.ts +22 -22
  135. package/src/claim-tracker.ts +3 -3
  136. package/src/claude-settings.ts +8 -8
  137. package/src/claude-system-prompt.ts +1 -1
  138. package/src/claude-workspace-trust.ts +1 -1
  139. package/src/config.ts +62 -61
  140. package/src/context-boundary.ts +5 -5
  141. package/src/context-command.ts +3 -3
  142. package/src/control-server.ts +32 -32
  143. package/src/delivery-result.ts +2 -2
  144. package/src/effort-command.ts +2 -2
  145. package/src/empty-turn.ts +22 -14
  146. package/src/free-port.ts +1 -1
  147. package/src/gate-run-config.ts +3 -3
  148. package/src/host-asset-plan.ts +24 -17
  149. package/src/host-claude-config-worker.ts +2 -2
  150. package/src/host-sources.ts +2 -2
  151. package/src/index.ts +16 -16
  152. package/src/launch-assembly.ts +94 -96
  153. package/src/liveness.ts +15 -15
  154. package/src/logger.ts +3 -3
  155. package/src/mcp-outbox.ts +3 -3
  156. package/src/mcp-preflight.ts +2 -2
  157. package/src/mcp-source-selection.ts +2 -2
  158. package/src/mcp-tool-deferral.ts +1 -1
  159. package/src/mcp-vault-env.ts +2 -2
  160. package/src/message-body-config/config.ts +4 -4
  161. package/src/mid-turn-delivery.ts +2 -2
  162. package/src/native-memory-lever.ts +8 -8
  163. package/src/outbox.ts +15 -15
  164. package/src/pending-permission-store.ts +1 -1
  165. package/src/profile-home.ts +19 -19
  166. package/src/profile-projection.ts +26 -24
  167. package/src/project-instructions.ts +5 -5
  168. package/src/provider-auth-warning.ts +17 -0
  169. package/src/provider-double.ts +1 -1
  170. package/src/provider-hold.ts +3 -3
  171. package/src/provider-home-marker.ts +1 -1
  172. package/src/provider-home-tombstone.ts +1 -1
  173. package/src/provider-input-queue.ts +2 -2
  174. package/src/provider-integration.ts +15 -13
  175. package/src/provider-setup.ts +15 -3
  176. package/src/providers.ts +1 -1
  177. package/src/provisioning.ts +37 -40
  178. package/src/question-decision.ts +2 -2
  179. package/src/quota.ts +0 -6
  180. package/src/rate-limit.ts +24 -13
  181. package/src/refused-prompt-store.ts +2 -2
  182. package/src/relay-injection-events.ts +2 -2
  183. package/src/relay-instructions.ts +5 -5
  184. package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
  185. package/src/relay-mcp-proxy.ts +57 -58
  186. package/src/relay-mcp.ts +55 -69
  187. package/src/reply-obligation-cache.ts +9 -9
  188. package/src/response-capture-report.ts +9 -9
  189. package/src/response-routing.ts +1 -1
  190. package/src/runner-core.ts +209 -222
  191. package/src/runner-exit.ts +1 -1
  192. package/src/runner-helpers.ts +10 -10
  193. package/src/runner-insights.ts +4 -2
  194. package/src/runner-mcp-proxy.ts +3 -3
  195. package/src/runner-spawn-config.ts +3 -3
  196. package/src/runtime-token-file.ts +1 -1
  197. package/src/runtime-token-remint.ts +1 -1
  198. package/src/service-access.ts +2 -2
  199. package/src/session-destroy.ts +1 -1
  200. package/src/session-insights.ts +1 -1
  201. package/src/session-scratch.ts +24 -37
  202. package/src/spawn-input.ts +6 -6
  203. package/src/startup-config-fetch.ts +4 -4
  204. package/src/subreaper.ts +3 -5
  205. package/src/test-run-dedup.ts +13 -13
  206. package/src/transcript-context.ts +2 -2
  207. package/src/transport-errors.ts +1 -1
  208. package/src/turn-opener.ts +1 -1
  209. package/src/turn-reconcile-event.ts +2 -2
  210. package/src/vault-proxy-hop.ts +2 -2
  211. package/src/web-tools.ts +2 -2
  212. package/agent-bin/agent-relay-browser +0 -10
  213. package/plugins/claude/skills/disconnect/SKILL.md +0 -16
  214. package/plugins/claude/skills/pair/SKILL.md +0 -30
  215. package/plugins/codex/skills/disconnect/SKILL.md +0 -15
  216. package/plugins/codex/skills/pair/SKILL.md +0 -29
  217. package/src/adapters/provider-quota-harvest.ts +0 -76
@@ -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" (#693).
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
- // #1173: the immediate PreToolUse response for a "questions" prompt (see the deny-then-hold
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 Agent Relay, 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.";
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 (#198 item 5). Phase 1 logs it FATAL to the per-agent
31
- // log; Phase 2 (#196) will additionally route it through the runner outbox to the
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
- // #2459 — the CURRENT non-secret attachment generation (derived from the provider's active
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
- // #1173: a "questions" (AskUserQuestion) prompt's own PreToolUse hook is answered with `deny`
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
- // #2914: files a "questions" prompt as a durable operator decision; undefined = not filed, so the
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 (#2664).
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 (#198): a hook reporting an unhandled failure. The
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
- // #3648 — the agent runs in `open` approval mode: answer every permission request at once.
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 (#693).
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
- // #2459 round 1 (review A) — strip any credential-bearing env record from the body:
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
- // #2459 — deliberately returns ONLY the non-secret generation digest. No session id,
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 (#693).
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 Agent Relay 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.`,
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
- // #418 — never block the turn to nag for a `user` obligation: the operator already receives
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 (#413) gate the turn here; the mirror's replyTo-linking clears the user
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 Agent Relay.",
405
+ recommendedAction: "Review the permission request in Humwick.",
406
406
  });
407
407
  return Response.json(handler.buildHookResponse(decision, body));
408
408
  }
409
409
 
410
- // #3648 — bypass mode does not cover every provider check (Claude's dangerous-rm check on a variable
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 `Agent Relay 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 relay_decision directly.`;
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
- // #1173/#1194: a fresh "questions" prompt (the provider's AskUserQuestion) must not block
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 Agent Relay.",
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
- // #2913: the inject is the only release of the runner's question hold — never skip it.
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 #1173 deny-then-hold flow:
485
- // pre-flushes narrative (#435/#1116), builds+persists the interactive view, publishes the
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
- // #435/#1116: flush any un-mirrored narrative before the blocking form appears in
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 (#1185) — the hook can receive
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 (#264).
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 (#2664). */
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
- // #650: optional pids the runner reconciles against process liveness so a tracked
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
- // #286: the stop-failure hook reports a usage/rate-limit stall here. Build the
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
@@ -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
- // #2397 round 4 review (F5) — adapter.ts documents terminalFailureIds as a subset of the ids NOT
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
- // #2539 Phase B — accepted-but-unconsumed soft-steer ids: recorded, not settled (see settleMessageDelivery).
21
+ // Phase B — accepted-but-unconsumed soft-steer ids: recorded, not settled (see settleMessageDelivery).
22
22
  awaitingConsumptionIds: result.awaitingConsumptionIds ?? [],
23
23
  };
24
24
  }
@@ -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
- /** #2950 — this process can switch effort: the adapter can switch and its launch allows it. */
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
- * #2984 — converges the provider on Relay's desired effort version between turns. Triggered by the
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
- // #2389 — an empty provider turn is a failure, not a clean idle.
1
+ // an empty provider turn is a failure, not a clean idle.
2
2
  //
3
- // Observed live (blackbox, OpenCode via OpenRouter): the spawn prompt was delivered and
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 #2389 has had. It is kept because a post-restart
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 (#3411), so its tool and
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 (#2691).
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
- * (#1980) to report, not this one's.
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 (#2691). */
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 `[agent-relay` so `isRunnerInjectedPrompt` suppresses its prompt echo and it is
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
- "[agent-relay] Your previous turn ended without producing any output — no text, no tool calls. " +
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
- * #2389 — the announcement for a turn that ended clean and produced nothing.
472
+ * the announcement for a turn that ended clean and produced nothing.
468
473
  *
469
- * Emitted (not silently logged) for the same reason #1621 emits its reconcile: `idle` is exactly
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
- * #2389 — arming ALSO starts the timed backstop, not just the strike ladder. Without this the
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` (#2397 round 3) is an adapter narrating its OWN retry/failure mechanics —
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 #2389 escalation that wakes the spawn
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
- // #2719 — the ONE free-loopback-port allocator for provider launches.
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
@@ -1,8 +1,8 @@
1
- // #2461 — resolve the repo's CONFIGURED gate commands for a runner session's cwd.
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 wbox) was invisible to it. Relay
5
- // already knows every repo's real gate commands from `.agent-relay/land-gates.json`; this is the
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.
@@ -1,12 +1,11 @@
1
- import { CLAUDE_OWNED_PLUGIN_IDS, selectClaudePluginDirs } from "./provider-integration";
2
- import { runnerMcpGeneration } from "./relay-mcp";
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, type ToolGeneration } from "@humwick/sdk";
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?: { generation: ToolGeneration; ownedProvided: boolean }): ClaudeHostPluginRegistry {
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 = bridge.generation === "humwick" ? CLAUDE_OWNED_PLUGIN_IDS[1]! : CLAUDE_OWNED_PLUGIN_IDS[0]!;
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 === "agent-relay-claude-plugin"));
309
- const registry = selection.plugins ? selectClaudeHostPlugins(sourceHome, hostSettings, { generation: runnerMcpGeneration(config.env), ownedProvided }) : undefined;
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, runnerMcpGeneration(config.env), ownedProvided || Boolean(registry?.ids.some(id => CLAUDE_OWNED_PLUGIN_IDS.includes(id) && (registry.settings.enabledPlugins as Record<string, unknown> | undefined)?.[id] === true))) : [],
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 = "# >>> agent-relay host settings (generated; edits are replaced)";
408
- const CODEX_SETTINGS_END = "# <<< agent-relay host settings";
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
- const begin = text.indexOf(CODEX_SETTINGS_BEGIN);
412
- if (begin < 0) return text;
413
- const end = text.indexOf(CODEX_SETTINGS_END, begin);
414
- if (end < 0) return text;
415
- return (text.slice(0, begin) + text.slice(end + CODEX_SETTINGS_END.length)).replace(/^\n+/, "");
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
- // #3387 — a selected host class must fail loudly on a malformed settings.json, never launch as if empty.
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 (#3387).
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.
@@ -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 (#3387).
10
- * #2513 — keyed by provider id so a dedicatedHome claude-family provider (claude-b)
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");