@gajae-code/coding-agent 0.3.0 → 0.3.1

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 (175) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/async/job-manager.d.ts +7 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/commands/deep-interview.d.ts +3 -0
  5. package/dist/types/config/keybindings.d.ts +5 -0
  6. package/dist/types/config/settings-schema.d.ts +4 -4
  7. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  8. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  9. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  10. package/dist/types/eval/py/executor.d.ts +2 -0
  11. package/dist/types/eval/py/kernel.d.ts +2 -0
  12. package/dist/types/exec/bash-executor.d.ts +10 -0
  13. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  14. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  16. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  17. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  20. package/dist/types/hooks/skill-state.d.ts +21 -0
  21. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  22. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  23. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  24. package/dist/types/internal-urls/types.d.ts +4 -0
  25. package/dist/types/lsp/index.d.ts +10 -10
  26. package/dist/types/modes/bridge/auth.d.ts +12 -0
  27. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  28. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  29. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  30. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  32. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  33. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  34. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  35. package/dist/types/modes/components/status-line.d.ts +2 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  37. package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
  38. package/dist/types/modes/index.d.ts +1 -0
  39. package/dist/types/modes/interactive-mode.d.ts +1 -0
  40. package/dist/types/modes/jobs-observer.d.ts +57 -0
  41. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  42. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  43. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  44. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  46. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  47. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  48. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  49. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  50. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  51. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  52. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  53. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  54. package/dist/types/modes/types.d.ts +1 -0
  55. package/dist/types/sdk.d.ts +2 -0
  56. package/dist/types/session/agent-session.d.ts +11 -1
  57. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  58. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  59. package/dist/types/task/id.d.ts +7 -0
  60. package/dist/types/task/index.d.ts +5 -0
  61. package/dist/types/task/receipt.d.ts +85 -0
  62. package/dist/types/task/spawn-gate.d.ts +38 -0
  63. package/dist/types/task/types.d.ts +143 -11
  64. package/dist/types/tools/cron.d.ts +6 -0
  65. package/dist/types/tools/index.d.ts +2 -0
  66. package/dist/types/tools/path-utils.d.ts +1 -0
  67. package/dist/types/tools/subagent.d.ts +15 -0
  68. package/package.json +7 -7
  69. package/scripts/build-binary.ts +7 -0
  70. package/src/async/job-manager.ts +36 -0
  71. package/src/cli/args.ts +9 -2
  72. package/src/commands/deep-interview.ts +1 -0
  73. package/src/commands/harness.ts +289 -19
  74. package/src/commands/launch.ts +2 -2
  75. package/src/commands/state.ts +2 -1
  76. package/src/commands/team.ts +22 -4
  77. package/src/config/keybindings.ts +6 -0
  78. package/src/config/settings-schema.ts +6 -3
  79. package/src/dap/client.ts +17 -3
  80. package/src/debug/crash-diagnostics.ts +223 -0
  81. package/src/debug/runtime-gauges.ts +20 -0
  82. package/src/deep-interview/render-middleware.ts +6 -0
  83. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  84. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  85. package/src/defaults/gjc/skills/ultragoal/SKILL.md +28 -2
  86. package/src/eval/py/executor.ts +21 -1
  87. package/src/eval/py/kernel.ts +15 -0
  88. package/src/exec/bash-executor.ts +41 -0
  89. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  90. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  91. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  92. package/src/gjc-runtime/state-migrations.ts +54 -7
  93. package/src/gjc-runtime/state-runtime.ts +461 -64
  94. package/src/gjc-runtime/state-schema.ts +192 -0
  95. package/src/gjc-runtime/state-writer.ts +32 -1
  96. package/src/gjc-runtime/team-runtime.ts +177 -105
  97. package/src/gjc-runtime/ultragoal-runtime.ts +114 -26
  98. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  99. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  100. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  101. package/src/harness-control-plane/control-endpoint.ts +19 -8
  102. package/src/harness-control-plane/owner.ts +57 -10
  103. package/src/harness-control-plane/state-machine.ts +2 -1
  104. package/src/hooks/skill-state.ts +176 -26
  105. package/src/internal-urls/agent-protocol.ts +68 -21
  106. package/src/internal-urls/artifact-protocol.ts +12 -17
  107. package/src/internal-urls/docs-index.generated.ts +3 -2
  108. package/src/internal-urls/registry-helpers.ts +19 -16
  109. package/src/internal-urls/types.ts +4 -0
  110. package/src/lsp/client.ts +18 -2
  111. package/src/main.ts +21 -5
  112. package/src/modes/bridge/auth.ts +41 -0
  113. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  114. package/src/modes/bridge/bridge-mode.ts +520 -0
  115. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  116. package/src/modes/bridge/event-stream.ts +70 -0
  117. package/src/modes/components/custom-editor.ts +101 -0
  118. package/src/modes/components/hook-selector.ts +61 -18
  119. package/src/modes/components/jobs-overlay-model.ts +109 -0
  120. package/src/modes/components/jobs-overlay.ts +172 -0
  121. package/src/modes/components/status-line/presets.ts +7 -5
  122. package/src/modes/components/status-line/segments.ts +25 -0
  123. package/src/modes/components/status-line/types.ts +2 -0
  124. package/src/modes/components/status-line.ts +9 -1
  125. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  126. package/src/modes/controllers/input-controller.ts +97 -9
  127. package/src/modes/controllers/selector-controller.ts +29 -0
  128. package/src/modes/index.ts +1 -0
  129. package/src/modes/interactive-mode.ts +27 -0
  130. package/src/modes/jobs-observer.ts +204 -0
  131. package/src/modes/rpc/host-tools.ts +1 -186
  132. package/src/modes/rpc/host-uris.ts +1 -235
  133. package/src/modes/rpc/rpc-client.ts +25 -10
  134. package/src/modes/rpc/rpc-mode.ts +12 -381
  135. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  136. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  137. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  138. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  139. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  140. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  141. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  142. package/src/modes/shared/agent-wire/responses.ts +17 -0
  143. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  144. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  145. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  146. package/src/modes/types.ts +1 -0
  147. package/src/prompts/tools/subagent.md +12 -7
  148. package/src/prompts/tools/task-summary.md +3 -9
  149. package/src/prompts/tools/task.md +5 -1
  150. package/src/sdk.ts +4 -0
  151. package/src/session/agent-session.ts +214 -38
  152. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  153. package/src/skill-state/workflow-state-contract.ts +7 -4
  154. package/src/skill-state/workflow-state-version.ts +3 -0
  155. package/src/slash-commands/builtin-registry.ts +8 -0
  156. package/src/task/executor.ts +29 -5
  157. package/src/task/id.ts +33 -0
  158. package/src/task/index.ts +257 -67
  159. package/src/task/output-manager.ts +5 -4
  160. package/src/task/receipt.ts +297 -0
  161. package/src/task/render.ts +48 -131
  162. package/src/task/spawn-gate.ts +132 -0
  163. package/src/task/types.ts +48 -7
  164. package/src/tools/ask.ts +73 -33
  165. package/src/tools/ast-edit.ts +1 -0
  166. package/src/tools/ast-grep.ts +1 -0
  167. package/src/tools/bash.ts +1 -1
  168. package/src/tools/cron.ts +48 -0
  169. package/src/tools/find.ts +4 -1
  170. package/src/tools/index.ts +2 -0
  171. package/src/tools/path-utils.ts +3 -2
  172. package/src/tools/read.ts +1 -0
  173. package/src/tools/search.ts +1 -0
  174. package/src/tools/skill.ts +6 -1
  175. package/src/tools/subagent.ts +237 -84
