@gajae-code/coding-agent 0.6.4 → 0.7.0

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 (231) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/migrate-cli.d.ts +20 -0
  5. package/dist/types/cli/notify-cli.d.ts +23 -0
  6. package/dist/types/cli/setup-cli.d.ts +20 -1
  7. package/dist/types/commands/daemon.d.ts +41 -0
  8. package/dist/types/commands/migrate.d.ts +33 -0
  9. package/dist/types/commands/notify.d.ts +41 -0
  10. package/dist/types/config/keybindings.d.ts +4 -0
  11. package/dist/types/config/model-profile-activation.d.ts +12 -0
  12. package/dist/types/config/model-profiles.d.ts +2 -1
  13. package/dist/types/config/model-registry.d.ts +3 -3
  14. package/dist/types/config/models-config-schema.d.ts +5 -0
  15. package/dist/types/config/settings-schema.d.ts +38 -0
  16. package/dist/types/coordinator/contract.d.ts +1 -1
  17. package/dist/types/daemon/builtin.d.ts +20 -0
  18. package/dist/types/daemon/control-types.d.ts +57 -0
  19. package/dist/types/daemon/runtime.d.ts +25 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  21. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  22. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  23. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  24. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  25. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  26. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  27. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  28. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  29. package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
  30. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  31. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  32. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  33. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  34. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  35. package/dist/types/hooks/skill-state.d.ts +12 -4
  36. package/dist/types/migrate/action-planner.d.ts +11 -0
  37. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  38. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  39. package/dist/types/migrate/adapters/index.d.ts +45 -0
  40. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  41. package/dist/types/migrate/executor.d.ts +2 -0
  42. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  43. package/dist/types/migrate/report.d.ts +18 -0
  44. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  45. package/dist/types/migrate/types.d.ts +126 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  47. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  48. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  49. package/dist/types/modes/interactive-mode.d.ts +1 -1
  50. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  51. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  52. package/dist/types/modes/types.d.ts +7 -1
  53. package/dist/types/notifications/config-commands.d.ts +26 -0
  54. package/dist/types/notifications/config.d.ts +61 -0
  55. package/dist/types/notifications/helpers.d.ts +55 -0
  56. package/dist/types/notifications/html-format.d.ts +62 -0
  57. package/dist/types/notifications/index.d.ts +28 -0
  58. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  59. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  60. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  61. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  62. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  63. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  64. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  65. package/dist/types/notifications/threaded-render.d.ts +66 -0
  66. package/dist/types/notifications/topic-registry.d.ts +67 -0
  67. package/dist/types/research-plan/index.d.ts +1 -0
  68. package/dist/types/research-plan/ledger.d.ts +33 -0
  69. package/dist/types/rlm/artifacts.d.ts +1 -1
  70. package/dist/types/rlm/index.d.ts +12 -0
  71. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  72. package/dist/types/session/agent-session.d.ts +39 -2
  73. package/dist/types/session/auth-storage.d.ts +1 -1
  74. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  75. package/dist/types/setup/credential-import.d.ts +3 -0
  76. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  77. package/dist/types/skill-state/active-state.d.ts +6 -11
  78. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  79. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  80. package/dist/types/task/spawn-gate.d.ts +1 -10
  81. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  82. package/dist/types/tools/index.d.ts +18 -0
  83. package/dist/types/tools/subagent.d.ts +3 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +3 -0
  86. package/src/async/job-manager.ts +5 -1
  87. package/src/cli/daemon-cli.ts +122 -0
  88. package/src/cli/migrate-cli.ts +106 -0
  89. package/src/cli/notify-cli.ts +274 -0
  90. package/src/cli/setup-cli.ts +173 -84
  91. package/src/cli.ts +3 -0
  92. package/src/commands/daemon.ts +47 -0
  93. package/src/commands/deep-interview.ts +2 -2
  94. package/src/commands/migrate.ts +46 -0
  95. package/src/commands/notify.ts +61 -0
  96. package/src/commands/setup.ts +11 -1
  97. package/src/commands/state.ts +2 -1
  98. package/src/commands/team.ts +7 -3
  99. package/src/config/model-profile-activation.ts +74 -5
  100. package/src/config/model-profiles.ts +7 -4
  101. package/src/config/model-registry.ts +6 -3
  102. package/src/config/models-config-schema.ts +1 -1
  103. package/src/config/settings-schema.ts +29 -0
  104. package/src/coordinator/contract.ts +3 -0
  105. package/src/coordinator-mcp/policy.ts +10 -2
  106. package/src/coordinator-mcp/server.ts +270 -1
  107. package/src/daemon/builtin.ts +46 -0
  108. package/src/daemon/control-types.ts +65 -0
  109. package/src/daemon/runtime.ts +51 -0
  110. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  111. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  112. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  113. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  114. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
  115. package/src/extensibility/custom-commands/loader.ts +0 -7
  116. package/src/extensibility/extensions/runner.ts +4 -0
  117. package/src/extensibility/extensions/types.ts +8 -0
  118. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  119. package/src/extensibility/gjc-plugins/state.ts +16 -1
  120. package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  123. package/src/gjc-runtime/launch-tmux.ts +6 -1
  124. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/gjc-runtime/session-layout.ts +180 -0
  126. package/src/gjc-runtime/session-resolution.ts +217 -0
  127. package/src/gjc-runtime/state-graph.ts +1 -2
  128. package/src/gjc-runtime/state-migrations.ts +1 -0
  129. package/src/gjc-runtime/state-runtime.ts +247 -124
  130. package/src/gjc-runtime/state-schema.ts +2 -0
  131. package/src/gjc-runtime/state-writer.ts +289 -41
  132. package/src/gjc-runtime/team-runtime.ts +43 -19
  133. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  134. package/src/gjc-runtime/ultragoal-guard.ts +102 -4
  135. package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
  136. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  138. package/src/gjc-runtime/workflow-manifest.ts +12 -3
  139. package/src/goals/tools/goal-tool.ts +11 -2
  140. package/src/harness-control-plane/storage.ts +14 -4
  141. package/src/hooks/native-skill-hook.ts +38 -12
  142. package/src/hooks/skill-state.ts +178 -83
  143. package/src/internal-urls/docs-index.generated.ts +9 -6
  144. package/src/main.ts +30 -0
  145. package/src/migrate/action-planner.ts +318 -0
  146. package/src/migrate/adapters/claude-code.ts +39 -0
  147. package/src/migrate/adapters/codex.ts +70 -0
  148. package/src/migrate/adapters/index.ts +277 -0
  149. package/src/migrate/adapters/opencode.ts +52 -0
  150. package/src/migrate/executor.ts +81 -0
  151. package/src/migrate/mcp-mapper.ts +152 -0
  152. package/src/migrate/report.ts +104 -0
  153. package/src/migrate/skill-normalizer.ts +80 -0
  154. package/src/migrate/types.ts +163 -0
  155. package/src/modes/acp/acp-event-mapper.ts +1 -0
  156. package/src/modes/bridge/bridge-mode.ts +2 -2
  157. package/src/modes/components/custom-editor.ts +30 -20
  158. package/src/modes/components/hook-editor.ts +7 -2
  159. package/src/modes/components/oauth-selector.ts +19 -0
  160. package/src/modes/controllers/event-controller.ts +20 -0
  161. package/src/modes/controllers/selector-controller.ts +80 -17
  162. package/src/modes/interactive-mode.ts +6 -2
  163. package/src/modes/rpc/rpc-mode.ts +2 -2
  164. package/src/modes/runtime-init.ts +1 -0
  165. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  166. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  167. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  168. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  169. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  170. package/src/modes/types.ts +7 -1
  171. package/src/modes/utils/ui-helpers.ts +23 -0
  172. package/src/notifications/config-commands.ts +50 -0
  173. package/src/notifications/config.ts +107 -0
  174. package/src/notifications/helpers.ts +135 -0
  175. package/src/notifications/html-format.ts +389 -0
  176. package/src/notifications/index.ts +663 -0
  177. package/src/notifications/rate-limit-pool.ts +179 -0
  178. package/src/notifications/telegram-cli.ts +194 -0
  179. package/src/notifications/telegram-daemon-cli.ts +74 -0
  180. package/src/notifications/telegram-daemon-control.ts +370 -0
  181. package/src/notifications/telegram-daemon.ts +1370 -0
  182. package/src/notifications/telegram-reference.ts +335 -0
  183. package/src/notifications/threaded-inbound.ts +80 -0
  184. package/src/notifications/threaded-render.ts +155 -0
  185. package/src/notifications/topic-registry.ts +133 -0
  186. package/src/prompts/agents/init.md +1 -1
  187. package/src/prompts/system/plan-mode-active.md +1 -1
  188. package/src/prompts/tools/ast-grep.md +1 -1
  189. package/src/prompts/tools/search.md +1 -1
  190. package/src/prompts/tools/task.md +1 -2
  191. package/src/research-plan/index.ts +1 -0
  192. package/src/research-plan/ledger.ts +177 -0
  193. package/src/rlm/artifacts.ts +12 -3
  194. package/src/rlm/index.ts +26 -0
  195. package/src/runtime-mcp/config-writer.ts +46 -0
  196. package/src/sdk.ts +16 -0
  197. package/src/session/agent-session.ts +128 -24
  198. package/src/session/auth-storage.ts +3 -0
  199. package/src/session/session-dump-format.ts +43 -2
  200. package/src/session/session-manager.ts +39 -5
  201. package/src/setup/credential-auto-import.ts +258 -0
  202. package/src/setup/credential-import.ts +17 -0
  203. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  204. package/src/setup/hermes-setup.ts +1 -1
  205. package/src/setup/host-plugin-setup.ts +142 -0
  206. package/src/skill-state/active-state.ts +72 -108
  207. package/src/skill-state/canonical-skills.ts +4 -0
  208. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  209. package/src/skill-state/workflow-hud.ts +4 -2
  210. package/src/skill-state/workflow-state-contract.ts +3 -3
  211. package/src/slash-commands/builtin-registry.ts +4 -1
  212. package/src/task/agents.ts +1 -22
  213. package/src/task/executor.ts +5 -1
  214. package/src/task/index.ts +1 -41
  215. package/src/task/spawn-gate.ts +1 -38
  216. package/src/task/types.ts +1 -1
  217. package/src/tools/ask-answer-registry.ts +25 -0
  218. package/src/tools/ask.ts +108 -16
  219. package/src/tools/computer.ts +58 -4
  220. package/src/tools/image-gen.ts +5 -8
  221. package/src/tools/index.ts +19 -0
  222. package/src/tools/inspect-image.ts +16 -11
  223. package/src/tools/subagent-render.ts +7 -0
  224. package/src/tools/subagent.ts +38 -7
  225. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  226. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  227. package/src/prompts/agents/explore.md +0 -58
  228. package/src/prompts/agents/plan.md +0 -49
  229. package/src/prompts/agents/reviewer.md +0 -141
  230. package/src/prompts/agents/task.md +0 -16
  231. package/src/prompts/review-request.md +0 -70
