pi-plans 0.2.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 (75) hide show
  1. package/README.md +90 -26
  2. package/agents/ref-analyst.md +18 -0
  3. package/index.ts +121 -9
  4. package/package.json +16 -1
  5. package/references/pi-planning-workflow.md +21 -6
  6. package/references/state-and-config.md +52 -5
  7. package/scripts/validate.ts +5 -0
  8. package/skills/plan-with-refs/SKILL.md +3 -3
  9. package/src/code-graph/commands.ts +483 -0
  10. package/src/code-graph/discovery.ts +118 -0
  11. package/src/code-graph/git.ts +108 -0
  12. package/src/code-graph/identity.ts +59 -0
  13. package/src/code-graph/indexer.ts +281 -0
  14. package/src/code-graph/materialize.ts +166 -0
  15. package/src/code-graph/mode.ts +28 -0
  16. package/src/code-graph/mutations.ts +160 -0
  17. package/src/code-graph/parser.ts +51 -0
  18. package/src/code-graph/parsers/javascript.ts +35 -0
  19. package/src/code-graph/parsers/python.ts +160 -0
  20. package/src/code-graph/parsers/tree-sitter.ts +316 -0
  21. package/src/code-graph/paths.ts +85 -0
  22. package/src/code-graph/prompts.ts +18 -0
  23. package/src/code-graph/resolver.ts +69 -0
  24. package/src/code-graph/runtime.ts +158 -0
  25. package/src/code-graph/schema.ts +135 -0
  26. package/src/code-graph/screening.ts +82 -0
  27. package/src/code-graph/store.ts +278 -0
  28. package/src/code-graph/summary.ts +435 -0
  29. package/src/code-graph/types.ts +163 -0
  30. package/src/compaction.ts +1125 -371
  31. package/src/config-command.ts +361 -0
  32. package/src/exec.ts +508 -693
  33. package/src/guard.ts +14 -1
  34. package/src/refine-prompts.ts +109 -0
  35. package/src/refine-ui-helpers.ts +71 -18
  36. package/src/refine-ui-state.ts +88 -22
  37. package/src/refine-ui.ts +210 -102
  38. package/src/state.ts +36 -7
  39. package/src/subagent.ts +164 -61
  40. package/src/termination-prompt.ts +22 -0
  41. package/tests/analyze-refs.test.ts +265 -0
  42. package/tests/ask-choice.test.ts +264 -0
  43. package/tests/autocomplete.test.ts +6 -1
  44. package/tests/code-graph-apply-action.test.ts +173 -0
  45. package/tests/code-graph-apply.test.ts +185 -0
  46. package/tests/code-graph-commands.test.ts +211 -0
  47. package/tests/code-graph-db.test.ts +166 -0
  48. package/tests/code-graph-discovery.test.ts +38 -0
  49. package/tests/code-graph-git.test.ts +94 -0
  50. package/tests/code-graph-index.test.ts +175 -0
  51. package/tests/code-graph-loop.e2e.test.ts +159 -0
  52. package/tests/code-graph-mutations.test.ts +117 -0
  53. package/tests/code-graph-parser.test.ts +85 -0
  54. package/tests/code-graph-rollback.test.ts +100 -0
  55. package/tests/code-graph-summary-batching.test.ts +518 -0
  56. package/tests/code-graph-summary.test.ts +148 -0
  57. package/tests/compaction.test.ts +371 -57
  58. package/tests/config-command.test.ts +263 -0
  59. package/tests/exec.test.ts +808 -241
  60. package/tests/fixtures/code-graph/sample.js +36 -0
  61. package/tests/fixtures/code-graph/sample.py +20 -0
  62. package/tests/fixtures/code-graph/sample.ts +15 -0
  63. package/tests/graph-aware-file-tools.test.ts +411 -0
  64. package/tests/guard.test.ts +27 -1
  65. package/tests/plans.test.ts +10 -0
  66. package/tests/refine-prompts.test.ts +101 -2
  67. package/tests/refine-ui.test.ts +371 -72
  68. package/tests/state.test.ts +32 -0
  69. package/tests/subagent.test.ts +48 -20
  70. package/tools/analyze-refs.ts +263 -0
  71. package/tools/ask-choice.ts +159 -11
  72. package/tools/code-graph.ts +277 -0
  73. package/tools/graph-aware-file-tools.ts +392 -0
  74. package/tools/plans.ts +97 -2
  75. package/tools/refine.ts +61 -15