@@ -0,0 +1,297 @@
1
+ import type { SingleResult, TaskToolDetails } from "./types";
2
+
3
+ export interface TaskRoi {
4
+ tokens: number;
5
+ contextTokens?: number;
6
+ clonedTokens?: number;
7
+ costTotal?: number;
8
+ outputBytes?: number;
9
+ outputLines?: number;
10
+ producedChanges: boolean;
11
+ materialContribution: boolean;
12
+ lowRoi: boolean;
13
+ }
14
+ export interface TaskResultReceipt {
15
+ index: number;
16
+ id: string;
17
+ agent: string;
18
+ agentSource: SingleResult["agentSource"];
19
+ task: string;
20
+ assignment?: string;
21
+ description?: string;
22
+ status: "completed" | "failed" | "aborted" | "merge_failed" | "paused";
23
+ exitCode: number;
24
+ aborted?: boolean;
25
+ paused?: boolean;
26
+ truncated: boolean;
27
+ durationMs: number;
28
+ tokens: number;
29
+ contextTokens?: number;
30
+ contextWindow?: number;
31
+ modelOverride?: string | string[];
32
+ usage?: SingleResult["usage"];
33
+ cost?: number;
34
+ branchName?: string;
35
+ retryFailure?: { attempt: number; errorSummary: string };
36
+ errorSummary?: string;
37
+ abortSummary?: string;
38
+ preview: string;
39
+ previewTruncated: boolean;
40
+ outputRef?: { uri: string; sizeBytes: number; lineCount: number; sha256?: string };
41
+ outputUnavailable?: boolean;
42
+ review?: {
43
+ overallCorrectness?: string;
44
+ findingCount: number;
45
+ findings?: Array<{ severity?: string; summary: string }>;
46
+ };
47
+ extractedToolCounts?: Record<string, number>;
48
+ forkContext?: SingleResult["forkContext"];
49
+ roi?: TaskRoi;
50
+ }
51
+
52
+ const BANNED_RAW_TASK_KEYS = new Set([
53
+ "output",
54
+ "stderr",
55
+ "extractedToolData",
56
+ "resultText",
57
+ "errorText",
58
+ "artifactPayload",
59
+ "rawResult",
60
+ "rawResults",
61
+ "rawNestedResults",
62
+ "fullOutput",
63
+ "full_result",
64
+ "toolOutput",
65
+ "toolResultRaw",
66
+ "stdout",
67
+ "rawOutput",
68
+ "recentOutput",
69
+ "currentToolArgs",
70
+ "inflightTaskDetails",
71
+ ]);
72
+
73
+ function truncateText(value: string | undefined, maxChars: number): string | undefined {
74
+ if (!value) return undefined;
75
+ return value.length > maxChars ? value.slice(0, maxChars) : value;
76
+ }
77
+
78
+ function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outputRef"]): string {
79
+ const status = getStatus(raw);
80
+ if (raw.retryFailure) {
81
+ return `Task ${status}; retry stopped after attempt ${raw.retryFailure.attempt}.`;
82
+ }
83
+ if (raw.abortReason) {
84
+ return `Task ${status}; abort reason recorded.`;
85
+ }
86
+ if (raw.error) {
87
+ return `Task ${status}; error recorded.`;
88
+ }
89
+ if (outputRef) {
90
+ return `Task ${status}; output stored in ${outputRef.uri} (${outputRef.lineCount} lines, ${outputRef.sizeBytes} bytes).`;
91
+ }
92
+ return `Task ${status}; output artifact unavailable.`;
93
+ }
94
+
95
+ function getStatus(raw: SingleResult): TaskResultReceipt["status"] {
96
+ if (raw.paused) return "paused";
97
+ if (raw.aborted) return "aborted";
98
+ if (raw.exitCode === 0 && raw.error) return "merge_failed";
99
+ if (raw.exitCode !== 0 || raw.error) return "failed";
100
+ return "completed";
101
+ }
102
+
103
+ function buildReview(raw: SingleResult): TaskResultReceipt["review"] | undefined {
104
+ const data = raw.extractedToolData;
105
+ if (!data) return undefined;
106
+ const yields = Array.isArray(data.yield) ? data.yield : [];
107
+ const reviewYield = yields
108
+ .map(item => (item && typeof item === "object" ? (item as { data?: unknown }).data : undefined))
109
+ .findLast(item => item && typeof item === "object" && "overall_correctness" in item) as
110
+ | { overall_correctness?: unknown }
111
+ | undefined;
112
+ const rawFindings = Array.isArray(data.report_finding) ? data.report_finding : [];
113
+ const findings = rawFindings.slice(0, 20).map(item => {
114
+ const value = item && typeof item === "object" ? (item as Record<string, unknown>) : {};
115
+ const severity =
116
+ typeof value.severity === "string"
117
+ ? value.severity
118
+ : typeof value.priority === "string"
119
+ ? value.priority
120
+ : undefined;
121
+ const summaryValue = value.summary ?? value.title ?? value.message ?? value.body ?? "finding";
122
+ return { severity, summary: truncateText(String(summaryValue), 200) ?? "finding" };
123
+ });
124
+ if (!reviewYield && findings.length === 0) return undefined;
125
+ return {
126
+ overallCorrectness:
127
+ typeof reviewYield?.overall_correctness === "string" ? reviewYield.overall_correctness : undefined,
128
+ findingCount: rawFindings.length,
129
+ findings: findings.length > 0 ? findings : undefined,
130
+ };
131
+ }
132
+
133
+ function hasReviewFindings(raw: SingleResult): boolean {
134
+ const findings = raw.extractedToolData?.report_finding;
135
+ return Array.isArray(findings) && findings.length > 0;
136
+ }
137
+
138
+ function hasNonEmptyPreview(raw: SingleResult): boolean {
139
+ return Boolean((raw.output.trim() || raw.stderr.trim()).trim());
140
+ }
141
+
142
+ /**
143
+ * Heuristic task ROI signal built only from receipt-safe accounting fields.
144
+ * Advisory only: these flags never change task success/failure semantics.
145
+ */
146
+ export function buildTaskRoi(raw: SingleResult): TaskRoi {
147
+ const outputBytes = raw.outputMeta?.byteSize ?? (raw.outputMeta ? Buffer.byteLength(raw.output, "utf8") : undefined);
148
+ const outputLines = raw.outputMeta?.lineCount;
149
+ const producedChanges =
150
+ raw.producedChanges ??
151
+ Boolean(raw.branchName || (Array.isArray(raw.nestedPatches) && raw.nestedPatches.length > 0));
152
+ const status = getStatus(raw);
153
+ const terminal = status !== "paused" && !raw.aborted;
154
+ const materialContribution = Boolean(
155
+ producedChanges ||
156
+ (outputBytes !== undefined && outputBytes > 0) ||
157
+ hasReviewFindings(raw) ||
158
+ (status === "completed" && raw.tokens > 0 && hasNonEmptyPreview(raw)),
159
+ );
160
+ const lowRoi = terminal && raw.tokens > 0 && !materialContribution;
161
+ return {
162
+ tokens: raw.tokens,
163
+ contextTokens: raw.contextTokens,
164
+ clonedTokens: raw.forkContext?.clonedTokens,
165
+ costTotal: raw.usage?.cost.total,
166
+ outputBytes,
167
+ outputLines,
168
+ producedChanges,
169
+ materialContribution,
170
+ lowRoi,
171
+ };
172
+ }
173
+
174
+ export function buildTaskRoiSummary(receipts: readonly TaskResultReceipt[]): TaskToolDetails["roiSummary"] {
175
+ const totalCostTotal = receipts.reduce((total, receipt) => total + (receipt.roi?.costTotal ?? 0), 0);
176
+ const totalClonedTokens = receipts.reduce((total, receipt) => total + (receipt.roi?.clonedTokens ?? 0), 0);
177
+ return {
178
+ childCount: receipts.length,
179
+ totalTokens: receipts.reduce((total, receipt) => total + (receipt.roi?.tokens ?? receipt.tokens), 0),
180
+ totalCostTotal: totalCostTotal > 0 ? totalCostTotal : undefined,
181
+ totalClonedTokens: totalClonedTokens > 0 ? totalClonedTokens : undefined,
182
+ lowRoiChildIds: receipts.filter(receipt => receipt.roi?.lowRoi).map(receipt => receipt.id),
183
+ };
184
+ }
185
+
186
+ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
187
+ const outputRef = raw.outputMeta
188
+ ? {
189
+ uri: `agent://${raw.id}`,
190
+ sizeBytes: raw.outputMeta.byteSize ?? Buffer.byteLength(raw.output, "utf8"),
191
+ lineCount: raw.outputMeta.lineCount,
192
+ sha256: raw.outputMeta.sha256,
193
+ }
194
+ : undefined;
195
+ const preview = buildSafeSynopsis(raw, outputRef);
196
+ const extractedToolCounts = raw.extractedToolData
197
+ ? Object.fromEntries(
198
+ Object.entries(raw.extractedToolData).map(([tool, values]) => [
199
+ tool,
200
+ Array.isArray(values) ? values.length : 0,
201
+ ]),
202
+ )
203
+ : undefined;
204
+ return {
205
+ index: raw.index,
206
+ id: raw.id,
207
+ agent: raw.agent,
208
+ agentSource: raw.agentSource,
209
+ task: raw.task,
210
+ assignment: raw.assignment,
211
+ description: raw.description,
212
+ status: getStatus(raw),
213
+ exitCode: raw.exitCode,
214
+ aborted: raw.aborted,
215
+ paused: raw.paused,
216
+ truncated: raw.truncated,
217
+ durationMs: raw.durationMs,
218
+ tokens: raw.tokens,
219
+ contextTokens: raw.contextTokens,
220
+ contextWindow: raw.contextWindow,
221
+ modelOverride: raw.modelOverride,
222
+ usage: raw.usage,
223
+ cost: raw.usage?.cost.total,
224
+ branchName: raw.branchName,
225
+ retryFailure: raw.retryFailure
226
+ ? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
227
+ : undefined,
228
+ errorSummary: raw.error ? "Error recorded." : undefined,
229
+ abortSummary: raw.abortReason ? "Abort reason recorded." : undefined,
230
+ preview,
231
+ previewTruncated: false,
232
+ outputRef,
233
+ outputUnavailable: outputRef ? undefined : true,
234
+ review: buildReview(raw),
235
+ extractedToolCounts,
236
+ forkContext: raw.forkContext,
237
+ roi: buildTaskRoi(raw),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Raw, pre-sanitization task details: the internal shape produced during task
243
+ * execution, where `results` are full `SingleResult` objects. The public
244
+ * `TaskToolDetails` exposes only receipts.
245
+ */
246
+ export interface RawTaskToolDetails {
247
+ projectAgentsDir: string | null;
248
+ results: SingleResult[];
249
+ totalDurationMs: number;
250
+ usage?: TaskToolDetails["usage"];
251
+ async?: TaskToolDetails["async"];
252
+ forkContextClonedTokens?: number;
253
+ roiSummary?: TaskToolDetails["roiSummary"];
254
+ }
255
+
256
+ /** Central converter from raw task details to receipt-only public details. */
257
+ export function sanitizeTaskToolDetails(raw: RawTaskToolDetails): TaskToolDetails {
258
+ return {
259
+ projectAgentsDir: raw.projectAgentsDir,
260
+ results: raw.results.map(buildTaskReceipt),
261
+ totalDurationMs: raw.totalDurationMs,
262
+ usage: raw.usage,
263
+ forkContextClonedTokens: raw.forkContextClonedTokens,
264
+ roiSummary: raw.roiSummary ?? buildTaskRoiSummary(raw.results.map(buildTaskReceipt)),
265
+ async: raw.async,
266
+ };
267
+ }
268
+
269
+ export function findRawTaskLeakKeys(value: unknown): string[] {
270
+ const found = new Set<string>();
271
+ const seen = new WeakSet<object>();
272
+ const visit = (current: unknown) => {
273
+ if (!current || typeof current !== "object") return;
274
+ if (seen.has(current)) return;
275
+ seen.add(current);
276
+ if (Array.isArray(current)) {
277
+ for (const item of current) visit(item);
278
+ return;
279
+ }
280
+ for (const [key, child] of Object.entries(current)) {
281
+ // Banned keys only leak when they carry text or structure. A numeric
282
+ // value (e.g. the `output` token count on a canonical `Usage` record,
283
+ // whose shape is `input/output/cacheRead/cacheWrite/totalTokens`) is safe.
284
+ if (BANNED_RAW_TASK_KEYS.has(key) && typeof child !== "number") found.add(key);
285
+ visit(child);
286
+ }
287
+ };
288
+ visit(value);
289
+ return [...found].sort();
290
+ }
291
+
292
+ export function assertNoRawTaskFields(value: unknown, surface: string): void {
293
+ const keys = findRawTaskLeakKeys(value);
294
+ if (keys.length > 0) {
295
+ throw new Error(`${surface} contains raw task fields: ${keys.join(", ")}`);
296
+ }
297
+ }
@@ -6,12 +6,13 @@
6
6
  */
7
7
  import path from "node:path";
8
8
  import type { Component } from "@gajae-code/tui";
9
- import { Container, Text } from "@gajae-code/tui";
9
+ import { Text } from "@gajae-code/tui";
10
10
  import { formatNumber } from "@gajae-code/utils";
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import type { Theme } from "../modes/theme/theme";
13
13
  import {
14
14
  formatBadge,
15
+ formatBytes,
15
16
  formatDuration,
16
17
  formatMoreItems,
17
18
  formatStatusIcon,
@@ -27,8 +28,9 @@ import {
27
28
  type SubmitReviewDetails,
28
29
  } from "../tools/review";
29
30
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
31
+ import type { TaskResultReceipt } from "./receipt";
30
32
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
31
- import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
33
+ import type { AgentProgress, TaskParams, TaskToolDetails } from "./types";
32
34
 
33
35
  /**
34
36
  * Get status icon for agent state.
@@ -139,21 +141,6 @@ function formatTaskId(id: string): string {
139
141
  return `${indices} ${labels}`;
140
142
  }
141
143
 
142
- const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
143
-
144
- function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
145
- const lines = output.split("\n");
146
- const firstLine = lines[0]?.trim() ?? "";
147
- if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
148
- return { rest: output };
149
- }
150
- const rest = lines
151
- .slice(1)
152
- .join("\n")
153
- .replace(/^\s*\n+/, "");
154
- return { warning: firstLine, rest };
155
- }
156
-
157
144
  function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
158
145
  return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
159
146
  }
@@ -804,35 +791,24 @@ function renderFindings(
804
791
  /**
805
792
  * Render final result for a single agent.
806
793
  */
807
- function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
794
+ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded: boolean, theme: Theme): string[] {
808
795
  const lines: string[] = [];
809
796
  const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
810
797
  const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
811
798
 
812
- const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
813
- const aborted = result.aborted ?? false;
814
- const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
815
- const success = !aborted && result.exitCode === 0 && !result.error;
816
- const needsWarning = Boolean(missingCompleteWarning) && success;
817
- const icon = aborted
818
- ? theme.status.aborted
819
- : needsWarning
820
- ? theme.status.warning
821
- : success
822
- ? theme.status.success
799
+ const success = result.status === "completed";
800
+ const mergeFailed = result.status === "merge_failed";
801
+ const aborted = result.status === "aborted";
802
+ const icon = success
803
+ ? theme.status.success
804
+ : aborted
805
+ ? theme.status.aborted
806
+ : mergeFailed
807
+ ? theme.status.warning
823
808
  : theme.status.error;
824
- const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
825
- const statusText = aborted
826
- ? "aborted"
827
- : needsWarning
828
- ? "warning"
829
- : success
830
- ? "done"
831
- : mergeFailed
832
- ? "merge failed"
833
- : "failed";
809
+ const iconColor = success ? "success" : mergeFailed ? "warning" : "error";
810
+ const statusText = mergeFailed ? "merge failed" : success ? "done" : result.status;
834
811
 
835
- // Main status line: id: description [status] · stats · ⟨agent⟩
836
812
  const description = result.description?.trim();
837
813
  const displayId = formatTaskId(result.id);
838
814
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
@@ -847,121 +823,62 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
847
823
  tokens: result.tokens,
848
824
  contextTokens: result.contextTokens,
849
825
  contextWindow: result.contextWindow,
850
- cost: result.usage?.cost.total ?? 0,
826
+ cost: result.cost ?? result.usage?.cost.total ?? 0,
851
827
  },
852
828
  theme,
853
829
  );