@@ -3,20 +3,14 @@
3
3
  *
4
4
  * Agents are embedded at build time via Bun's import with { type: "text" }.
5
5
  */
6
- import { Effort } from "@gajae-code/ai";
7
6
  import { parseFrontmatter, prompt } from "@gajae-code/utils";
8
7
  import { parseAgentFields } from "../discovery/helpers";
8
+ // Embed agent markdown files at build time
9
9
  import architectMd from "../prompts/agents/architect.md" with { type: "text" };
10
10
  import criticMd from "../prompts/agents/critic.md" with { type: "text" };
11
11
  import executorMd from "../prompts/agents/executor.md" with { type: "text" };
12
- import exploreMd from "../prompts/agents/explore.md" with { type: "text" };
13
- // Embed agent markdown files at build time
14
12
  import agentFrontmatterTemplate from "../prompts/agents/frontmatter.md" with { type: "text" };
15
-
16
- import planMd from "../prompts/agents/plan.md" with { type: "text" };
17
13
  import plannerMd from "../prompts/agents/planner.md" with { type: "text" };
18
- import reviewerMd from "../prompts/agents/reviewer.md" with { type: "text" };
19
- import taskMd from "../prompts/agents/task.md" with { type: "text" };
20
14
 
21
15
  import type { AgentDefinition, AgentSource } from "./types";