package/src/guard.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
- import { getRun, readActive, resolveStateRootOrNull } from "./state.ts";
10
+ import { getRun, loadConfig, readActive, resolveStateRootOrNull } from "./state.ts";
11
11
 
12
12
  const GUARDED_TOOLS = new Set(["write", "edit"]);
13
13
  const GUARDED_STATUSES = new Set(["planning", "accepted"]);
@@ -31,6 +31,19 @@ export function planningWriteBlockReason(input: GuardInput): string | null {
31
31
  const allowedRoots = [stateRoot, active.artifact_dir, path.join(os.homedir(), ".cache", "pi-plans")].filter(
32
32
  (root): root is string => root !== null,
33
33
  );
34
+ // A configured refs root (plan-with-refs downloads) is writable while planning.
35
+ if (stateRoot !== null) {
36
+ try {
37
+ const config = loadConfig(stateRoot);
38
+ if (config.refs_root) {
39
+ allowedRoots.push(
40
+ path.isAbsolute(config.refs_root) ? config.refs_root : path.resolve(input.workdir, config.refs_root),
41
+ );
42
+ }
43
+ } catch {
44
+ /* config read failed: no extra root */
45
+ }
46
+ }
34
47
  const allowed = allowedRoots.some((root) => target === root || target.startsWith(`${root}${path.sep}`));
35
48
  if (allowed) return null;
36
49
 
@@ -30,6 +30,23 @@ Authority boundary: read-only analysis only. Do not edit, write, delete, commit,
30
30
  Evidence: inspect the repository with read, grep, find, and ls before judging the plan.${lensLine}${focusLine}${contextLine}`;
31
31
  }
32
32
 
33
+ /** Shared header for the post-execution implementation review: the
34
+ * accepted plan is the contract, the IMPLEMENTATION in the worktree is
35
+ * under review. Findings must anchor to the plan's goals/acceptance
36
+ * criteria and explicitly assess delivery maturity. */
37
+ function buildImplementationSharedHeader(role: "reviewer" | "criticizer", opts: RefinePromptInput): string {
38
+ const lensLine = role === "reviewer" && opts.lens ? `\nReview lens: ${opts.lens}.` : "";
39
+ const focusLine = opts.focus ? `\n\nSpecific concerns from the main agent: ${opts.focus}` : "";
40
+ const contextLine = opts.context ? `\n\nContext: ${opts.context}` : "";
41
+ return `Goal: ${role === "reviewer" ? "review the implemented result in the worktree against the plan" : "stress-test the implemented result's assumptions"}.
42
+
43
+ Target: ${opts.planPath} (the accepted plan; the IMPLEMENTATION in the worktree is under review)
44
+
45
+ Authority boundary: read-only analysis only. Do not edit, write, delete, commit, push, or spawn subagents.
46
+
47
+ Evidence: inspect the repository with read, grep, find, and ls before judging the implementation. Judge the implementation against the plan's goals, verifier checklist, and acceptance criteria. Explicitly assess delivery maturity: did the executor ship a minimal MVP only, or refine for long-term growth (no stopgaps, long-term architectural decisions, missing tests, technical debt, production readiness)? Calibrate severity accordingly. Out-of-scope improvement ideas are low severity by default and must not be forced into findings.${lensLine}${focusLine}${contextLine}`;
48
+ }
49
+
33
50
  export function reviewerLanes(count: number): ReviewerLane[] {
34
51
  if (count === 3) return [...REVIEWER_LENSES];
35
52
  if (count === 2) return [...REVIEWER_LENSES.slice(0, 2)];
@@ -68,3 +85,95 @@ Plan file: ${opts.planPath}
68
85
  ${opts.planText}
69
86
  ---8<--- END PLAN CONTENT ---8<---`;
70
87
  }
88
+
89
+ export function buildImplementationReviewerTask(opts: RefinePromptInput): string {
90
+ return `${buildImplementationSharedHeader("reviewer", opts)}
91
+
92
+ Success criteria: return evidence-backed findings or explicitly say the implementation holds up.
93
+
94
+ Output: Markdown, highest severity first. For each finding use this shape:
95
+ - \`F-###\` — severity: high | medium | low; affected plan IDs (e.g. R-001, I-003) or files; evidence: repo path/command that proves it; impact; recommended fix; suggested disposition (accept | reject | needs-discussion).
96
+
97
+ Surface at most five high-priority findings; list lower-severity findings after them. If the implementation holds up, say so explicitly and list what you checked.
98
+
99
+ Plan file: ${opts.planPath}
100
+
101
+ ---8<--- PLAN CONTENT ---8<---
102
+ ${opts.planText}
103
+ ---8<--- END PLAN CONTENT ---8<---`;
104
+ }
105
+
106
+ export interface RefAnalystTaskInput {
107
+ refId: string;
108
+ localPath: string;
109
+ title?: string;
110
+ url?: string;
111
+ kind?: string;
112
+ context?: string;
113
+ languageTag?: string | null;
114
+ }
115
+
116
+ const REF_ANALYST_SECTIONS = [
117
+ "Overview",
118
+ "Key Mechanisms And Design Tradeoffs",
119
+ "Adoptable Ideas For The Target Repo",
120
+ "Pitfalls And Anti-Patterns",
121
+ "Evidence Citations",
122
+ "Coverage",
123
+ "Evidence Gaps",
124
+ ] as const;
125
+
126
+ export function refAnalystSections(): readonly string[] {
127
+ return REF_ANALYST_SECTIONS;
128
+ }
129
+
130
+ /** Task brief for the plan-with-refs per-reference analysis subagent. */
131
+ export function buildRefAnalystTask(opts: RefAnalystTaskInput): string {
132
+ const titleLine = opts.title ? `\nTitle: ${opts.title}` : "";
133
+ const urlLine = opts.url ? `\nURL: ${opts.url}` : "";
134
+ const kindLine = opts.kind ? `\nKind: ${opts.kind}` : "";
135
+ const contextLine = opts.context ? `\n\nTarget repo context: ${opts.context}` : "";
136
+ const languageLine = opts.languageTag
137
+ ? `\n\nWrite all prose in the language with BCP47 tag "${opts.languageTag}". Keep file paths, identifiers, and code snippets verbatim.`
138
+ : "";
139
+ const template = REF_ANALYST_SECTIONS.map((section) => `## ${section}`).join("\n\n(empty)\n\n");
140
+ return `Goal: deep-read this downloaded reference and extract what the target repository should adopt from it.
141
+
142
+ Reference id: ${opts.refId}${titleLine}${urlLine}${kindLine}
143
+ Local path (your working directory): ${opts.localPath}
144
+
145
+ Authority boundary: read-only analysis only. Do not edit, write, delete, commit, push, or spawn subagents. Stay inside the reference directory.
146
+
147
+ Evidence: inspect the reference with read, grep, find, and ls before judging it. Cite files as <relative-path>:<line> for every claim; quote only what you verified.${contextLine}${languageLine}
148
+
149
+ Success criteria: a structured analysis the main agent can paste into REF_ANALYSIS.md and turn into adoption questions.
150
+
151
+ Output: Markdown with exactly these seven top-level sections, in this order:
152
+
153
+ ${template}
154
+
155
+ Section contracts:
156
+ - Overview: what the reference is, its scope, maturity, and license (when discoverable).
157
+ - Key Mechanisms And Design Tradeoffs: the mechanisms that make it work and the tradeoffs they embody.
158
+ - Adoptable Ideas For The Target Repo: concrete, portable ideas ranked by expected value; name the target-repo surface each would touch.
159
+ - Pitfalls And Anti-Patterns: what to avoid when borrowing; failure modes the reference itself documents or exhibits.
160
+ - Evidence Citations: the file:line references backing the claims above.
161
+ - Coverage: which parts of the reference you actually read versus skipped.
162
+ - Evidence Gaps: what you could not determine from the reference alone.`;
163
+ }
164
+
165
+ export function buildImplementationCriticizerTask(opts: RefinePromptInput): string {
166
+ return `${buildImplementationSharedHeader("criticizer", opts)}
167
+
168
+ Success criteria: return concrete, answerable questions only; never rewrite the plan or the implementation.
169
+
170
+ Output: Markdown in exactly this shape:
171
+ 1. A summary of your core criticism in at most three sentences, highlighting the single most important point.
172
+ 2. Then at most five adaptive questions, numbered, each with one line of why it matters. Questions must be answerable by a user with repo access — never rhetorical. Stop earlier if the implementation genuinely holds.
173
+
174
+ Plan file: ${opts.planPath}
175
+
176
+ ---8<--- PLAN CONTENT ---8<---
177
+ ${opts.planText}
178
+ ---8<--- END PLAN CONTENT ---8<---`;
179
+ }
@@ -9,8 +9,32 @@
9
9
 
