@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.
Files changed (211) hide show
  1. package/agent-bin/humwick +2 -2
  2. package/agent-bin/humwick-browser +4 -0
  3. package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
  4. package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
  5. package/package.json +7 -10
  6. package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
  7. package/plugins/claude/.claude-plugin/plugin.json +3 -3
  8. package/plugins/claude/claude-statusline-probe.cjs +26 -16
  9. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
  10. package/plugins/claude/hooks/gate-run-guard.sh +3 -3
  11. package/plugins/claude/hooks/gate-run-guard.ts +5 -5
  12. package/plugins/claude/hooks/kill-guard.sh +4 -4
  13. package/plugins/claude/hooks/kill-guard.ts +1 -1
  14. package/plugins/claude/hooks/permission-request.sh +1 -1
  15. package/plugins/claude/hooks/post-compact.sh +1 -1
  16. package/plugins/claude/hooks/post-tool-use.sh +3 -3
  17. package/plugins/claude/hooks/pre-compact.sh +2 -2
  18. package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
  19. package/plugins/claude/hooks/relay-status.sh +32 -32
  20. package/plugins/claude/hooks/session-end.sh +2 -2
  21. package/plugins/claude/hooks/session-start.sh +3 -3
  22. package/plugins/claude/hooks/shell-trust.ts +1 -1
  23. package/plugins/claude/hooks/stop-failure.sh +2 -2
  24. package/plugins/claude/hooks/stop.sh +6 -6
  25. package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
  26. package/plugins/claude/hooks/subagent-stop.sh +3 -3
  27. package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
  28. package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
  29. package/plugins/claude/monitors/monitors.json +2 -2
  30. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
  31. package/plugins/claude/monitors/relay-monitor.ts +5 -5
  32. package/plugins/claude/skills/label/SKILL.md +3 -3
  33. package/plugins/claude/skills/message/SKILL.md +5 -5
  34. package/plugins/claude/skills/react/SKILL.md +1 -1
  35. package/plugins/claude/skills/read-message/SKILL.md +4 -4
  36. package/plugins/claude/skills/reply/SKILL.md +5 -5
  37. package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
  38. package/plugins/claude/skills/slice-first/SKILL.md +5 -3
  39. package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
  40. package/plugins/claude/skills/status/SKILL.md +1 -1
  41. package/plugins/claude/skills/tags/SKILL.md +5 -5
  42. package/plugins/claude/skills/task/SKILL.md +10 -10
  43. package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
  44. package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
  45. package/plugins/codex/skills/guide/SKILL.md +1 -1
  46. package/plugins/codex/skills/label/SKILL.md +3 -3
  47. package/plugins/codex/skills/message/SKILL.md +5 -5
  48. package/plugins/codex/skills/react/SKILL.md +1 -1
  49. package/plugins/codex/skills/read-message/SKILL.md +4 -4
  50. package/plugins/codex/skills/reply/SKILL.md +5 -5
  51. package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
  52. package/plugins/codex/skills/slice-first/SKILL.md +5 -3
  53. package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
  54. package/plugins/codex/skills/status/SKILL.md +1 -1
  55. package/plugins/codex/skills/tags/SKILL.md +5 -5
  56. package/plugins/codex/skills/task/SKILL.md +11 -10
  57. package/src/adapter.ts +95 -92
  58. package/src/adapters/claude-api.ts +8 -11
  59. package/src/adapters/claude-context-command.ts +1 -1
  60. package/src/adapters/claude-credentials.ts +7 -3
  61. package/src/adapters/claude-delivery-lanes.ts +2 -2
  62. package/src/adapters/claude-delivery.ts +16 -16
  63. package/src/adapters/claude-effort.ts +2 -2
  64. package/src/adapters/claude-exit.ts +2 -2
  65. package/src/adapters/claude-headless-mirror.ts +18 -2
  66. package/src/adapters/claude-headless-refusal.ts +3 -3
  67. package/src/adapters/claude-headless.ts +11 -10
  68. package/src/adapters/claude-launch-exit.ts +1 -1
  69. package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
  70. package/src/adapters/claude-permission.ts +5 -5
  71. package/src/adapters/claude-prompt-gates.ts +9 -9
  72. package/src/adapters/claude-prompt-hook.ts +2 -2
  73. package/src/adapters/claude-session-capture.ts +6 -6
  74. package/src/adapters/claude-session-probe.ts +25 -25
  75. package/src/adapters/claude-shutdown.ts +11 -11
  76. package/src/adapters/claude-status-detectors.ts +31 -31
  77. package/src/adapters/claude-subagents.ts +7 -7
  78. package/src/adapters/claude-submit-confirmation.ts +6 -6
  79. package/src/adapters/claude-tmux.ts +43 -43
  80. package/src/adapters/claude-token-usage-capture.ts +5 -5
  81. package/src/adapters/claude-tool-policy.ts +7 -7
  82. package/src/adapters/claude-transcript.ts +24 -24
  83. package/src/adapters/claude.ts +100 -100
  84. package/src/adapters/codex-agent-message-capture.ts +26 -63
  85. package/src/adapters/codex-app-server.ts +1 -1
  86. package/src/adapters/codex-client.ts +9 -9
  87. package/src/adapters/codex-effective-config.ts +1 -1
  88. package/src/adapters/codex-launch-config.ts +5 -5
  89. package/src/adapters/codex-rollout-context.ts +2 -2
  90. package/src/adapters/codex-skill-discovery.ts +1 -1
  91. package/src/adapters/codex-token-usage-capture.ts +16 -16
  92. package/src/adapters/codex-tool-output.ts +4 -4
  93. package/src/adapters/codex-version.ts +3 -3
  94. package/src/adapters/codex.ts +64 -64
  95. package/src/adapters/opencode-api.ts +1 -1
  96. package/src/adapters/opencode-context.ts +20 -22
  97. package/src/adapters/opencode-model-registry.ts +7 -7
  98. package/src/adapters/opencode-provider.ts +8 -8
  99. package/src/adapters/opencode-provisioning.ts +30 -0
  100. package/src/adapters/opencode-server-retry.ts +1 -1
  101. package/src/adapters/opencode-soft-steer.ts +5 -5
  102. package/src/adapters/opencode-spawn.ts +18 -14
  103. package/src/adapters/opencode-token-usage-capture.ts +12 -12
  104. package/src/adapters/opencode-tool-events.ts +15 -15
  105. package/src/adapters/opencode.ts +232 -232
  106. package/src/adapters/project-shell.ts +11 -11
  107. package/src/adapters/service-access.ts +6 -6
  108. package/src/adapters/tmux-offload.ts +9 -9
  109. package/src/adapters/tool-summary.ts +5 -5
  110. package/src/agent-redact.ts +1 -1
  111. package/src/agent-runner/commands/dispatch.ts +10 -10
  112. package/src/agent-runner/commands/lifecycle.ts +2 -2
  113. package/src/agent-runner/commands/prompt.ts +19 -19
  114. package/src/agent-runner/credentials.ts +5 -5
  115. package/src/agent-runner/inbox.ts +1 -1
  116. package/src/agent-runner/lifecycle-milestones.ts +3 -3
  117. package/src/agent-runner/lifecycle-phase.ts +2 -2
  118. package/src/agent-runner/liveness-exchange.ts +4 -10
  119. package/src/agent-runner/registration.ts +8 -8
  120. package/src/agent-runner/runner-context.ts +9 -14
  121. package/src/agent-runner/session-mirror.ts +12 -12
  122. package/src/agent-runner/task-claims.ts +2 -2
  123. package/src/agent-runner/token-usage-coverage.ts +1 -1
  124. package/src/agent-runner/turn-report.ts +12 -12
  125. package/src/api-budget-accounting.ts +3 -3
  126. package/src/area-memory.ts +6 -6
  127. package/src/artifact-path-materialize.ts +1 -1
  128. package/src/async-guard.ts +5 -5
  129. package/src/attachment-cache.ts +17 -22
  130. package/src/background-script-reconcile.ts +6 -6
  131. package/src/bounded-log.ts +1 -1
  132. package/src/busy-reconciler.ts +22 -22
  133. package/src/claim-tracker.ts +3 -3
  134. package/src/claude-settings.ts +8 -8
  135. package/src/claude-system-prompt.ts +1 -1
  136. package/src/claude-workspace-trust.ts +1 -1
  137. package/src/config.ts +62 -61
  138. package/src/context-boundary.ts +5 -5
  139. package/src/context-command.ts +3 -3
  140. package/src/control-server.ts +32 -32
  141. package/src/delivery-result.ts +2 -2
  142. package/src/effort-command.ts +2 -2
  143. package/src/empty-turn.ts +22 -14
  144. package/src/free-port.ts +1 -1
  145. package/src/gate-run-config.ts +3 -3
  146. package/src/host-asset-plan.ts +24 -17
  147. package/src/host-claude-config-worker.ts +2 -2
  148. package/src/host-sources.ts +2 -2
  149. package/src/index.ts +16 -16
  150. package/src/launch-assembly.ts +94 -96
  151. package/src/liveness.ts +15 -15
  152. package/src/logger.ts +3 -3
  153. package/src/mcp-outbox.ts +3 -3
  154. package/src/mcp-preflight.ts +2 -2
  155. package/src/mcp-source-selection.ts +2 -2
  156. package/src/mcp-tool-deferral.ts +1 -1
  157. package/src/mcp-vault-env.ts +2 -2
  158. package/src/message-body-config/config.ts +4 -4
  159. package/src/mid-turn-delivery.ts +2 -2
  160. package/src/native-memory-lever.ts +8 -8
  161. package/src/outbox.ts +15 -15
  162. package/src/pending-permission-store.ts +1 -1
  163. package/src/profile-home.ts +19 -19
  164. package/src/profile-projection.ts +26 -24
  165. package/src/project-instructions.ts +5 -5
  166. package/src/provider-auth-warning.ts +17 -0
  167. package/src/provider-double.ts +1 -1
  168. package/src/provider-hold.ts +3 -3
  169. package/src/provider-home-marker.ts +1 -1
  170. package/src/provider-home-tombstone.ts +1 -1
  171. package/src/provider-input-queue.ts +2 -2
  172. package/src/provider-integration.ts +15 -13
  173. package/src/provider-setup.ts +15 -3
  174. package/src/providers.ts +1 -1
  175. package/src/provisioning.ts +37 -40
  176. package/src/question-decision.ts +2 -2
  177. package/src/quota.ts +0 -6
  178. package/src/rate-limit.ts +24 -13
  179. package/src/refused-prompt-store.ts +2 -2
  180. package/src/relay-injection-events.ts +2 -2
  181. package/src/relay-instructions.ts +5 -5
  182. package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
  183. package/src/relay-mcp-proxy.ts +57 -58
  184. package/src/relay-mcp.ts +55 -69
  185. package/src/reply-obligation-cache.ts +9 -9
  186. package/src/response-capture-report.ts +9 -9
  187. package/src/response-routing.ts +1 -1
  188. package/src/runner-core.ts +209 -222
  189. package/src/runner-exit.ts +1 -1
  190. package/src/runner-helpers.ts +10 -10
  191. package/src/runner-insights.ts +4 -2
  192. package/src/runner-mcp-proxy.ts +3 -3
  193. package/src/runner-spawn-config.ts +3 -3
  194. package/src/runtime-token-file.ts +1 -1
  195. package/src/runtime-token-remint.ts +1 -1
  196. package/src/service-access.ts +2 -2
  197. package/src/session-destroy.ts +1 -1
  198. package/src/session-insights.ts +1 -1
  199. package/src/session-scratch.ts +24 -37
  200. package/src/spawn-input.ts +6 -6
  201. package/src/startup-config-fetch.ts +4 -4
  202. package/src/subreaper.ts +3 -5
  203. package/src/test-run-dedup.ts +13 -13
  204. package/src/transcript-context.ts +2 -2
  205. package/src/transport-errors.ts +1 -1
  206. package/src/turn-opener.ts +1 -1
  207. package/src/turn-reconcile-event.ts +2 -2
  208. package/src/vault-proxy-hop.ts +2 -2
  209. package/src/web-tools.ts +2 -2
  210. package/agent-bin/agent-relay-browser +0 -10
  211. package/src/adapters/provider-quota-harvest.ts +0 -76