22
16
 
@@ -50,21 +44,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
50
44
  { fileName: "architect.md", template: architectMd },
51
45
  { fileName: "planner.md", template: plannerMd },
52
46
  { fileName: "critic.md", template: criticMd },
53
- { fileName: "explore.md", template: exploreMd },
54
- { fileName: "plan.md", template: planMd },
55
- { fileName: "reviewer.md", template: reviewerMd },
56
- {
57
- fileName: "task.md",
58
- frontmatter: {
59
- name: "task",
60
- description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
61
- spawns: "*",
62
- model: "pi/default",
63
- thinkingLevel: Effort.Medium,
64
- hide: true,
65
- },
66
- template: taskMd,
67
- },
68
47
  ];
69
48
 
70
49
  // Computed lazily on first loadBundledAgents() call to avoid eager prompt.render at module load.
@@ -1298,11 +1298,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1298
1298
  requestPause: () => {
1299
1299
  pauseRequested = true;
1300
1300
  },
1301
- injectMessage: async (content, deliverAs) => {
1301
+ injectMessage: async (content, deliverAs, opts) => {
1302
1302
  if (deliverAs === "nextTurn") {
1303
1303
  await session.prompt(content, { attribution: "agent" });
1304
1304
  return;
1305
1305
  }
1306
+ if (deliverAs === "steer") {
1307
+ const from = opts?.fromAgentId ?? manager.getSubagentRecord(liveSubagentId)?.ownerId ?? "?";
1308
+ session.emitSubagentSteerObservation({ from, to: liveSubagentId, body: content });
1309
+ }
1306
1310
  await session.sendUserMessage(content, { deliverAs });
1307
1311
  },
1308
1312
  });
package/src/task/index.ts CHANGED
@@ -55,7 +55,7 @@ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./
55
55
  import { renderResult, renderCall as renderTaskCall } from "./render";
56
56
  import { reconcileSpawnRoi } from "./roi-reconciliation";
57
57
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
58
- import { DEFAULT_SPAWN_THRESHOLD, evaluateReviewerExploreGate, evaluateSpawnGate } from "./spawn-gate";
58
+ import { DEFAULT_SPAWN_THRESHOLD, evaluateSpawnGate } from "./spawn-gate";
59
59
  import {
60
60
  applyNestedPatches,
61
61
  captureBaseline,
@@ -359,7 +359,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
359
359
  readonly renderResult = renderResult;
360
360
  readonly #discoveredAgents: AgentDefinition[];
361
361
  readonly #blockedAgent: string | undefined;
362
- readonly #spawningAgentType: string | undefined;
363
362
 
364
363
  get parameters(): TaskToolSchemaInstance {
365
364
  const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
@@ -391,7 +390,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
391
390
  discoveredAgents: AgentDefinition[],
392
391
  ) {
393
392
  this.#blockedAgent = $env.PI_BLOCKED_AGENT;
394
- this.#spawningAgentType = session.currentAgentType;
395
393
  this.#discoveredAgents = discoveredAgents;
396
394
  }
397
395
 
@@ -478,23 +476,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
478
476
  };
479
477
  }
480
478
 