10
10
  const ELLIPSIS = "…";
11
11
 
12
- export function visibleWidth(text: string): number {
13
- if (!text) return 0;
12
+ /**
13
+ * ECMA-48 CSI sequence: ESC [ parameter bytes (0x30-0x3F), intermediate bytes
14
+ * (0x20-0x2F), final byte (0x40-0x7E). Matched atomically so styling payloads
15
+ * never leak into width math and are never split mid-sequence.
16
+ */
17
+ const CSI_PATTERN = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
18
+
19
+ interface AnsiPart {
20
+ kind: "csi" | "text";
21
+ value: string;
22
+ }
23
+
24
+ function splitAnsi(text: string): AnsiPart[] {
25
+ const parts: AnsiPart[] = [];
26
+ let last = 0;
27
+ for (const match of text.matchAll(CSI_PATTERN)) {
28
+ const start = match.index ?? 0;
29
+ if (start > last) parts.push({ kind: "text", value: text.slice(last, start) });
30
+ parts.push({ kind: "csi", value: match[0] });
31
+ last = start + match[0].length;
32
+ }
33
+ if (last < text.length) parts.push({ kind: "text", value: text.slice(last) });
34
+ return parts;
35
+ }
36
+
37
+ function graphemeWidth(text: string): number {
14
38
  let width = 0;
15
39
  for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
16
40
  const cp = segment.segment.codePointAt(0) ?? 0;
@@ -26,6 +50,16 @@ export function visibleWidth(text: string): number {
26
50
  return width;
27
51
  }
28
52
 
53
+ export function visibleWidth(text: string): number {
54
+ if (!text) return 0;
55
+ let width = 0;
56
+ for (const part of splitAnsi(text)) {
57
+ if (part.kind === "csi") continue; // escape sequences render at zero width
58
+ width += graphemeWidth(part.value);
59
+ }
60
+ return width;
61
+ }
62
+
29
63
  export function truncateToWidth(text: string, maxWidth: number, ellipsis = ELLIPSIS): string {
30
64
  const measured = visibleWidth(text);
31
65
  if (measured <= maxWidth) return text;
@@ -33,12 +67,25 @@ export function truncateToWidth(text: string, maxWidth: number, ellipsis = ELLIP
33
67
  const target = Math.max(0, maxWidth - ellipsisWidth);
34
68
  let result = "";
35
69
  let used = 0;
36
- for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
37
- const w = visibleWidth(segment.segment);
38
- if (used + w > target) break;
39
- result += segment.segment;
40
- used += w;
70
+ let exhausted = false;
71
+ for (const part of splitAnsi(text)) {
72
+ if (part.kind === "csi") {
73
+ if (!exhausted) result += part.value; // keep styling runs intact; never split them
74
+ continue;
75
+ }
76
+ for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(part.value)) {
77
+ const w = visibleWidth(segment.segment);
78
+ if (used + w > target) {
79
+ exhausted = true;
80
+ break;
81
+ }
82
+ result += segment.segment;
83
+ used += w;
84
+ }
41
85
  }
86
+ // A kept open SGR (its reset lived past the cut) must not bleed into the
87
+ // ellipsis or anything rendered after this string.
88
+ if (exhausted && result.includes("\x1b[")) result += "\x1b[0m";
42
89
  return ellipsis ? `${result}${ellipsis}` : result;
43
90
  }
44
91
 
@@ -53,19 +100,25 @@ export function wrapTextWithAnsi(text: string, maxWidth: number): string[] {
53
100
  if (word === "") continue;
54
101
  const wordWidth = visibleWidth(word);
55
102
  if (wordWidth > maxWidth) {
56
- // Hard split a single overlong word.
103
+ // Hard split a single overlong word (ANSI-aware: CSI runs are zero-width and atomic).
57
104
  if (current) { lines.push(current); current = ""; currentWidth = 0; }
58
105
  let buffer = "";
59
106
  let bufferWidth = 0;
60
- for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(word)) {
61
- const w = visibleWidth(segment.segment);
62
- if (bufferWidth + w > maxWidth) {
63
- lines.push(buffer);
64
- buffer = segment.segment;
65
- bufferWidth = w;
66
- } else {
67
- buffer += segment.segment;
68
- bufferWidth += w;
107
+ for (const part of splitAnsi(word)) {
108
+ if (part.kind === "csi") {
109
+ buffer += part.value;
110
+ continue;
111
+ }
112
+ for (const segment of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(part.value)) {
113
+ const w = visibleWidth(segment.segment);
114
+ if (bufferWidth + w > maxWidth) {
115
+ lines.push(buffer);
116
+ buffer = segment.segment;
117
+ bufferWidth = w;
118
+ } else {
119
+ buffer += segment.segment;
120
+ bufferWidth += w;
121
+ }
69
122
  }
70
123
  }
71
124
  if (buffer) { lines.push(buffer); buffer = ""; bufferWidth = 0; }
@@ -86,4 +139,4 @@ export function wrapTextWithAnsi(text: string, maxWidth: number): string[] {
86
139
 
87
140
  export function matchesEscape(data: string): boolean {
88
141
  return data === "\x1b" || data === "\x1b\x1b" || /^(\x1b\[\??\d*[A-Za-z])|(\x1bO[A-Za-z])$/.test(data);
89
- }
142
+ }
@@ -1,7 +1,18 @@
1
1
  import type { SubagentProgressEvent, SubagentResult } from "./subagent.ts";
2
2
 
3
- export type RefineOverlayRole = "reviewer" | "criticizer";
3
+ export type RefineOverlayRole = "reviewer" | "criticizer" | "refs";
4
4
  export type RefineLaneStatus = "queued" | "running" | "complete" | "failed" | "cancelled";
5
+ export type RefineTranscriptEntryType = "assistant-text" | "thinking" | "tool-call" | "tool-result" | "diagnostic";
6
+
7
+ export interface RefineTranscriptEntry {
8
+ id: string;
9
+ type: RefineTranscriptEntryType;
10
+ text: string;
11
+ streaming: boolean;
12
+ toolCallId?: string;
13
+ toolName?: string;
14
+ isError?: boolean;
15
+ }
5
16
 
6
17
  export interface RefineLaneState {
7
18
  id: string;
@@ -9,11 +20,11 @@ export interface RefineLaneState {
9
20
  status: RefineLaneStatus;
10
21
  phase: string;
11
22
  detail: string;
12
- }
13
-
14
- function shorten(text: string, maxLength: number): string {
15
- const normalized = text.replace(/\s+/g, " ").trim();
16
- return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 3)}...` : normalized;
23
+ transcript: RefineTranscriptEntry[];
24
+ currentTurnIndex: number;
25
+ scrollOffset: number;
26
+ followTranscript: boolean;
27
+ viewportHeight: number;
17
28
  }
18
29
 
19
30
  export function statusLabel(status: RefineLaneStatus): string {
@@ -31,6 +42,56 @@ export function statusLabel(status: RefineLaneStatus): string {
31
42
  }
32
43
  }
33
44
 
45
+ function transcriptId(lane: RefineLaneState, event: Extract<SubagentProgressEvent, { type: "transcript" }>): string {
46
+ return `${lane.currentTurnIndex}:${event.key}`;
47
+ }
48
+
49
+ function phaseForEntry(event: Extract<SubagentProgressEvent, { type: "transcript" }>): string {
50
+ switch (event.entryType) {
51
+ case "assistant-text":
52
+ return "responding";
53
+ case "thinking":
54
+ return "thinking";
55
+ case "tool-call":
56
+ return event.toolName ? `tool: ${event.toolName}` : "tool call";
57
+ case "tool-result":
58
+ return event.toolName ? `result: ${event.toolName}` : "tool result";
59
+ case "diagnostic":
60
+ return "diagnostic";
61
+ }
62
+ }
63
+
64
+ function applyTranscriptEvent(lane: RefineLaneState, event: Extract<SubagentProgressEvent, { type: "transcript" }>): void {
65
+ const id = transcriptId(lane, event);
66
+ let entry = lane.transcript.find((candidate) => candidate.id === id);
67
+ if (!entry && event.entryType === "tool-call" && event.toolCallId) {
68
+ entry = lane.transcript.find((candidate) => candidate.type === "tool-call" && candidate.toolCallId === event.toolCallId);
69
+ }
70
+ if (!entry) {
71
+ entry = { id, type: event.entryType, text: "", streaming: event.streaming };
72
+ lane.transcript.push(entry);
73
+ }
74
+ if (event.update === "append") entry.text += event.text;
75
+ else entry.text = event.text;
76
+ entry.streaming = event.streaming;
77
+ if (event.toolCallId !== undefined) entry.toolCallId = event.toolCallId;
78
+ if (event.toolName !== undefined) entry.toolName = event.toolName;
79
+ if (event.isError !== undefined) entry.isError = event.isError;
80
+ lane.detail = entry.text;
81
+ lane.status = "running";
82
+ lane.phase = phaseForEntry(event);
83
+ }
84
+
85
+ function applyDiagnostic(lane: RefineLaneState, text: string): void {
86
+ if (!text) return;
87
+ const id = `${lane.currentTurnIndex}:diagnostic`;
88
+ const existing = lane.transcript.find((entry) => entry.id === id);
89
+ if (existing) existing.text += text;
90
+ else lane.transcript.push({ id, type: "diagnostic", text, streaming: true });
91
+ lane.detail = text;
92
+ lane.phase = "diagnostic";
93
+ }
94
+
34
95
  export function applyRefineProgress(lane: RefineLaneState, event: SubagentProgressEvent): void {
35
96
  if (lane.status === "complete" || lane.status === "failed" || lane.status === "cancelled") return;
36
97
 
@@ -39,40 +100,45 @@ export function applyRefineProgress(lane: RefineLaneState, event: SubagentProgre
39
100
  lane.phase = event.phase === "started" ? "starting" : "exiting";
40
101
  return;
41
102
  case "turn":
42
- lane.status = "running";
43
- lane.phase = event.phase === "start" ? "thinking" : "waiting";
103
+ if (event.phase === "start") {
104
+ lane.status = "running";
105
+ lane.phase = "thinking";
106
+ lane.currentTurnIndex = event.turnIndex ?? lane.currentTurnIndex + 1;
107
+ } else {
108
+ lane.phase = "waiting";
109
+ }
44
110
  return;
45
- case "message":
46
- lane.status = "running";
47
- lane.phase = event.role === "assistant" ? "responding" : event.role;
48
- if (event.text) lane.detail = shorten(event.text, 180);
49
- return;
50
- case "tool":
51
- lane.status = "running";
52
- lane.phase = `tool: ${event.toolName}`;
53
- if (event.detail) lane.detail = shorten(event.detail, 180);
111
+ case "transcript":
112
+ applyTranscriptEvent(lane, event);
54
113
  return;
55
114
  case "stderr":
56
- lane.phase = "diagnostic";
57
- if (event.text) lane.detail = shorten(event.text, 180);
115
+ applyDiagnostic(lane, event.text);
58
116
  return;
59
117
  }
60
118
  }
61
119
 
62
120
  export function applyRefineResult(lane: RefineLaneState, result: SubagentResult): void {
121
+ const finalText = result.output || result.errorMessage || result.stderr || "";
122
+ const finalEntry = [...lane.transcript].reverse().find((entry) => entry.type === "assistant-text");
123
+ if (finalText) {
124
+ if (finalEntry) {
125
+ finalEntry.text = finalText;
126
+ finalEntry.streaming = false;
127
+ } else {
128
+ lane.transcript.push({ id: `${lane.currentTurnIndex}:result`, type: "assistant-text", text: finalText, streaming: false });
129
+ }
130
+ }
131
+ lane.detail = finalText;
63
132
  if (result.ok) {
64
133
  lane.status = "complete";
65
134
  lane.phase = "complete";
66
- lane.detail = shorten(result.output, 180);
67
135
  return;
68
136
  }
69
137
  if (result.cancelled) {
70
138
  lane.status = "cancelled";
71
139
  lane.phase = "cancelled";
72
- lane.detail = shorten(result.errorMessage ?? "Subagent was aborted", 180);
73
140
  return;
74
141
  }
75
142
  lane.status = "failed";
76
143
  lane.phase = result.timedOut ? "timed out" : "failed";
77
- lane.detail = shorten((result.errorMessage ?? result.stderr) || "Subagent failed", 180);
78
144
  }