@@ -10,7 +10,7 @@ import { openCodeBackend, openCodeCustomBackendIDs, openCodeHostAuthKey, openCod
10
10
  import { openCodeMessageModel } from "./opencode-token-usage-capture";
11
11
 
12
12
  /**
13
- * #2659 — the host's opencode model registry, used to normalize a launch model before spawn.
13
+ * the host's opencode model registry, used to normalize a launch model before spawn.
14
14
  *
15
15
  * Primary: `opencode models --pure` run with the launch env but isolated XDG config/data (the
16
16
  * spawned server gets fresh isolated homes too), so it lists exactly the providers the server
@@ -55,7 +55,7 @@ export async function probeOpenCodeModelRegistry(
55
55
  extraProviders: string[],
56
56
  deps: OpenCodeRegistryProbeDeps = {},
57
57
  ): Promise<OpenCodeModelRegistry | undefined> {
58
- const scratch = mkdtempSync(join(tmpdir(), "agent-relay-opencode-models-"));
58
+ const scratch = mkdtempSync(join(tmpdir(), "humwick-opencode-models-"));
59
59
  try {
60
60
  const { exitCode, stdout } = await (deps.run ?? runProbe)(
61
61
  [command, "models", "--pure"],
@@ -89,7 +89,7 @@ export function cachedOpenCodeModelRegistry(command: string, env: Record<string,
89
89
  }
90
90
 
91
91
  /**
92
- * #2659 — normalize the launch model against the host's opencode registry BEFORE the server
92
+ * normalize the launch model against the host's opencode registry BEFORE the server
93
93
  * starts: a registry-listed id passes through unchanged, and an unknown/ambiguous id fails the
94
94
  * spawn here with candidates instead of dying later as a provider 400.
95
95
  */
@@ -99,13 +99,13 @@ export async function resolveOpenCodeLaunchModel(
99
99
  provider: string,
100
100
  registryFor: typeof cachedOpenCodeModelRegistry = cachedOpenCodeModelRegistry,
101
101
  ): Promise<OpenCodeModelId> {
102
- const env: Record<string, string> = writeChildEnv({}, { ...config.env, ...providerConfig.env, PATH: config.env.PATH ?? providerConfig.env.PATH ?? process.env.PATH ?? "" });
103
- // #2866 — the registry probe runs under fresh XDG roots too, so a hostAuth backend only lists
102
+ const env: Record<string, string> = writeChildEnv({}, { ...config.env, ...providerConfig.env, PATH: config.env.PATH ?? providerConfig.env.PATH ?? process.env.PATH ?? "" }, []);
103
+ // the registry probe runs under fresh XDG roots too, so a hostAuth backend only lists
104
104
  // its models when the host login key rides in as the backend's env credential.
105
105
  const { backend } = openCodeBackend(provider);
106
106
  if (backend.hostAuth && !relayEnv(env, backend.apiKeyEnvVar)) {
107
107
  const hostKey = openCodeHostAuthKey(backend.opencodeProviderID, env);
108
- if (hostKey) writeChildEnv(env, { [backend.apiKeyEnvVar]: hostKey });
108
+ if (hostKey) writeChildEnv(env, { [backend.apiKeyEnvVar]: hostKey }, []);
109
109
  }
110
110
  const registry = await registryFor(providerConfig.command || "opencode", env, openCodeCustomBackendIDs());
111
111
  const launch = openCodeLaunchModel(config.model, provider, openCodeModelRecord(config.modelRecord, provider), registry);
@@ -114,7 +114,7 @@ export async function resolveOpenCodeLaunchModel(
114
114
  }
115
115
 
116
116
  /**
117
- * #2659 — a model the human picked in the attached TUI rides on that prompt's user row
117
+ * a model the human picked in the attached TUI rides on that prompt's user row
118
118
  * (`info.model = {providerID, modelID}`). Adopt it as the session model so the next Relay chat
119
119
  * turn keeps it instead of re-sending the launch model.
120
120
  */
@@ -19,7 +19,7 @@ export function openCodeBackend(provider: string): OpenCodeBackend {
19
19
  }
20
20
 
21
21
  /**
22
- * #2599 — resolve the payload record for THIS provider, or reject an unknown provider LOUD.
22
+ * resolve the payload record for THIS provider, or reject an unknown provider LOUD.
23
23
  * A record whose providerModel/alias resolves against a DIFFERENT registered provider is a
24
24
  * routing error, not a fallback: silently running it under the wrong backend namespace would
25
25
  * launch the wrong model. The manifest fallback only applies when NO record is present at all.
@@ -36,7 +36,7 @@ export function openCodeBackendApiKeyEnvVar(backendID: string): string | undefin
36
36
  }
37
37
 
38
38
  /**
39
- * #2659 — the launch model: THE one normalization of a spawn's model through the shared seam
39
+ * the launch model: THE one normalization of a spawn's model through the shared seam
40
40
  * (`resolveOpenCodeModelId`). A server-threaded record's qualified name wins over the alias.
41
41
  * With a host registry (spawn), unknown/ambiguous ids throw with candidates; without one
42
42
  * (static fallback) a known-backend first segment is structural, anything else is a
@@ -61,7 +61,7 @@ export function openCodeModelDescriptor(model: string | undefined, provider = "o
61
61
  }
62
62
 
63
63
  /**
64
- * #2659 — the per-turn descriptor for an ALREADY-normalized qualified name (process meta.model).
64
+ * the per-turn descriptor for an ALREADY-normalized qualified name (process meta.model).
65
65
  * Never re-resolves: re-running the static split here re-prefixed live-only backends with the
66
66
  * default backend on every chat turn.
67
67
  */
@@ -72,7 +72,7 @@ export function openCodeTurnDescriptor(qualified: string, provider = "opencode")
72
72
  }
73
73
 
74
74
  /**
75
- * #2600 — the config-block + credential-env half of the per-model backend decision: the
75
+ * the config-block + credential-env half of the per-model backend decision: the
76
76
  * openCodeProviderID to key `provider.<id>` under, the apiKey env var the config references and
77
77
  * the launch env must carry (undefined for a live-registry backend Relay has no credential
78
78
  * mapping for — opencode then authenticates it from the inherited env itself), and any
@@ -111,7 +111,7 @@ export function openCodeContextWindowTokens(_model: string | undefined, provider
111
111
  }
112
112
 
113
113
  /**
114
- * #2599 — the record's explicit threshold, when present (precedence payload > manifest-derived).
114
+ * the record's explicit threshold, when present (precedence payload > manifest-derived).
115
115
  * Returns undefined when the record carries none — the caller then derives from the window.
116
116
  */
117
117
  export function openCodeContextThreshold(record: ResolvedModelRecord | undefined, provider = "opencode"): ResolvedModelRecord["contextThreshold"] {
@@ -121,7 +121,7 @@ export function openCodeContextThreshold(record: ResolvedModelRecord | undefined
121
121
  }
122
122
 
123
123
  /**
124
- * #2600/#2659 — the selected backend's credential from the runner or provider-config env
124
+ * the selected backend's credential from the runner or provider-config env
125
125
  * (`||`: an empty runner var must not mask a provider-config key). A live-registry backend
126
126
  * without a Relay credential mapping has none — opencode authenticates it from the inherited env.
127
127
  */
@@ -133,7 +133,7 @@ export function openCodeBackendCredential(apiKeyEnvVar: string | undefined, runn
133
133
  }
134
134
 
135
135
  /**
136
- * #2866 — the host's own `opencode auth login` key (`<data home>/opencode/auth.json`), which the
136
+ * the host's own `opencode auth login` key (`<data home>/opencode/auth.json`), which the
137
137
  * isolated XDG launch hides from the server. Only for manifests with `opencode.hostAuth`.
138
138
  */
139
139
  export function openCodeHostAuthKey(backendID: string, env: Record<string, string>): string | undefined {
@@ -147,7 +147,7 @@ export function openCodeHostAuthKey(backendID: string, env: Record<string, strin
147
147
  }
148
148
  }
149
149
 
150
- /** #2659 — is the session still on its launch model (no terminal model switch adopted)? */
150
+ /** is the session still on its launch model (no terminal model switch adopted)? */
151
151
  export function openCodeOnLaunchModel(meta: Record<string, unknown> | undefined, model: string | undefined): boolean {
152
152
  const launchModel = typeof meta?.launchModel === "string" ? meta.launchModel : undefined;
153
153
  return !launchModel || !model || model === launchModel;
@@ -0,0 +1,30 @@
1
+ import { join } from "node:path";
2
+ import { writeFileSync } from "node:fs";
3
+ import { sanitizeFsName } from "@humwick/sdk/fs-name";
4
+ import type { RunnerSpawnConfig } from "../adapter";
5
+ import { providerHomeRootFromEnv } from "../config";
6
+ import { bundledIntegrationAssets } from "../provider-integration";
7
+ import { materializeResolvedAssets, resolveProvisionedSkillPaths } from "../provisioning";
8
+ import { profileAllowsRelayFeature, resolveRelayContext } from "../relay-instructions";
9
+ import { launchWebTools } from "../web-tools";
10
+
11
+ export function openCodeHome(config: RunnerSpawnConfig, provider = config.provider): string {
12
+ const profile = sanitizeFsName(config.agentProfile?.name || config.profile || "default", { replacement: "-", lowercase: true });
13
+ const instance = sanitizeFsName(config.instanceId || config.runnerId, { replacement: "-", lowercase: true });
14
+ return join(providerHomeRootFromEnv(), provider, profile, instance);
15
+ }
16
+
17
+ export function resolveOpenCodeAssets(config: RunnerSpawnConfig) {
18
+ const configDir = join(openCodeHome(config), "config", "opencode");
19
+ const provisioned = resolveProvisionedSkillPaths(configDir, config);
20
+ const bundled = profileAllowsRelayFeature(config, "skills") ? bundledIntegrationAssets(config) : [];
21
+ const context = resolveRelayContext({ slug: "relay.context.opencode", config, webTools: { names: launchWebTools(config), claude: false } });
22
+ const instructions = [context.text, config.systemPromptAppend?.trim()].filter(Boolean).join("\n\n");
23
+ return { configDir, provisioned, bundled, skills: [...provisioned, ...bundled], context,
24
+ instructions: instructions ? { path: join(configDir, "humwick-instructions.md"), text: instructions } : undefined };
25
+ }
26
+
27
+ export function materializeOpenCodeAssets(assets: ReturnType<typeof resolveOpenCodeAssets>): void {
28
+ materializeResolvedAssets(assets.skills);
29
+ if (assets.instructions) writeFileSync(assets.instructions.path, assets.instructions.text, { mode: 0o600 });
30
+ }
@@ -1,4 +1,4 @@
1
- // #2775 — OpenCode's OWN in-session retry. On a retryable provider error OpenCode does not end the
1
+ // OpenCode's OWN in-session retry. On a retryable provider error OpenCode does not end the
2
2
  // turn: it reports `session.status` `{ type: "retry", attempt, message, next, action? }` and keeps
3
3
  // the synchronous /message POST open until it succeeds. A free-tier wall schedules `next` for the
4
4
  // daily reset (hours out), so the agent sat busy and silent. These pure helpers decide when that
@@ -1,4 +1,4 @@
1
- // #2539 Phase B — OpenCode/ZAI non-destructive soft-steer lane.
1
+ // Phase B — OpenCode/ZAI non-destructive soft-steer lane.
2
2
  //
3
3
  // The soft op is `POST /session/:id/message` with a CALLER messageID + `noReply:true`: it
4
4
  // persists a user row WITHOUT joining/cancelling the active run, and the run's loop reloads
@@ -24,9 +24,9 @@ import { logger } from "../logger";
24
24
  * The one-row-per-batch marker the design mandates: tells the model the payload is a steer, not
25
25
  * a new assignment, and that completed work must not be discarded to act on it.
26
26
  */
27
- export const OPENCODE_SOFT_STEER_MARKER = "Agent Relay soft steer: incorporate at next safe step; do not discard completed work.";
27
+ export const OPENCODE_SOFT_STEER_MARKER = "Humwick soft steer: incorporate at next safe step; do not discard completed work.";
28
28
 
29
- /** Short deadlines only — never the 60m whole-turn budget (#2539 design F). */
29
+ /** Short deadlines only — never the 60m whole-turn budget. */
30
30
  export const OPENCODE_SOFT_STEER_POST_TIMEOUT_MS = 5_000;
31
31
  export const OPENCODE_SOFT_STEER_RECONCILE_TIMEOUT_MS = 5_000;
32
32
 
@@ -41,7 +41,7 @@ export const OPENCODE_SOFT_STEER_MAX_BATCH_BYTES = 48 * 1024;
41
41
  * `send()` turn gives the wake a real busy/idle pair and the full retry semantics instead of a
42
42
  * second, parallel turn machinery.
43
43
  */
44
- export const OPENCODE_SOFT_STEER_WAKE_TEXT = "Agent Relay: soft-steer guidance above is pending. Incorporate it now at this safe step; do not discard completed work. If nothing needs incorporating, reply with a single line: ACK.";
44
+ export const OPENCODE_SOFT_STEER_WAKE_TEXT = "Humwick: soft-steer guidance above is pending. Incorporate it now at this safe step; do not discard completed work. If nothing needs incorporating, reply with a single line: ACK.";
45
45
 
46
46
  /**
47
47
  * Caller-side OpenCode messageID for a soft batch — `msg_` prefix (the shape the server accepts
@@ -99,7 +99,7 @@ export interface SoftSteerEntry {
99
99
  }
100
100
 
101
101
  /**
102
- * #2539 Phase A reconciliation — the ledger's settlement exits are the RUNNER's seams, handed in
102
+ * Phase A reconciliation — the ledger's settlement exits are the RUNNER's seams, handed in
103
103
  * through ProviderDeliverContext (settleMidTurnConsumed / reofferMidTurnIds). The ledger never
104
104
  * settles on its own: an absent callback is a fail-safe hold, never a silent settle.
105
105
  */
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { sanitizeFsName } from "@humwick/sdk/fs-name";
5
5
  import { providerScratchEnvOf } from "@humwick/sdk/provider-scratch";
6
6
  import { openCodeProjectShellEnv } from "./project-shell";
7
- import { providerHomeRootFromEnv } from "../config";
7
+ import { openCodeHome, resolveOpenCodeAssets, materializeOpenCodeAssets } from "./opencode-provisioning";
8
8
  import { materializeProviderHome } from "../profile-home";
9
9
  import { syncOpenCodeRedactPlugin } from "../agent-redact";
10
10
  import type { ProviderConfig, RunnerSpawnConfig, SpawnArgs } from "../adapter";
@@ -40,14 +40,14 @@ export function assertOpenCodeManagedServerArgs(providerArgs: string[]): void {
40
40
  }
41
41
  }
42
42
 
43
- /** Prevent config substitution in caller-supplied model names (#2378). */
43
+ /** Prevent config substitution in caller-supplied model names. */
44
44
  export function assertOpenCodeSafeModel(model: string | undefined): void {
45
45
  if (model?.includes("{")) {
46
46
  throw new Error(`OpenCode model must not contain "{" (would be spliced by OpenCode's own {env:...}/{file:...} config substitution): received ${JSON.stringify(model)}`);
47
47
  }
48
48
  }
49
49
 
50
- /** Preserve host gh auth while isolating OpenCode XDG state (#2531). */
50
+ /** Preserve host gh auth while isolating OpenCode XDG state. */
51
51
  export function resolveGhConfigDir(inputEnv: Record<string, string | undefined>): string | undefined {
52
52
  const explicit = inputEnv.GH_CONFIG_DIR;
53
53
  if (explicit) return explicit;
@@ -64,14 +64,14 @@ export function buildOpenCodeSpawnArgs(config: RunnerSpawnConfig, providerConfig
64
64
  assertOpenCodeSafeModel(config.model);
65
65
  assertOpenCodeSafeModel(config.modelRecord?.qualifiedModel);
66
66
  openCodeBackend(provider);
67
- // The selected model owns its backend and credential (#2599/#2600).
67
+ // The selected model owns its backend and credential.
68
68
  const resolved = openCodeResolvedBackend(launch.backendID, provider);
69
69
  // `||`, not `??`: an empty-string runner env var must not mask a valid providerConfig key.
70
70
  const apiKey = openCodeApiKey(config, launch.qualified) ?? openCodeBackendCredential(resolved.apiKeyEnvVar, config.env, providerConfig.env, resolved.hostAuth ? resolved.opencodeProviderID : undefined);
71
71
  const assembled = assembleLaunch(provider, config, providerConfig);
72
72
  const profile = sanitizeFsName(config.agentProfile?.name || config.profile || "default", { replacement: "-", lowercase: true });
73
73
  const instanceId = sanitizeFsName(config.instanceId || config.runnerId, { replacement: "-", lowercase: true });
74
- const root = join(providerHomeRootFromEnv(), provider, profile, instanceId);
74
+ const root = openCodeHome(config, provider);
75
75
  materializeProviderHome(root, { provider: provider, profile, instanceId, agentId: config.agentId }, 0o700);
76
76
  const xdgConfig = join(root, "config");
77
77
  const xdgData = join(root, "data");
@@ -79,14 +79,18 @@ export function buildOpenCodeSpawnArgs(config: RunnerSpawnConfig, providerConfig
79
79
  const xdgCache = join(root, "cache");
80
80
  const configDir = join(xdgConfig, "opencode");
81
81
  for (const path of [xdgConfig, xdgData, xdgState, xdgCache, configDir]) mkdirSync(path, { recursive: true, mode: 0o700 });
82
+ const assets = resolveOpenCodeAssets(config);
83
+ materializeOpenCodeAssets(assets);
82
84
  // Permission is pinned in the isolated server config. A guarded/read-only request rejects
83
85
  // before this file is written, so a policy downgrade can never become a permissive launch.
84
86
  const configFile = join(configDir, "opencode.json");
85
87
  const mcp = openCodeMcpConfigForLaunch(config);
86
- // Keep credentials out of disk config using native env references (#2378).
88
+ // Keep credentials out of disk config using native env references.
87
89
  writeFileSync(configFile, `${JSON.stringify({
88
90
  $schema: "https://opencode.ai/config.json",
89
91
  model: launch.qualified,
92
+ ...(assets.instructions ? { instructions: [assets.instructions.path] } : {}),
93
+ ...(assets.skills.length ? { skills: { paths: assets.skills.map(skill => skill.dir) } } : {}),
90
94
  ...(launch.backendID === "openai" ? { small_model: launch.qualified, enabled_providers: ["openai"] } : {}),
91
95
  provider: {
92
96
  [resolved.opencodeProviderID]: {
@@ -100,11 +104,11 @@ export function buildOpenCodeSpawnArgs(config: RunnerSpawnConfig, providerConfig
100
104
  permission: openCodePermissions(config), ...(Object.keys(mcp).length ? { mcp } : {}), ...openCodeToolsConfig(config, Object.keys(mcp).length > 0),
101
105
  }, null, 2)}\n`, { mode: 0o600 });
102
106
  chmodSync(configFile, 0o600);
103
- // #3025 — `--pure` skips every plugin, so it is dropped only when agent-redact's plugin is in place.
107
+ // `--pure` skips every plugin, so it is dropped only when agent-redact's plugin is in place.
104
108
  const pure = syncOpenCodeRedactPlugin(configDir, assembled.redaction) ? [] : ["--pure"];
105
- // Bridge gh from the host env before overlaying isolated XDG roots (#2531).
109
+ // Bridge gh from the host env before overlaying isolated XDG roots.
106
110
  const ghConfigDir = resolveGhConfigDir({ ...config.env, ...providerConfig.env, ...assembled.env });
107
- // #2719 — allocated, never derived from another port. `controlPort + 1_000` overflowed 65535
111
+ // allocated, never derived from another port. `controlPort + 1_000` overflowed 65535
108
112
  // whenever the OS handed the runner a high ephemeral control port (macOS: 49152-65535), and
109
113
  // `serve --port 66178` did not fail loudly: opencode bound a different port, so readiness
110
114
  // polled the wrong URL for 20s and the runner exited 1 with no report.
@@ -113,12 +117,12 @@ export function buildOpenCodeSpawnArgs(config: RunnerSpawnConfig, providerConfig
113
117
  const env: SpawnArgs["env"] = writeChildEnv({}, {
114
118
  ...config.env,
115
119
  ...providerConfig.env,
116
- // #3084 — the runner already nested the agent's private TMPDIR under any provider-config one;
117
- // the shell hooks keep it (and #2947's toolchain PATH) after a login profile runs.
120
+ // the runner already nested the agent's private TMPDIR under any provider-config one;
121
+ // the shell hooks keep it (and the toolchain PATH) after a login profile runs.
118
122
  ...providerScratchEnvOf(config.env),
119
123
  ...openCodeProjectShellEnv(config.env),
120
124
  ...assembled.env,
121
- // #2531 — gh (and git credential helpers wrapping gh) read the HOST gh config even
125
+ // gh (and git credential helpers wrapping gh) read the HOST gh config even
122
126
  // under the isolated XDG roots below. No other credential surface is bridged.
123
127
  ...(ghConfigDir ? { GH_CONFIG_DIR: ghConfigDir } : {}),
124
128
  XDG_CONFIG_HOME: xdgConfig,
@@ -131,8 +135,8 @@ export function buildOpenCodeSpawnArgs(config: RunnerSpawnConfig, providerConfig
131
135
  ...(resolved.apiKeyEnvVar && apiKey ? { [resolved.apiKeyEnvVar]: apiKey } : {}),
132
136
  OPENCODE_SERVER_PASSWORD: crypto.randomUUID(),
133
137
  OPENCODE_SERVER_URL: serverUrl,
134
- AGENT_RELAY_PROVIDER: provider,
135
- });
138
+ HUMWICK_PROVIDER: provider,
139
+ }, []);
136
140
  if (launch.backendID === "openai") {
137
141
  for (const key of ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "OPENCODE_API_KEY"]) writeChildEnv(env, {}, [key]);
138
142
  }
@@ -1,13 +1,13 @@
1
- // #2484 — per-turn OpenCode/Zai token-usage capture, reusing the #2482/#2485 spine seam
1
+ // per-turn OpenCode/Zai token-usage capture, reusing the spine seam
2
2
  // (ProviderTokenUsageReporter + POST /api/agents/:id/token-usage, validated live via Claude's
3
- // #2491 capture). Unlike Claude, OpenCode never hands the runner a transcript file to tail: the
3
+ // capture). Unlike Claude, OpenCode never hands the runner a transcript file to tail: the
4
4
  // `message.updated` SSE door already carries a structured per-assistant-message `tokens` object
5
5
  // once a message finalizes — `{ input, output, reasoning, cache: { read, write } }`, confirmed
6
- // live against a real captured v1.18.21 session (see opencode-context.ts's #2355 note beside the
6
+ // live against a real captured v1.18.21 session (see opencode-context.ts's note beside the
7
7
  // same wire shape). So there is no incremental-file tail here: this module is a pure decoder from
8
8
  // that object to the spine's required breakdown, called straight from the SSE handler.
9
9
  //
10
- // Dedupe: the door fires more than once per message as it streams toward its final total (#2427).
10
+ // Dedupe: the door fires more than once per message as it streams toward its final total.
11
11
  // Keying on the message id makes every echo collapse onto the SAME spine row via the dedupe_key
12
12
  // upsert (`created_at >= existing` replaces) — the row simply refines towards the message's true
13
13
  // total rather than posting a new one per frame, exactly like Claude's uuid-keyed entries collapse
@@ -35,7 +35,7 @@ export function openCodeTokenUsageRecord(input: {
35
35
  messageId: string;
36
36
  tokens: unknown;
37
37
  createdAt?: number;
38
- /** #3351 — the message is final, so unreadable counts mean unknown spend, not "not yet". */
38
+ /** the message is final, so unreadable counts mean unknown spend, not "not yet". */
39
39
  finished?: boolean;
40
40
  }): OpenCodeTokenUsageSpineRecord | undefined {
41
41
  const unknown = (): OpenCodeTokenUsageSpineRecord | undefined => input.finished ? {
@@ -53,7 +53,7 @@ export function openCodeTokenUsageRecord(input: {
53
53
  const cacheReadTokens = numberOrZero(cache?.read);
54
54
  const cacheWriteTokens = numberOrZero(cache?.write);
55
55
  // A recognised-but-entirely-empty object carries no signal — the same no-signal rule
56
- // `openCodeContextState` applies to a `tokensUsed:0` reading of this identical shape (#2355
56
+ // `openCodeContextState` applies to a `tokensUsed:0` reading of this identical shape (
57
57
  // review MED-1: a genuinely engaged assistant message always has input > 0).
58
58
  if (inputTokens === 0 && outputTokens === 0 && reasoningOutputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) return unknown();
59
59
  return {
@@ -71,9 +71,9 @@ export function openCodeTokenUsageRecord(input: {
71
71
  }
72
72
 
73
73
  /**
74
- * #2484 round 1 review (F6) — the message's OWN observed usage time: `time.completed` once
74
+ * round 1 review (F6) — the message's OWN observed usage time: `time.completed` once
75
75
  * finalized, else `time.created` (present on every AssistantMessage, per opencode-context.ts's
76
- * #2355 note on this same wire family). Reading this instead of `Date.now()` at report time
76
+ * note on this same wire family). Reading this instead of `Date.now` at report time
77
77
  * removes the equal-millisecond upsert race for the TERMINAL echo specifically: every
78
78
  * streaming echo of a still-open message shares the SAME `time.created`, and the terminal
79
79
  * echo's own `time.completed` is always `>=` it — so the spine's `created_at >= existing`
@@ -98,10 +98,10 @@ export function openCodeMessageFinished(info: unknown): boolean {
98
98
  }
99
99
 
100
100
  /**
101
- * #2484 round 1 review (F5) — the model the message was actually generated with, read off its own
101
+ * round 1 review (F5) — the model the message was actually generated with, read off its own
102
102
  * `providerID`/`modelID` (the same shape `openCodeModelDescriptor` sends on the request) rather
103
103
  * than the session's currently-configured model, which can have moved on (a mid-session
104
- * model/provider swap, a #2397 retry re-roll) by the time this echo is processed.
104
+ * model/provider swap, a retry re-roll) by the time this echo is processed.
105
105
  */
106
106
  export function openCodeMessageModel(info: unknown): string | undefined {
107
107
  if (!isRecord(info)) return undefined;
@@ -113,7 +113,7 @@ export function openCodeMessageModel(info: unknown): string | undefined {
113
113
  const TOKEN_USAGE_DEBOUNCE_MS = 300;
114
114
 
115
115
  /**
116
- * #2484 round 1 review (F2/F3) — the `message.updated` door fires repeatedly as one message
116
+ * round 1 review (F2/F3) — the `message.updated` door fires repeatedly as one message
117
117
  * streams to completion; posting on every echo means an HTTP POST + SQLite write per frame,
118
118
  * fleet-wide, for a value the very next echo immediately supersedes. Trailing-debounce per
119
119
  * message id: each new echo replaces the pending record and resets the timer, so a whole
@@ -137,7 +137,7 @@ export class OpenCodeTokenUsagePoster {
137
137
  this.pending.set(record.dedupeKey, { record, timer });
138
138
  }
139
139
 
140
- // #2484 round 2 review (MED-1) — returns the in-flight delivery so a teardown caller can
140
+ // round 2 review (MED-1) — returns the in-flight delivery so a teardown caller can
141
141
  // actually await it (previously fire-and-forget: a caller that returned right after calling
142
142
  // this had no guarantee the POST had even started, let alone landed — the repo's
143
143
  // persisted-is-not-delivered shape). A live turn-boundary call (closeSuppressedTurn/
@@ -1,5 +1,5 @@
1
1
  /**
2
- * #2354 — decode OpenCode's `message.part.updated` tool parts (v1.18.21 `ToolPart`) into the
2
+ * decode OpenCode's `message.part.updated` tool parts (v1.18.21 `ToolPart`) into the
3
3
  * provider-neutral tool-step shape the chat/timeline already renders (TraceView `ToolCallRow`).
4
4
  *
5
5
  * `opencode.ts` is near the module-size ceiling, so the wire-decoding lives here as a small,
@@ -31,16 +31,16 @@ export interface OpenCodeToolStep {
31
31
  status: OpenCodeToolStepStatus;
32
32
  /** Safe one-line summary (redacted + clipped) of what the tool is doing. */
33
33
  body: string;
34
- /** #2437 — redacted, size-capped tool input. Only present on the "running" state (the ToolPart
34
+ /** redacted, size-capped tool input. Only present on the "running" state (the ToolPart
35
35
  * wire shape drops `input` once the tool completes) — undefined here means "not captured on
36
36
  * THIS decode", not "no params exist"; see `emitToolStep`'s stepId cache for the carry-forward
37
37
  * onto the terminal state. */
38
38
  params?: unknown;
39
- /** #3411 — absolute paths a file tool reads or edits, from the raw input (running state only). */
39
+ /** absolute paths a file tool reads or edits, from the raw input (running state only). */
40
40
  paths?: string[];
41
41
  }
42
42
 
43
- /** OpenCode built-in tool name → the provider-independent category taxonomy (#1498). Anything
43
+ /** OpenCode built-in tool name → the provider-independent category taxonomy. Anything
44
44
  * not a known built-in (an MCP tool, a future built-in) falls to the mcp/generic default —
45
45
  * never force-fit, so nothing is hidden from the trace. */
46
46
  const OPENCODE_TOOL_CATEGORIES: Record<string, ToolCallCategory> = {
@@ -56,7 +56,7 @@ const OPENCODE_TOOL_CATEGORIES: Record<string, ToolCallCategory> = {
56
56
  todoread: "plan",
57
57
  };
58
58
 
59
- /** #3411 — OpenCode's file tools; each names its file in `input.filePath`. */
59
+ /** OpenCode's file tools; each names its file in `input.filePath`. */
60
60
  const OPENCODE_FILE_TOOLS = new Set(["read", "edit", "write", "multiedit"]);
61
61
 
62
62
  /** Extra input-field names OpenCode's built-in tools use that aren't in the shared candidate
@@ -67,7 +67,7 @@ export function openCodeToolCategory(tool: string): ToolCallCategory {
67
67
  const known = OPENCODE_TOOL_CATEGORIES[tool.toLowerCase()];
68
68
  if (known) return known;
69
69
  // MCP tools surface through OpenCode with a namespaced name; the `__` convention (shared with
70
- // Claude's `mcp__server__tool`) is the safe signal. Everything else is generic (#2354 rule:
70
+ // Claude's `mcp__server__tool`) is the safe signal. Everything else is generic (rule:
71
71
  // unknown tool → generic), so a mystery tool still renders rather than vanishing.
72
72
  if (tool.includes("__") || tool.toLowerCase().startsWith("mcp")) return "mcp-tool-call";
73
73
  return "generic";
@@ -169,7 +169,7 @@ export function openCodeToolSessionEvent(step: OpenCodeToolStep, turnId: string
169
169
  }
170
170
 
171
171
  /**
172
- * #2413 — concatenate the terminal assistant message's text parts (wire order) into the turn-final
172
+ * concatenate the terminal assistant message's text parts (wire order) into the turn-final
173
173
  * response text. Non-text parts (tool, step-start/finish, reasoning) are skipped; the `\n\n` join
174
174
  * matches the dashboard's block delimiter, so a multi-part response's leading blocks dedupe against
175
175
  * the streamed narration. `undefined` when there is no assistant text.
@@ -186,7 +186,7 @@ export function openCodeAssistantText(parts: unknown[] | undefined): string | un
186
186
  }
187
187
 
188
188
  /**
189
- * #2605 — decode one `message.part.updated` reasoning part (`{ type: "reasoning", text }`,
189
+ * decode one `message.part.updated` reasoning part (`{ type: "reasoning", text }`,
190
190
  * pinned v1.18.21, live-verified in the provider's own SQLite: opencode/minimax-m2.5-free AND
191
191
  * kimi-for-coding sessions persist `{type:"reasoning",text}` parts) into the provider-neutral
192
192
  * emit fields. Pure — the adapter owns the dedup/emission, exactly like decodeOpenCodeToolPart.
@@ -202,7 +202,7 @@ export function decodeOpenCodeReasoningPart(part: unknown): { stepId: string; bo
202
202
  return { stepId, body };
203
203
  }
204
204
 
205
- /** #2605 — build the `reasoning` session event the chat/timeline consumes (TraceView reasoning
205
+ /** build the `reasoning` session event the chat/timeline consumes (TraceView reasoning
206
206
  * row): `stepId` = the stable part id, so the server upserts the row in place on growth instead
207
207
  * of appending a duplicate — the same stable-key shape the Codex adapter flushes with.
208
208
  * `streaming` stays true while the part is unfinished (`time.end` unset), false on finalize. */
@@ -218,7 +218,7 @@ export function openCodeReasoningSessionEvent(step: { stepId: string; body: stri
218
218
  }
219
219
 
220
220
  /**
221
- * #2605 — one decoded reasoning part with its provider-side identity. The `id` is the wire that
221
+ * one decoded reasoning part with its provider-side identity. The `id` is the wire that
222
222
  * makes the turn-final backstop upsert instead of duplicate: the server keys step rows on
223
223
  * `stepId` ONLY, so every turn-final reasoning row must carry the SAME stepId the live SSE door
224
224
  * already emitted for that part (see openCodeReasoningSessionEvent) — a merged/concatenated
@@ -231,7 +231,7 @@ export interface OpenCodeReasoningStep {
231
231
  }
232
232
 
233
233
  /**
234
- * #2605 — decode the completed assistant message's reasoning parts (wire order) into
234
+ * decode the completed assistant message's reasoning parts (wire order) into
235
235
  * per-part steps. Mirrors openCodeAssistantText's part walk, but keeps identity: one entry per
236
236
  * non-empty reasoning part (empty text is backend muzak — dropped by decodeOpenCodeReasoningPart
237
237
  * — so it never occupies a row). `[]` when the message carried no reasoning.
@@ -247,7 +247,7 @@ export function openCodeAssistantReasoningSteps(parts: unknown[] | undefined): O
247
247
  }
248
248
 
249
249
  /**
250
- * #2605 — concatenate the terminal assistant message's reasoning parts (wire order) into the
250
+ * concatenate the terminal assistant message's reasoning parts (wire order) into the
251
251
  * turn-final reasoning text. `undefined` when the completed message carried no (non-empty)
252
252
  * reasoning. Kept for the completion log line / describeCompletion only — the EMIT path uses
253
253
  * openCodeAssistantReasoningSteps (per-part identity) so live+final collapse to one row.
@@ -257,7 +257,7 @@ export function openCodeAssistantReasoning(parts: unknown[] | undefined): string
257
257
  return steps.length ? steps.map((step) => step.body).join("\n\n") : undefined;
258
258
  }
259
259
 
260
- /** #2413 — build the turn-final `response` event (marks it for Final Response capture / auto-forward
260
+ /** build the turn-final `response` event (marks it for Final Response capture / auto-forward
261
261
  * and styles it apart from reasoning). `undefined` when the turn produced no assistant text. */
262
262
  export function openCodeTurnFinalResponseEvent(text: string | undefined, turnId: string | undefined): ProviderSessionEvent | undefined {
263
263
  const body = text?.trim();
@@ -266,7 +266,7 @@ export function openCodeTurnFinalResponseEvent(text: string | undefined, turnId:
266
266
  }
267
267
 
268
268
  /**
269
- * #2605 — emit the turn-final `reasoning` rows (`completion.reasoningSteps`, read from the SAME
269
+ * emit the turn-final `reasoning` rows (`completion.reasoningSteps`, read from the SAME
270
270
  * synchronous POST body as the response below) plus that response, in order. Each reasoning row
271
271
  * carries its OWN part id as stepId — the exact key the live SSE door already emitted — so the
272
272
  * server upserts live+final into ONE row instead of storing the turn's thinking twice. Pure
@@ -281,7 +281,7 @@ export function openCodeTurnFinalResponseEvent(text: string | undefined, turnId:
281
281
  * path) — comparison is scoped to THOSE part ids, never to a global body pool, so an identical
282
282
  * body in a later turn is never misread as already-seen.
283
283
  *
284
- * The text is ground truth per #2389: the POST returning IS the provider-native turn-end edge,
284
+ * The text is ground truth: the POST returning IS the provider-native turn-end edge,
285
285
  * hence `confidence:"definitive"` on the response; `final:true` is what makes it a turn-final
286
286
  * capture downstream. Emits reasoning first so the trace reads thinking-then-answer, the order
287
287
  * the model produced them in. Either half may be absent (reasoning-only or text-only turns).