481
- const reviewerExploreDecision = evaluateReviewerExploreGate({
482
- spawningAgentType: this.#spawningAgentType,
483
- targetAgent: params.agent,
484
- plan: params.spawnPlan,
485
- });
486
- if (reviewerExploreDecision.outcome === "rejected") {
487
- return {
488
- content: [
489
- {
490
- type: "text",
491
- text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
492
- },
493
- ],
494
- details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
495
- };
496
- }
497
-
498
479
  const manager = AsyncJobManager.instance();
499
480
  if (!manager) {
500
481
  return {
@@ -1083,27 +1064,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1083
1064
  };
1084
1065
  }
1085
1066
 
1086
- const reviewerExploreDecision = evaluateReviewerExploreGate({
1087
- spawningAgentType: this.#spawningAgentType,
1088
- targetAgent: agentName,
1089
- plan: params.spawnPlan,
1090
- });
1091
- if (reviewerExploreDecision.outcome === "rejected") {
1092
- return {
1093
- content: [
1094
- {
1095
- type: "text",
1096
- text: `Task spawn gate rejected reviewer->explore: ${reviewerExploreDecision.reason}. Provide spawnPlan fields: ${reviewerExploreDecision.missingFields.join(", ")}.`,
1097
- },
1098
- ],
1099
- details: {
1100
- projectAgentsDir,
1101
- results: [],
1102
- totalDurationMs: Date.now() - startTime,
1103
- },
1104
- };
1105
- }
1106
-
1107
1067
  let repoRoot: string | null = null;
1108
1068
  let baseline: WorktreeBaseline | null = null;