854
830
  statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
855
-
856
- if (result.truncated) {
831
+ if (result.truncated || result.previewTruncated) {
857
832
  statusLine += ` ${theme.fg("warning", "[truncated]")}`;
858
833
  }
859
-
860
834
  lines.push(statusLine);
861
835
 
862
836
  lines.push(...renderTaskSection(result.assignment ?? result.task, continuePrefix, expanded, theme));
863
837
 
864
- if (aborted && result.abortReason) {
865
- lines.push(
866
- `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
867
- );
868
- }
869
- // Check for review result (yield with review schema + report_finding)
870
- const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
871
- const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
872
-
873
- // Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
874
- const reviewData = completeData
875
- ?.map(c => c.data as SubmitReviewDetails)
876
- .filter(d => d && typeof d === "object" && "overall_correctness" in d);
877
- const submitReviewData = reviewData && reviewData.length > 0 ? reviewData : undefined;
878
-
879
- if (submitReviewData && submitReviewData.length > 0) {
880
- // Use combined review renderer
881
- const summary = submitReviewData[submitReviewData.length - 1];
882
- const findings = reportFindingData;
883
- lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
884
- return lines;
885
- }
886
- if (reportFindingData.length > 0) {
887
- const hasCompleteData = completeData && completeData.length > 0;
888
- const message = hasCompleteData
889
- ? "Review verdict missing expected fields"
890
- : "Review incomplete (yield not called)";
891
- lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
892
- lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
893
- lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
894
- return lines;
895
- }
896
-
897
- // Check for extracted tool data with custom renderers (skip review tools)
898
- let hasCustomRendering = false;
899
- const deferredToolLines: string[] = [];
900
- if (result.extractedToolData) {
901
- for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
902
- // Skip review tools - handled above
903
- if (toolName === "yield" || toolName === "report_finding") continue;
904
-
905
- const handler = subprocessToolRegistry.getHandler(toolName);
906
- if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
907
- const isTaskTool = toolName === "task";
908
- const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
909
- const target = isTaskTool ? deferredToolLines : lines;
910
- if (!isTaskTool) {
911
- hasCustomRendering = true;
912
- target.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
913
- }
914
- if (component instanceof Text) {
915
- // Prefix each line with continuePrefix
916
- const text = component.getText();
917
- for (const line of text.split("\n")) {
918
- target.push(`${continuePrefix}${line}`);
919
- }
920
- } else if (component instanceof Container) {
921
- // For containers, render each child
922
- for (const child of (component as Container).children) {
923
- if (child instanceof Text) {
924
- target.push(`${continuePrefix}${child.getText()}`);
925
- }
926
- }
838
+ if (result.review) {
839
+ if (result.review.overallCorrectness) {
840
+ lines.push(`${continuePrefix}${theme.fg("dim", `Review: ${result.review.overallCorrectness}`)}`);
841
+ }
842
+ if (result.review.findingCount > 0) {
843
+ lines.push(`${continuePrefix}${theme.fg("dim", `${result.review.findingCount} findings`)}`);
844
+ if (expanded && result.review.findings) {
845
+ for (const finding of result.review.findings) {
846
+ const severity = finding.severity ? `${finding.severity}: ` : "";
847
+ lines.push(`${continuePrefix}${theme.fg("dim", `- ${severity}${finding.summary}`)}`);
927
848
  }
928
849
  }
929
850
  }
851
+ } else {
852
+ lines.push(...renderOutputSection(result.preview, continuePrefix, expanded, theme, 3, 12));
853
+ }
854
+ if (result.roi?.lowRoi) {
855
+ lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
930
856
  }
931
857
 
932
- if (hasCustomRendering && missingCompleteWarning) {
858
+ if (result.outputRef) {
933
859
  lines.push(
934
- `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg(
860
+ `${continuePrefix}${theme.fg(
935
861
  "dim",
936
- truncateToWidth(missingCompleteWarning, 80),
862
+ `Output: ${result.outputRef.uri} (${formatBytes(result.outputRef.sizeBytes)}, ${result.outputRef.lineCount} lines)`,
937
863
  )}`,
938
864
  );
865
+ } else if (result.outputUnavailable) {
866
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output artifact unavailable")}`);
939
867
  }
940
868
 
941
- // Fallback to output preview if no custom rendering
942
- if (!hasCustomRendering) {
869
+ if (result.branchName && success) {
870
+ lines.push(`${continuePrefix}${theme.fg("dim", `Branch: ${result.branchName}`)}`);
871
+ }
872
+ if (result.abortSummary) {
943
873
  lines.push(
944
- ...renderOutputSection(outputWithoutWarning, continuePrefix, expanded, theme, 3, 12, missingCompleteWarning),
874
+ `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortSummary), 80))}`,
945
875
  );
946
876
  }
947
-
948
- if (deferredToolLines.length > 0) {
949
- lines.push(...deferredToolLines);
950
- }
951
-
952
- if (result.patchPath && !aborted && result.exitCode === 0) {
953
- lines.push(`${continuePrefix}${theme.fg("dim", `Patch: ${result.patchPath}`)}`);
954
- } else if (result.branchName && !aborted && result.exitCode === 0) {
955
- lines.push(`${continuePrefix}${theme.fg("dim", `Branch: ${result.branchName}`)}`);
956
- }
957
-
958
- // Error message
959
- if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
877
+ if (result.errorSummary && (!success || mergeFailed)) {
960
878
  lines.push(
961
- `${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(replaceTabs(result.error), 70))}`,
879
+ `${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(replaceTabs(result.errorSummary), 70))}`,
962
880
  );
963
881
  }
964
-
965
882
  return lines;
966
883
  }
967
884
 
@@ -1009,9 +926,9 @@ export function renderResult(
1009
926
  lines.push(...renderAgentResult(res, isLast, expanded, theme));
1010
927
  });
1011
928
 
1012
- const abortedCount = details.results.filter(r => r.aborted).length;
1013
- const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
1014
- const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && !r.error).length;
929
+ const abortedCount = details.results.filter(r => r.status === "aborted").length;
930
+ const mergeFailedCount = details.results.filter(r => r.status === "merge_failed").length;
931
+ const successCount = details.results.filter(r => r.status === "completed").length;
1015
932
  const failCount = details.results.length - successCount - mergeFailedCount - abortedCount;