1109
1069
  if (isIsolated) {
@@ -1,7 +1,7 @@
1
1
  /** The hard, locked batch threshold enforced by the runtime gate. */
2
2
  export const DEFAULT_SPAWN_THRESHOLD = 4;
3
3
 
4
- /** The justification a large batch or reviewer-spawned explorer must supply to pass the hard gate. */
4
+ /** The justification a large batch must supply to pass the hard gate. */
5
5
  export interface SpawnPlanReceipt {
6
6
  whyParallel: string;
7
7
  whyNotLocal: string;
@@ -17,15 +17,6 @@ export interface SpawnGateRequest {
17
17
  plan?: SpawnPlanReceipt;
18
18
  }
19
19
 
20
- export interface ReviewerExploreGateRequest {
21
- /** Agent type/name doing the spawning, when known. */
22
- spawningAgentType?: string | null;
23
- /** Target agent type/name requested by the task call. */
24
- targetAgent: string;
25
- /** The spawn-plan receipt, when provided. */
26
- plan?: SpawnPlanReceipt;
27
- }
28
-
29
20
  export type SpawnGateOutcome = "allowed" | "rejected";
30
21
 
31
22
  export interface SpawnGateDecision {
@@ -102,31 +93,3 @@ export function decide(childCount: number, threshold: number, plan: SpawnPlanRec
102
93
  export function evaluateSpawnGate(request: SpawnGateRequest): SpawnGateDecision {
103
94
  return decide(request.childCount, DEFAULT_SPAWN_THRESHOLD, request.plan);
104
95
  }
105
-
106
- export function evaluateReviewerExploreGate(request: ReviewerExploreGateRequest): SpawnGateDecision {
107
- if (request.spawningAgentType !== "reviewer" || request.targetAgent !== "explore") {
108
- return {
109
- outcome: "allowed",
110
- reason: "reviewer->explore gate does not apply",
111
- planRequired: false,
112
- missingFields: [],
113
- };
114
- }
115
-
116
- const missingFields = findMissingPlanFields(request.plan);
117
- if (missingFields.length > 0) {
118
- return {
119
- outcome: "rejected",
120
- reason: `reviewer->explore spawn requires a complete spawn-plan receipt (${missingFields.join(", ")})`,
121
- planRequired: true,
122
- missingFields,
123
- };
124
- }
125
-
126
- return {
127
- outcome: "allowed",
128
- reason: "reviewer->explore spawn has a complete spawn-plan receipt",
129
- planRequired: true,
130
- missingFields: [],
131
- };
132
- }
package/src/task/types.ts CHANGED
@@ -72,7 +72,7 @@ const spawnPlanSchema = z
72
72
  expectedReceiptShape: z.string(),
73
73
  maxInlineTokens: z.number(),
74
74
  })
75
- .describe("justification required before spawning more than four tasks or reviewer-spawned explore tasks");
75
+ .describe("justification required before spawning more than four tasks");
76
76
 
77
77
  const createTaskItemSchema = (_contextEnabled: boolean) =>
78
78
  z.object({
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Process-wide registry mapping a session id to its active {@link AskAnswerSource}.
3
+ *
4
+ * Decouples the `ask` tool (which reads the source via `AgentSession`) from the
5
+ * notifications extension (which registers one), without threading a new method
6
+ * through the extension/runner/controller wiring. A session has at most one
7
+ * source; registering returns a disposer.
8
+ */
9
+
10
+ import type { AskAnswerSource } from "./index";
11
+
12
+ const sources = new Map<string, AskAnswerSource>();
13
+
14
+ /** Register `source` for `sessionId`. Returns a disposer that clears it. */
15
+ export function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void {
16
+ sources.set(sessionId, source);
17
+ return () => {
18
+ if (sources.get(sessionId) === source) sources.delete(sessionId);
19
+ };
20
+ }
21
+
22
+ /** The answer source for `sessionId`, if one is registered. */
23
+ export function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined {
24
+ return sources.get(sessionId);
25
+ }
package/src/tools/ask.ts CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  renderDeepInterviewAskQuestion,
35
35
  } from "../deep-interview/render-middleware";
36
36
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
37
- import { appendOrMergeDeepInterviewRound } from "../gjc-runtime/deep-interview-recorder";
37
+ import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../gjc-runtime/deep-interview-recorder";
38
38
  import { deepInterviewStatePath } from "../gjc-runtime/deep-interview-runtime";
39
39
  import { gateAnswerToResult, questionToGate } from "../modes/shared/agent-wire/deep-interview-gate";
40
40
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
@@ -104,6 +104,30 @@ export interface AskToolDetails {
104
104
  const OTHER_OPTION = "Other (type your own)";
105
105
  const RECOMMENDED_SUFFIX = " (Recommended)";
106
106
  const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
107
+ const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
108
+
109
+ function errorMessage(error: unknown): string {
110
+ return error instanceof Error ? error.message : String(error);
111
+ }
112
+
113
+ async function awaitDeepInterviewRecorderPersistence(persistence: Promise<void>): Promise<void> {
114
+ let timeout: ReturnType<typeof setTimeout> | undefined;
115
+ try {
116
+ await Promise.race([
117
+ persistence,
118
+ new Promise<never>((_resolve, reject) => {
119
+ timeout = setTimeout(
120
+ () => reject(new Error(`timed out after ${DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS}ms`)),
121
+ DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS,
122
+ );
123
+ }),
124
+ ]);
125
+ } catch (error) {
126
+ logger.warn(`ask: deep-interview round recording failed: ${errorMessage(error)}`);
127
+ } finally {
128
+ if (timeout) clearTimeout(timeout);
129
+ }
130
+ }
107
131
 
108
132
  function getDoneOptionLabel(): string {
109
133
  return `${theme.status.success} Done selecting`;
@@ -406,8 +430,18 @@ async function askSingleQuestion(
406
430
  // If input was dismissed (undefined), keep prior selectedOptions/customInput intact
407
431
  }
408
432
  } else {
409
- selectedOptions = [stripRecommendedSuffix(choice)];
410
- customInput = undefined;
433
+ const stripped = stripRecommendedSuffix(choice);
434
+ if (optionLabels.includes(stripped)) {
435
+ selectedOptions = [stripped];
436
+ customInput = undefined;
437
+ } else {
438
+ // A remote answer (e.g. a typed Telegram reply) that is not one of the
439
+ // listed options is the "provide my own" custom input — recorded the same
440
+ // as picking Other and typing it. The local selector can only ever return
441
+ // a listed entry, so this branch is reached only for free-text answers.
442
+ customInput = choice;
443
+ selectedOptions = [];
444
+ }
411
445
  }
412
446
  if (navigation?.allowForward) {
413
447
  return { selectedOptions, customInput, timedOut, navigation: "forward" };
@@ -481,11 +515,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
481
515
  ): Promise<void> {
482
516
  const meta = q.deepInterview;
483
517
  if (!meta) return;
484
- try {
485
- const cwd = this.session.cwd;
486
- const sessionId = this.session.getSessionId?.() ?? undefined;
487
- const statePath = deepInterviewStatePath(cwd, sessionId);
488
- await appendOrMergeDeepInterviewRound(
518
+ const cwd = this.session.cwd;
519
+ const sessionId = this.session.getSessionId?.() ?? undefined;
520
+ const statePath = deepInterviewStatePath(cwd, sessionId);
521
+ await awaitDeepInterviewRecorderPersistence(
522
+ appendOrMergeDeepInterviewRound(
489
523
  cwd,
490
524
  statePath,
491
525
  {
@@ -500,12 +534,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
500
534
  customInput,
501
535
  },
502
536
  { sessionId },
503
- );
504
- } catch (error) {
505
- logger.warn(
506
- `ask: deep-interview round recording failed: ${error instanceof Error ? error.message : String(error)}`,
507
- );
508
- }
537
+ ).then(async () => {
538
+ await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
539
+ }),
540
+ );
509
541
  }
510
542
 
511
543
  async execute(
@@ -529,11 +561,71 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
529
561
  const ui: UIContext = {
530
562
  select: (prompt, options, dialogOptions) => {
531
563
  if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
532
- return extensionUi.select(prompt, options, dialogOptions);
564
+ const source = this.session.getAskAnswerSource?.();
565
+ if (!source) return extensionUi.select(prompt, options, dialogOptions);
566
+ // Race the local UI against a remote answer (e.g. a Telegram reply via the
567
+ // notifications SDK) so asks can be answered without RPC mode. When the
568
+ // local UI wins, abort the remote source so it stops waiting and marks the
569
+ // action resolved-locally. First valid answer wins.
570
+ // Race the local UI against a remote answer (e.g. a Telegram reply via the
571
+ // notifications SDK) so asks can be answered without RPC mode. First valid
572
+ // answer wins; the loser is aborted so neither side is left hanging:
573
+ // - local wins -> abort the remote source (marks the action resolved-locally)
574
+ // - remote wins -> abort the local selector so the TUI dialog actually closes
575
+ const remoteController = new AbortController();
576
+ const localController = new AbortController();
577
+ // Propagate an external cancel (the tool's signal) to the local selector too.
578
+ const toolSignal = dialogOptions?.signal;
579
+ if (toolSignal) {
580
+ if (toolSignal.aborted) localController.abort();
581
+ else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
582
+ }
583
+ const remote = source.awaitAnswer(prompt, options, remoteController.signal).then(answer => {
584
+ // undefined is not a valid remote answer (registration failed, or the local
585
+ // UI already won and aborted us): never settle the race, let the local
586
+ // selector decide instead of cancelling the ask.
587
+ if (answer === undefined) return new Promise<string | undefined>(() => {});
588
+ localController.abort();
589
+ return answer;
590
+ });
591
+ const local = extensionUi
592
+ .select(prompt, options, { ...dialogOptions, signal: localController.signal })
593
+ .then(answer => {
594
+ remoteController.abort();
595
+ return answer;
596
+ });
597
+ // The losing selector may reject when aborted after the race already settled;
598
+ // swallow that so it is not an unhandled rejection (the race result is unaffected).
599
+ void local.catch(() => undefined);
600
+ return Promise.race([local, remote]);
533
601
  },
534
602
  editor: (title, prefill, dialogOptions, editorOptions) => {
535
603
  if (!extensionUi) throw new ToolAbortError("Ask tool requires interactive mode");
536
- return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
604
+ const source = this.session.getAskAnswerSource?.();
605
+ if (!source) return extensionUi.editor(title, prefill, dialogOptions, editorOptions);
606
+ // Race the local editor against a remote free-text answer so "Other / type
607
+ // your own" custom input can be provided remotely (e.g. a typed Telegram
608
+ // reply) instead of blocking on the local-only editor. Mirrors `select`.
609
+ const remoteController = new AbortController();
610
+ const localController = new AbortController();
611
+ const toolSignal = dialogOptions?.signal;
612
+ if (toolSignal) {
613
+ if (toolSignal.aborted) localController.abort();
614
+ else toolSignal.addEventListener("abort", () => localController.abort(), { once: true });
615
+ }
616
+ const remote = source.awaitAnswer(title, [], remoteController.signal).then(answer => {
617
+ if (answer === undefined) return new Promise<string | undefined>(() => {});
618
+ localController.abort();
619
+ return answer;
620
+ });
621
+ const local = extensionUi
622
+ .editor(title, prefill, { ...(dialogOptions ?? {}), signal: localController.signal }, editorOptions)
623
+ .then(answer => {
624
+ remoteController.abort();
625
+ return answer;
626
+ });
627
+ void local.catch(() => undefined);
628
+ return Promise.race([local, remote]);
537
629
  },
538
630
  };
539
631
 
@@ -6,6 +6,7 @@ import type { ImageContent } from "@gajae-code/ai";
6
6
  import { prompt } from "@gajae-code/utils";
7
7
  import * as z from "zod/v4";
8
8
  import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
9
+ import { resizeImage } from "../utils/image-resize";
9
10
  import type { ToolSession } from "./index";
10
11
  import type { OutputMeta } from "./output-meta";
11
12
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
@@ -174,6 +175,11 @@ let platformOverrideForTests: NodeJS.Platform | undefined;
174
175
  let archOverrideForTests: NodeJS.Architecture | undefined;
175
176
  const screenshotFallbackDirs = new WeakMap<ToolSession, Promise<string>>();
176
177
 
178
+ const COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH = 1568;
179
+ const COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT = 1568;
180
+ const COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES = 5 * 1024 * 1024;
181
+ const COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY = 70;
182
+
177
183
  export function setComputerControllerFactoryForTests(factory: ComputerControllerFactory | undefined): void {
178
184
  controllerFactory = factory ?? createNativeComputerController;
179
185
  }
@@ -278,6 +284,9 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
278
284
  if (batchResult.failedStep) {
279
285
  details.code = batchResult.failedStep.code;
280
286
  details.message = batchResult.failedStep.message;
287
+ if (batchResult.screenshotSource !== undefined) {
288
+ await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
289
+ }
281
290
  await writeComputerAuditLog(this.session, details);
282
291
  return {
283
292
  ...toolResult(details).text(`${details.code}: ${details.message}`).done(),
@@ -285,11 +294,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
285
294
  };
286
295
  }
287
296
  details.message = describeComputerSuccess(details);
288
- const image = imageContentFromNativeResult(batchResult.screenshotSource);
289
297
  if (batchResult.screenshotSource !== undefined) {
290
298
  await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
291
299
  details.message = describeComputerSuccess(details);
292
300
  }
301
+ const image = await inlineImageContentFromNativeResult(batchResult.screenshotSource, details, this.session);
293
302
  await writeComputerAuditLog(this.session, details);
294
303
  return image
295
304
  ? toolResult(details)
@@ -302,11 +311,11 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
302
311
  if (screenshot) details.screenshot = screenshot;
303
312
  details.status = "success";
304
313
  details.message = describeComputerSuccess(details);
305
- const image = imageContentFromNativeResult(result);
306
314
  if (screenshot) {
307
315
  await persistScreenshotFallback(result, details.screenshot, this.session);
308
316
  details.message = describeComputerSuccess(details);
309
317
  }
318
+ const image = await inlineImageContentFromNativeResult(result, details, this.session);
310
319
  await writeComputerAuditLog(this.session, details);
311
320
  return image
312
321
  ? toolResult(details)
@@ -472,7 +481,7 @@ function normalizeScreenshot(value: unknown): ComputerScreenshotDetails | undefi
472
481
  };
473
482
  }
474
483
 
475
- function imageContentFromNativeResult(value: unknown): ImageContent | undefined {
484
+ function fullResolutionImageContentFromNativeResult(value: unknown): ImageContent | undefined {
476
485
  const candidate =
477
486
  value && typeof value === "object" && "screenshot" in value
478
487
  ? (value as { screenshot?: unknown }).screenshot
@@ -483,13 +492,42 @@ function imageContentFromNativeResult(value: unknown): ImageContent | undefined
483
492
  return data ? { type: "image", data, mimeType: "image/png" } : undefined;
484
493
  }
485
494
 
495
+ async function inlineImageContentFromNativeResult(
496
+ value: unknown,
497
+ details: ComputerToolDetails,
498
+ session: ToolSession,
499
+ ): Promise<ImageContent | undefined> {
500
+ const image = fullResolutionImageContentFromNativeResult(value);
501
+ if (!image) return undefined;
502
+ const maxBytes = getInlineScreenshotMaxBytes(session);
503
+ const originalBytes = Buffer.byteLength(image.data, "base64");
504
+ if (originalBytes <= maxBytes) return image;
505
+
506
+ try {
507
+ const resized = await resizeImage(image, {
508
+ maxWidth: COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH,
509
+ maxHeight: COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT,
510
+ maxBytes,
511
+ jpegQuality: COMPUTER_INLINE_SCREENSHOT_JPEG_QUALITY,
512
+ });
513
+ if (resized.buffer.length <= maxBytes) {
514
+ return { type: "image", data: resized.data, mimeType: resized.mimeType };
515
+ }
516
+ } catch {
517
+ // Keep the action successful and rely on the full-resolution artifact path below.
518
+ }
519
+
520
+ details.message = `${details.message} Inline screenshot omitted because it could not be bounded below ${formatByteCount(maxBytes)}; use the saved screenshot artifact instead.`;
521
+ return undefined;
522
+ }
523
+
486
524
  async function persistScreenshotFallback(
487
525
  value: unknown,
488
526
  screenshot: ComputerScreenshotDetails | undefined,
489
527
  session: ToolSession,
490
528
  ): Promise<void> {
491
529
  if (!screenshot || screenshot.path) return;
492
- const image = imageContentFromNativeResult(value);
530
+ const image = fullResolutionImageContentFromNativeResult(value);
493
531
  if (!image) return;
494
532
  const dir = await getScreenshotFallbackDir(session);
495
533
  const filePath = path.join(dir, `computer-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
@@ -526,6 +564,22 @@ function getPngByteLength(png: NativeScreenshot["png"]): number | undefined {
526
564
  return png.byteLength;
527
565
  }
528
566
 
567
+ function getInlineScreenshotMaxBytes(session: Pick<ToolSession, "settings">): number {
568
+ const configured = Number(session.settings.get("computer.screenshotMaxBytes"));
569
+ const finiteConfigured =
570
+ Number.isFinite(configured) && configured > 0
571
+ ? Math.floor(configured)
572
+ : COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES;
573
+ return Math.min(finiteConfigured, COMPUTER_INLINE_SCREENSHOT_PROVIDER_MAX_BYTES);
574
+ }
575
+
576
+ function formatByteCount(bytes: number): string {
577
+ if (bytes < 1024) return `${bytes} bytes`;
578
+ const kib = bytes / 1024;
579
+ if (kib < 1024) return `${Math.round(kib)} KiB`;
580
+ return `${(kib / 1024).toFixed(1)} MiB`;
581
+ }
582
+
529
583
  function mapComputerError(error: unknown, hotkey?: string): { code: string; message: string } {
530
584
  if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
531
585
  return {
@@ -472,20 +472,17 @@ async function findImageApiKey(
472
472
  const openAI = await findOpenAIHostedImageCredentials(modelRegistry, activeModel, sessionId);
473
473
  if (openAI) return openAI;
474
474
  // Fall through to auto-detect if preferred provider key not found.
475
- } else if (preferredImageProvider === "antigravity" && modelRegistry) {
476
- const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
477
- if (antigravity) return antigravity;
478
- // Fall through to auto-detect if preferred provider key not found.
475
+ } else if (preferredImageProvider === "antigravity") {
476
+ if (!modelRegistry) return null;
477
+ return await findAntigravityCredentials(modelRegistry, sessionId);
479
478
  } else if (preferredImageProvider === "gemini") {
480
479
  const geminiKey = getEnvApiKey("google");
481
480
  if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
482
481
  const googleKey = $env.GOOGLE_API_KEY;
483
- if (googleKey) return { provider: "gemini", apiKey: googleKey };
484
- // Fall through to auto-detect if preferred provider key not found.
482
+ return googleKey ? { provider: "gemini", apiKey: googleKey } : null;
485
483
  } else if (preferredImageProvider === "openrouter") {
486
484
  const openRouterKey = getEnvApiKey("openrouter");
487
- if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
488
- // Fall through to auto-detect if preferred provider key not found.
485
+ return openRouterKey ? { provider: "openrouter", apiKey: openRouterKey } : null;
489
486
  }
490
487
 
491
488
  // Auto-detect: GPT hosted image generation, then Antigravity, OpenRouter, Gemini.
@@ -118,6 +118,18 @@ export type {
118
118
  DiscoverableToolSource,
119
119
  } from "../tool-discovery/tool-index";
120
120
 
121
+ /**
122
+ * Source of remote answers for interactive asks (e.g. a Telegram reply routed
123
+ * through the notifications SDK). Lets a pending ask resolve without RPC mode.
124
+ */
125
+ export interface AskAnswerSource {
126
+ /**
127
+ * Race a remote answer against the local UI for one question. Resolves with the
128
+ * chosen option label or free-text answer, or `undefined` to defer to local UI.
129
+ */
130
+ awaitAnswer(question: string, options: string[], signal?: AbortSignal): Promise<string | undefined>;
131
+ }
132
+
121
133
  /** Session context for tool factories */
122
134
  export interface ToolSession {
123
135
  /** Current working directory */
@@ -214,6 +226,13 @@ export interface ToolSession {
214
226
  getGoalModeState?: () => GoalModeState | undefined;
215
227
  /** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
216
228
  getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
229
+ /**
230
+ * Optional remote answer source for interactive asks. When present, the ask
231
+ * tool races the local UI selection against a remote answer (e.g. a Telegram
232
+ * reply via the notifications SDK) so asks can be answered without RPC mode.
233
+ * No-op when undefined: the ask path behaves exactly as before.
234
+ */
235
+ getAskAnswerSource?: () => AskAnswerSource | undefined;
217
236
  /** Optional per-session restriction for goal tool operations. */
218
237
  goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
219
238
  /** Goal runtime for the active agent session. */
@@ -78,21 +78,26 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
78
78
  };
79
79
 
80
80
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
81
- let model = resolvePattern("pi/default") ?? resolvePattern(activeModelPattern) ?? availableModels[0];
81
+ const configuredVisionPattern = this.session.settings.getModelRole("vision")?.trim();
82
+ const configuredVisionModel = configuredVisionPattern ? resolvePattern("pi/vision") : undefined;
83
+ if (configuredVisionPattern && !configuredVisionModel) {
84
+ throw new ToolError(
85
+ `Configured modelRoles.vision (${configuredVisionPattern}) did not resolve to an available model. Configure modelRoles.vision with a vision-capable model.`,
86
+ );
87
+ }
88
+ const model = configuredVisionModel ?? resolvePattern("pi/default") ?? resolvePattern(activeModelPattern);
82
89
  if (!model) {
83
- throw new ToolError("Unable to resolve a model for inspect_image.");
90
+ throw new ToolError(
91
+ "Unable to resolve a model for inspect_image. Configure modelRoles.vision with a vision-capable model or select a vision-capable active/default model.",
92
+ );
84
93
  }
85
94
 
86
- // inspect_image requires image input; if the resolved model is text-only,
87
- // fall back to any available vision-capable model before failing.
95
+ // inspect_image requires image input. A text-only selected model must be
96
+ // paired with an explicit vision role so the model/cost boundary is visible.
88
97
  if (!model.input.includes("image")) {
89
- const visionModel = availableModels.find(candidate => candidate.input.includes("image"));
90
- if (!visionModel) {
91
- throw new ToolError(
92
- `Resolved model ${model.provider}/${model.id} does not support image input, and no vision-capable model is available. Configure a vision-capable model.`,
93
- );
94
- }
95
- model = visionModel;
98
+ throw new ToolError(
99
+ `Resolved model ${model.provider}/${model.id} does not support image input. Configure modelRoles.vision with a vision-capable model.`,
100
+ );
96
101
  }
97
102
 
98
103
  const apiKey = await modelRegistry.getApiKey(model);
@@ -160,6 +160,13 @@ function renderSubagentSnapshotBody(snapshot: SubagentSnapshot, expanded: boolea
160
160
  lines.push(` ${theme.fg("dim", "Assignment:")}`);
161
161
  for (const al of snapshot.assignment.split("\n")) lines.push(` ${theme.fg("toolOutput", replaceTabs(al))}`);
162
162
  }
163
+ if (snapshot.steerMessage) {
164
+ lines.push(` ${theme.fg("accent", `Steer (${snapshot.steerState ?? "queued"})`)}`);
165
+ const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
166
+ for (const pl of getPreviewLines(snapshot.steerMessage, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode)) {
167
+ lines.push(` ${theme.fg("toolOutput", replaceTabs(pl))}`);
168
+ }
169
+ }
163
170
 
164
171
  // Defense in depth: the producer only attaches `progress` when a live producer
165
172
  // exists (subagent.ts #liveProgressFields), but the renderer also honors an