1016
933
  let summary = `${theme.fg("dim", "Total:")} `;
1017
934
  if (abortedCount > 0) {
@@ -0,0 +1,132 @@
1
+ /** The hard, locked batch threshold enforced by the runtime gate. */
2
+ export const DEFAULT_SPAWN_THRESHOLD = 4;
3
+
4
+ /** The justification a large batch or reviewer-spawned explorer must supply to pass the hard gate. */
5
+ export interface SpawnPlanReceipt {
6
+ whyParallel: string;
7
+ whyNotLocal: string;
8
+ independence: string;
9
+ expectedReceiptShape: string;
10
+ maxInlineTokens: number;
11
+ }
12
+
13
+ export interface SpawnGateRequest {
14
+ /** Number of children the batch wants to spawn. */
15
+ childCount: number;
16
+ /** The spawn-plan receipt, when provided. */
17
+ plan?: SpawnPlanReceipt;
18
+ }
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
+ export type SpawnGateOutcome = "allowed" | "rejected";
30
+
31
+ export interface SpawnGateDecision {
32
+ outcome: SpawnGateOutcome;
33
+ /** Human-readable reason, suitable for a blocked-result message. */
34
+ reason: string;
35
+ /** Whether a plan was required for this request. */
36
+ planRequired: boolean;
37
+ /** Missing plan field names when rejected for an incomplete plan. */
38
+ missingFields: readonly string[];
39
+ }
40
+
41
+ const REQUIRED_STRING_FIELDS = ["whyParallel", "whyNotLocal", "independence", "expectedReceiptShape"] as const;
42
+
43
+ export function findMissingPlanFields(plan: SpawnPlanReceipt | undefined): string[] {
44
+ if (plan === undefined) {
45
+ return [...REQUIRED_STRING_FIELDS, "maxInlineTokens"];
46
+ }
47
+ const missing: string[] = [];
48
+ for (const field of REQUIRED_STRING_FIELDS) {
49
+ const value = plan[field];
50
+ if (typeof value !== "string" || value.trim().length === 0) {
51
+ missing.push(field);
52
+ }
53
+ }
54
+ if (
55
+ typeof plan.maxInlineTokens !== "number" ||
56
+ !Number.isFinite(plan.maxInlineTokens) ||
57
+ plan.maxInlineTokens <= 0
58
+ ) {
59
+ missing.push("maxInlineTokens");
60
+ }
61
+ return missing;
62
+ }
63
+
64
+ export function decide(childCount: number, threshold: number, plan: SpawnPlanReceipt | undefined): SpawnGateDecision {
65
+ if (!Number.isInteger(childCount) || childCount < 0) {
66
+ throw new RangeError("childCount must be a non-negative integer");
67
+ }
68
+ if (!Number.isInteger(threshold) || threshold < 1) {
69
+ throw new RangeError("threshold must be a positive integer");
70
+ }
71
+
72
+ const planRequired = childCount > threshold;
73
+ if (!planRequired) {
74
+ return {
75
+ outcome: "allowed",
76
+ reason: `batch of ${childCount} is at or below threshold ${threshold}`,
77
+ planRequired: false,
78
+ missingFields: [],
79
+ };
80
+ }
81
+
82
+ const missingFields = findMissingPlanFields(plan);
83
+ if (missingFields.length > 0) {
84
+ return {
85
+ outcome: "rejected",
86
+ reason: `batch of ${childCount} exceeds threshold ${threshold} and the spawn-plan receipt is ${
87
+ plan === undefined ? "missing" : `incomplete (${missingFields.join(", ")})`
88
+ }`,
89
+ planRequired: true,
90
+ missingFields,
91
+ };
92
+ }
93
+
94
+ return {
95
+ outcome: "allowed",
96
+ reason: `batch of ${childCount} exceeds threshold ${threshold} and a complete spawn-plan receipt was provided`,
97
+ planRequired: true,
98
+ missingFields: [],
99
+ };
100
+ }
101
+
102
+ export function evaluateSpawnGate(request: SpawnGateRequest): SpawnGateDecision {
103
+ return decide(request.childCount, DEFAULT_SPAWN_THRESHOLD, request.plan);
104
+ }
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
+ }