ultimate-pi 0.13.1 → 0.15.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 (66) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +42 -22
  2. package/.agents/skills/harness-orchestration/SKILL.md +3 -3
  3. package/.agents/skills/harness-plan/SKILL.md +10 -8
  4. package/.pi/agents/harness/planning/decompose.md +4 -2
  5. package/.pi/agents/harness/planning/execution-plan-author.md +25 -14
  6. package/.pi/agents/harness/planning/hypothesis-validator.md +21 -5
  7. package/.pi/agents/harness/planning/implementation-researcher.md +42 -0
  8. package/.pi/agents/harness/planning/plan-adversary.md +20 -4
  9. package/.pi/agents/harness/planning/plan-evaluator.md +28 -5
  10. package/.pi/agents/harness/planning/review-integrator.md +25 -9
  11. package/.pi/agents/harness/planning/scout-graphify.md +1 -1
  12. package/.pi/agents/harness/planning/sprint-contract-auditor.md +19 -4
  13. package/.pi/agents/harness/planning/stack-researcher.md +19 -10
  14. package/.pi/extensions/debate-orchestrator.ts +39 -435
  15. package/.pi/extensions/harness-debate-tools.ts +741 -0
  16. package/.pi/extensions/harness-live-widget.ts +39 -159
  17. package/.pi/extensions/harness-plan-approval.ts +88 -22
  18. package/.pi/extensions/harness-run-context.ts +18 -0
  19. package/.pi/extensions/lib/debate-bus-core.ts +488 -0
  20. package/.pi/extensions/lib/debate-bus-state.ts +64 -0
  21. package/.pi/extensions/lib/harness-spawn-budget.ts +5 -25
  22. package/.pi/extensions/lib/plan-approval/dialog.ts +33 -272
  23. package/.pi/extensions/lib/plan-approval/format-plan.ts +12 -85
  24. package/.pi/extensions/lib/plan-approval/plan-review.ts +62 -6
  25. package/.pi/extensions/lib/plan-approval/render.ts +6 -0
  26. package/.pi/extensions/lib/plan-approval/types.ts +1 -0
  27. package/.pi/extensions/lib/plan-approval/validate.ts +1 -1
  28. package/.pi/extensions/lib/plan-debate-eligibility.ts +214 -0
  29. package/.pi/extensions/lib/plan-debate-envelope.ts +2 -0
  30. package/.pi/extensions/lib/plan-debate-focus.ts +151 -0
  31. package/.pi/extensions/lib/plan-debate-gate.ts +198 -0
  32. package/.pi/extensions/lib/plan-debate-id.ts +39 -0
  33. package/.pi/extensions/lib/plan-debate-lane.ts +220 -0
  34. package/.pi/extensions/lib/plan-debate-lanes.ts +44 -0
  35. package/.pi/extensions/lib/plan-debate-round-status.ts +137 -0
  36. package/.pi/extensions/lib/plan-debate-write-guard.ts +20 -0
  37. package/.pi/extensions/lib/plan-messenger.ts +352 -0
  38. package/.pi/extensions/lib/plan-review-integrator-rules.ts +119 -0
  39. package/.pi/extensions/lib/plan-scope-guard.ts +89 -0
  40. package/.pi/extensions/policy-gate.ts +1 -1
  41. package/.pi/harness/README.md +1 -1
  42. package/.pi/harness/agents.manifest.json +16 -12
  43. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +1 -3
  44. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +13 -5
  45. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +51 -0
  46. package/.pi/harness/docs/adrs/README.md +2 -0
  47. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/implementation-research.yaml +28 -0
  48. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r1.yaml +24 -0
  49. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r2.yaml +25 -0
  50. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-packet.yaml +196 -0
  51. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-review.md +14 -0
  52. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/research-brief.yaml +62 -0
  53. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/implementation-research.yaml +28 -0
  54. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r2.yaml +24 -0
  55. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r3.yaml +24 -0
  56. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +29 -0
  57. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +97 -16
  58. package/.pi/harness/specs/plan-implementation-research-brief.schema.json +128 -0
  59. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  60. package/.pi/harness/specs/round-result.schema.json +15 -2
  61. package/.pi/lib/harness-ui-state.ts +92 -0
  62. package/.pi/prompts/harness-plan.md +90 -30
  63. package/.pi/prompts/planning-rubrics.md +31 -0
  64. package/CHANGELOG.md +23 -0
  65. package/package.json +3 -3
  66. package/.pi/extensions/lib/plan-approval/fallback.ts +0 -50
@@ -1,291 +1,52 @@
1
1
  import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
- import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
3
- import { formatPlanPacketLines } from "./format-plan.js";
2
+ import { runAskDialog } from "../ask-user/dialog.js";
3
+ import { runAskFallback } from "../ask-user/fallback.js";
4
+ import type { ValidatedAskParams } from "../ask-user/types.js";
5
+ import { formatPlanPacketMarkdown } from "./plan-review.js";
4
6
  import type {
5
7
  PlanApprovalDialogResult,
6
8
  ValidatedApprovePlanParams,
7
9
  } from "./types.js";
8
10
 
9
- type FocusRegion = "plan" | "options";
10
-
11
- interface CustomAnswer {
12
- response: { kind: "selection"; selections: string[] };
13
- }
14
-
15
- /** Lines reserved below overlay: harness-live widget + editor + footer. */
16
- export const PLAN_APPROVAL_BOTTOM_RESERVE_LINES = 11;
17
- /** Estimate agents widget height when stacking above harness live. */
18
- export const PLAN_APPROVAL_AGENTS_TOP_RESERVE_LINES = 12;
19
- export const PLAN_APPROVAL_MIN_VIEWPORT = 6;
20
-
21
- export function computePlanViewport(
22
- availableHeight: number,
23
- chromeLines: number,
24
- ): number {
25
- return Math.max(PLAN_APPROVAL_MIN_VIEWPORT, availableHeight - chromeLines);
26
- }
27
-
28
- export function computePlanOverlayMaxHeight(termHeight: number): number {
29
- return Math.max(
30
- PLAN_APPROVAL_MIN_VIEWPORT + 8,
31
- termHeight -
32
- PLAN_APPROVAL_BOTTOM_RESERVE_LINES -
33
- PLAN_APPROVAL_AGENTS_TOP_RESERVE_LINES,
34
- );
35
- }
11
+ export type RunPlanApprovalDialogOptions = {
12
+ onMounted?: () => void;
13
+ hasUI?: boolean;
14
+ };
36
15
 
37
- function countPlanChromeLines(
16
+ /** Full plan body shown in the transcript before the approval prompt. */
17
+ export function buildPlanApprovalMarkdown(
38
18
  validated: ValidatedApprovePlanParams,
39
- displayOptions: ValidatedApprovePlanParams["options"],
40
- useOverlay: boolean,
41
- ): number {
42
- let chrome = useOverlay ? 2 : 0; // borders
43
- chrome += 1; // title
44
- if (validated.human_summary) {
45
- chrome += validated.human_summary.split("\n").length;
46
- }
47
- chrome += 1; // blank before plan
48
- chrome += 1; // plan label
49
- chrome += 1; // blank before options
50
- chrome += 1; // options label
51
- for (const opt of displayOptions) {
52
- chrome += 1;
53
- if (opt.description) chrome += 1;
54
- }
55
- chrome += 1; // blank before hints
56
- chrome += 1; // hints
57
- return chrome;
19
+ ): string {
20
+ return formatPlanPacketMarkdown(validated.plan_packet, {
21
+ human_summary: validated.human_summary,
22
+ status: "draft",
23
+ research_brief: validated.research_brief,
24
+ }).trim();
58
25
  }
59
26
 
60
- function withTimeout<T>(
61
- promise: Promise<T | null>,
62
- ms: number | undefined,
63
- ): Promise<T | null> {
64
- if (!ms) return promise;
65
- return Promise.race([
66
- promise,
67
- new Promise<null>((resolve) => {
68
- setTimeout(() => resolve(null), ms);
69
- }),
70
- ]);
27
+ function toAskParams(
28
+ validated: ValidatedApprovePlanParams,
29
+ ): ValidatedAskParams {
30
+ return {
31
+ question: "How would you like to proceed with this harness plan?",
32
+ context: buildPlanApprovalMarkdown(validated),
33
+ options: validated.options,
34
+ allowMultiple: false,
35
+ allowFreeform: false,
36
+ // Inline prompt below the plan — no full-screen overlay.
37
+ displayMode: "inline",
38
+ };
71
39
  }
72
40
 
73
- export type RunPlanApprovalDialogOptions = {
74
- onMounted?: () => void;
75
- };
76
-
77
41
  export async function runPlanApprovalDialog(
78
42
  ui: ExtensionUIContext,
79
43
  validated: ValidatedApprovePlanParams,
80
44
  options?: RunPlanApprovalDialogOptions,
81
45
  ): Promise<PlanApprovalDialogResult> {
82
- const planLines = formatPlanPacketLines(validated.plan_packet, 100);
83
- const displayOptions = validated.options;
84
- const useOverlay = validated.displayMode !== "inline";
85
- let overlayTermHeight = 24;
86
-
87
- const result = await withTimeout(
88
- ui.custom<CustomAnswer | null>(
89
- (tui, theme, _kb, done) => {
90
- const tuiHeight = (tui as unknown as { height?: number }).height;
91
- overlayTermHeight =
92
- typeof tuiHeight === "number" && tuiHeight > 10 ? tuiHeight : 24;
93
- options?.onMounted?.();
94
-
95
- let scrollOffset = 0;
96
- let optionIndex = 0;
97
- let focus: FocusRegion = "plan";
98
- let cachedLines: string[] | undefined;
99
-
100
- function refresh() {
101
- cachedLines = undefined;
102
- tui.requestRender();
103
- }
104
-
105
- function submitSelection() {
106
- const opt = displayOptions[optionIndex];
107
- done({
108
- response: { kind: "selection", selections: [opt.title] },
109
- });
110
- }
111
-
112
- function handleInput(data: string) {
113
- if (focus === "plan") {
114
- if (matchesKey(data, Key.up) || data === "k") {
115
- scrollOffset = Math.max(0, scrollOffset - 1);
116
- refresh();
117
- return;
118
- }
119
- if (matchesKey(data, Key.down) || data === "j") {
120
- scrollOffset += 1;
121
- refresh();
122
- return;
123
- }
124
- if (matchesKey(data, Key.pageUp)) {
125
- scrollOffset = Math.max(0, scrollOffset - 8);
126
- refresh();
127
- return;
128
- }
129
- if (matchesKey(data, Key.pageDown)) {
130
- scrollOffset += 8;
131
- refresh();
132
- return;
133
- }
134
- if (matchesKey(data, Key.tab)) {
135
- focus = "options";
136
- refresh();
137
- return;
138
- }
139
- }
140
-
141
- if (focus === "options") {
142
- if (matchesKey(data, Key.up)) {
143
- optionIndex = Math.max(0, optionIndex - 1);
144
- refresh();
145
- return;
146
- }
147
- if (matchesKey(data, Key.down)) {
148
- optionIndex = Math.min(
149
- displayOptions.length - 1,
150
- optionIndex + 1,
151
- );
152
- refresh();
153
- return;
154
- }
155
- if (matchesKey(data, Key.tab)) {
156
- focus = "plan";
157
- refresh();
158
- return;
159
- }
160
- if (matchesKey(data, Key.enter)) {
161
- submitSelection();
162
- return;
163
- }
164
- }
165
-
166
- if (matchesKey(data, Key.escape)) {
167
- done(null);
168
- }
169
- }
170
-
171
- function render(width: number): string[] {
172
- if (cachedLines) return cachedLines;
173
-
174
- const lines: string[] = [];
175
- const add = (s: string) => lines.push(truncateToWidth(s, width));
176
- const dims = (tui as { height?: number }).height;
177
- const termHeight = typeof dims === "number" && dims > 10 ? dims : 24;
178
- const chromeLines = countPlanChromeLines(
179
- validated,
180
- displayOptions,
181
- useOverlay,
182
- );
183
-
184
- let availableHeight: number;
185
- if (useOverlay) {
186
- const overlayMax = computePlanOverlayMaxHeight(termHeight);
187
- availableHeight = overlayMax;
188
- } else {
189
- availableHeight = termHeight - PLAN_APPROVAL_BOTTOM_RESERVE_LINES;
190
- }
191
- const planViewport = computePlanViewport(
192
- availableHeight,
193
- chromeLines,
194
- );
195
-
196
- if (useOverlay) {
197
- add(theme.fg("accent", "─".repeat(width)));
198
- }
199
-
200
- add(theme.fg("accent", " Plan approval"));
201
- if (validated.human_summary) {
202
- for (const line of validated.human_summary.split("\n")) {
203
- add(theme.fg("muted", ` ${line}`));
204
- }
205
- }
206
- lines.push("");
207
-
208
- const maxScroll = Math.max(0, planLines.length - planViewport);
209
- scrollOffset = Math.min(scrollOffset, maxScroll);
210
- const visible = planLines.slice(
211
- scrollOffset,
212
- scrollOffset + planViewport,
213
- );
214
- const planLabel =
215
- focus === "plan"
216
- ? theme.fg("accent", " [plan — ↑↓/Pg scroll, Tab → options]")
217
- : theme.fg("dim", " [plan]");
218
- add(planLabel);
219
- for (const line of visible) {
220
- add(theme.fg("text", ` ${line}`));
221
- }
222
- if (planLines.length > planViewport) {
223
- add(
224
- theme.fg(
225
- "dim",
226
- ` … ${scrollOffset + 1}-${scrollOffset + visible.length} of ${planLines.length}`,
227
- ),
228
- );
229
- }
230
- lines.push("");
231
-
232
- const optLabel =
233
- focus === "options"
234
- ? theme.fg("accent", " Options (↑↓, Enter, Tab → plan):")
235
- : theme.fg("dim", " Options (Tab to focus):");
236
- add(optLabel);
237
- for (let i = 0; i < displayOptions.length; i++) {
238
- const opt = displayOptions[i];
239
- const focused = focus === "options" && i === optionIndex;
240
- const prefix = focused ? theme.fg("accent", "> ") : " ";
241
- const num = `${i + 1}. `;
242
- if (focused) {
243
- add(prefix + theme.fg("accent", `${num}${opt.title}`));
244
- } else {
245
- add(`${prefix}${theme.fg("text", `${num}${opt.title}`)}`);
246
- }
247
- if (opt.description) {
248
- add(` ${theme.fg("muted", opt.description)}`);
249
- }
250
- }
251
-
252
- lines.push("");
253
- add(theme.fg("dim", " Tab: plan ↔ options • Esc: cancel"));
254
-
255
- if (useOverlay) {
256
- add(theme.fg("accent", "─".repeat(width)));
257
- }
258
-
259
- cachedLines = lines;
260
- return lines;
261
- }
262
-
263
- return {
264
- render,
265
- invalidate: () => {
266
- cachedLines = undefined;
267
- },
268
- handleInput,
269
- };
270
- },
271
- useOverlay
272
- ? {
273
- overlay: true,
274
- overlayOptions: () => ({
275
- anchor: "bottom-center",
276
- width: "100%",
277
- margin: { bottom: PLAN_APPROVAL_BOTTOM_RESERVE_LINES },
278
- maxHeight: computePlanOverlayMaxHeight(overlayTermHeight),
279
- }),
280
- }
281
- : undefined,
282
- ),
283
- undefined,
284
- );
285
-
286
- if (!result) {
287
- return { response: null, cancelled: true };
46
+ options?.onMounted?.();
47
+ const askParams = toAskParams(validated);
48
+ if (options?.hasUI === false) {
49
+ return runAskFallback(ui, askParams);
288
50
  }
289
-
290
- return { response: result.response, cancelled: false };
51
+ return runAskDialog(ui, askParams);
291
52
  }
@@ -1,94 +1,21 @@
1
1
  import type { PlanPacketLike } from "../../../lib/harness-run-context.js";
2
+ import { stringifyYaml } from "../../../lib/harness-yaml.js";
2
3
 
3
- function wrapLine(text: string, width: number): string[] {
4
- if (width < 20) return [text];
5
- const words = text.split(/\s+/);
6
- const lines: string[] = [];
7
- let line = "";
8
- for (const word of words) {
9
- const next = line ? `${line} ${word}` : word;
10
- if (next.length > width && line) {
11
- lines.push(line);
12
- line = word;
13
- } else {
14
- line = next;
15
- }
16
- }
17
- if (line) lines.push(line);
18
- return lines.length > 0 ? lines : [""];
19
- }
20
-
21
- function riskBadge(risk: string | undefined): string {
22
- const r = (risk ?? "med").toLowerCase();
23
- return `[risk: ${r}]`;
4
+ /** Canonical YAML for plan_packet (same shape as plan-packet.yaml on disk). */
5
+ export function formatPlanPacketYaml(packet: PlanPacketLike): string {
6
+ return stringifyYaml(packet).trimEnd();
24
7
  }
25
8
 
9
+ /** Line array for TUI renderers; preserves YAML structure with optional per-line width cap. */
26
10
  export function formatPlanPacketLines(
27
11
  packet: PlanPacketLike,
28
12
  width: number,
29
13
  ): string[] {
30
- const lines: string[] = [];
31
- const w = Math.max(40, width - 2);
32
- const add = (s: string) => {
33
- for (const part of wrapLine(s, w)) lines.push(part);
34
- };
35
-
36
- lines.push(`plan_id: ${packet.plan_id ?? "?"}`);
37
- lines.push(`task_id: ${packet.task_id ?? "?"}`);
38
- lines.push(
39
- riskBadge(
40
- typeof packet.risk_level === "string" ? packet.risk_level : undefined,
41
- ),
42
- );
43
- lines.push("");
44
- lines.push("scope:");
45
- add(String(packet.scope ?? ""));
46
- lines.push("");
47
-
48
- const assumptions = Array.isArray(packet.assumptions)
49
- ? (packet.assumptions as string[])
50
- : [];
51
- if (assumptions.length > 0) {
52
- lines.push("assumptions:");
53
- for (const a of assumptions) {
54
- add(` • ${a}`);
55
- }
56
- lines.push("");
57
- }
58
-
59
- const checks = Array.isArray(packet.acceptance_checks)
60
- ? (packet.acceptance_checks as string[])
61
- : [];
62
- if (checks.length > 0) {
63
- lines.push("acceptance_checks:");
64
- for (let i = 0; i < checks.length; i++) {
65
- add(` ${i + 1}. ${checks[i]}`);
66
- }
67
- lines.push("");
68
- }
69
-
70
- const rollback = packet.rollback_plan as
71
- | {
72
- rollback_artifacts?: {
73
- revert_command?: string;
74
- revert_branch?: string;
75
- patch_bundle?: string;
76
- };
77
- }
78
- | undefined;
79
- const artifacts = rollback?.rollback_artifacts;
80
- if (artifacts) {
81
- lines.push("rollback:");
82
- if (artifacts.revert_command) {
83
- add(` revert_command: ${artifacts.revert_command}`);
84
- }
85
- if (artifacts.revert_branch) {
86
- add(` revert_branch: ${artifacts.revert_branch}`);
87
- }
88
- if (artifacts.patch_bundle) {
89
- add(` patch_bundle: ${artifacts.patch_bundle}`);
90
- }
91
- }
92
-
93
- return lines;
14
+ const w = Math.max(40, width);
15
+ return formatPlanPacketYaml(packet)
16
+ .split("\n")
17
+ .map((line) => {
18
+ if (line.length <= w) return line;
19
+ return `${line.slice(0, w - 1)}…`;
20
+ });
94
21
  }
@@ -6,7 +6,7 @@ import {
6
6
  type HarnessRunContext,
7
7
  type PlanPacketLike,
8
8
  } from "../../../lib/harness-run-context.js";
9
- import { formatPlanPacketLines } from "./format-plan.js";
9
+ import { formatPlanPacketYaml } from "./format-plan.js";
10
10
  import type { PlanResearchBrief } from "./types.js";
11
11
 
12
12
  export {
@@ -160,6 +160,62 @@ export function formatResearchBriefMarkdown(
160
160
  }
161
161
  }
162
162
 
163
+ const impl = asRecord(research.implementation);
164
+ if (impl) {
165
+ lines.push("## Phase 3.5 — Implementation research");
166
+ lines.push("");
167
+ const framing = str(impl.problem_framing);
168
+ if (framing) {
169
+ lines.push("**Problem framing:**");
170
+ lines.push("");
171
+ lines.push(framing);
172
+ lines.push("");
173
+ }
174
+ const rec = asRecord(impl.recommended_approach);
175
+ if (rec) {
176
+ const summary = str(rec.summary);
177
+ const conf = str(rec.recommended_approach_confidence);
178
+ if (summary) {
179
+ lines.push(
180
+ `**Recommended approach**${conf ? ` (${conf} confidence)` : ""}:`,
181
+ );
182
+ lines.push("");
183
+ lines.push(summary);
184
+ lines.push("");
185
+ }
186
+ const rationale = str(rec.confidence_rationale);
187
+ if (rationale) {
188
+ lines.push(`*Rationale:* ${rationale}`);
189
+ lines.push("");
190
+ }
191
+ }
192
+ const patterns = Array.isArray(impl.solution_patterns)
193
+ ? impl.solution_patterns
194
+ : [];
195
+ if (patterns.length) {
196
+ lines.push("**Solution patterns:**");
197
+ for (const p of patterns) {
198
+ const pat = asRecord(p);
199
+ const name = pat ? str(pat.name) : null;
200
+ const fit = pat ? str(pat.fit) : null;
201
+ if (name) lines.push(`- **${name}**${fit ? `: ${fit}` : ""}`);
202
+ }
203
+ lines.push("");
204
+ }
205
+ const openQs = strList(impl.open_questions);
206
+ if (openQs.length) {
207
+ lines.push("**Open questions:**");
208
+ for (const q of openQs) lines.push(`- ${q}`);
209
+ lines.push("");
210
+ }
211
+ const anti = strList(impl.anti_patterns);
212
+ if (anti.length) {
213
+ lines.push("**Anti-patterns:**");
214
+ for (const a of anti) lines.push(`- ${a}`);
215
+ lines.push("");
216
+ }
217
+ }
218
+
163
219
  if (evalBrief) {
164
220
  lines.push("## Self-evaluation");
165
221
  lines.push("");
@@ -217,7 +273,7 @@ export function formatPlanPacketMarkdown(
217
273
  `- **risk_level:** ${typeof packet.risk_level === "string" ? packet.risk_level : "med"}`,
218
274
  );
219
275
  if (opts?.plan_packet_path) {
220
- lines.push(`- **canonical JSON:** \`${opts.plan_packet_path}\``);
276
+ lines.push(`- **canonical YAML:** \`${opts.plan_packet_path}\``);
221
277
  }
222
278
  lines.push("");
223
279
  if (opts?.human_summary?.trim()) {
@@ -233,15 +289,15 @@ export function formatPlanPacketMarkdown(
233
289
  }
234
290
  lines.push("## Plan packet");
235
291
  lines.push("");
236
- lines.push("```text");
237
- for (const line of formatPlanPacketLines(packet, 100)) {
292
+ lines.push("```yaml");
293
+ for (const line of formatPlanPacketYaml(packet).split("\n")) {
238
294
  lines.push(line);
239
295
  }
240
296
  lines.push("```");
241
297
  lines.push("");
242
298
  if (status === "draft") {
243
299
  lines.push(
244
- "Review this file in your editor, then return to the harness TUI to **Approve**, **Request changes**, or **Cancel**.",
300
+ "Review this plan, then choose **Approve**, **Request changes**, or **Cancel** in the prompt below (same flow as `ask_user`).",
245
301
  );
246
302
  } else if (status === "approved") {
247
303
  lines.push(
@@ -388,6 +444,6 @@ export function formatPlanReviewUserHint(reviewPath: string | null): string {
388
444
  const abs = resolve(reviewPath);
389
445
  return (
390
446
  `Full plan for editor review: ${abs}\n` +
391
- `Open this markdown file in VS Code (or your editor), read the scope and acceptance checks, then return to the harness TUI to Approve / Request changes / Cancel.`
447
+ `Open this markdown file in your editor if you prefer; approval options appear in the harness prompt below.`
392
448
  );
393
449
  }
@@ -61,10 +61,16 @@ export function renderHarnessPlanDraft(
61
61
  details: {
62
62
  plan_packet?: PlanPacketLike;
63
63
  human_summary?: string | null;
64
+ plan_markdown?: string | null;
64
65
  },
65
66
  width: number,
66
67
  theme: Theme,
68
+ content?: string | null,
67
69
  ): string[] {
70
+ const markdown = content?.trim() || details.plan_markdown?.trim();
71
+ if (markdown) {
72
+ return markdown.split("\n").map((line) => truncateToWidth(line, width));
73
+ }
68
74
  const lines: string[] = [];
69
75
  lines.push(theme.fg("accent", "Harness plan (pending approval)"));
70
76
  if (details.human_summary) {
@@ -13,6 +13,7 @@ export interface PlanResearchBrief {
13
13
  hypothesis?: Record<string, unknown> | null;
14
14
  eval?: Record<string, unknown> | null;
15
15
  stack?: Record<string, unknown> | null;
16
+ implementation?: Record<string, unknown> | null;
16
17
  debate?: {
17
18
  rounds?: Record<string, unknown>[];
18
19
  hypothesis_validations?: Record<string, unknown>[];
@@ -36,7 +36,7 @@ export function validateApprovePlanParams(
36
36
  human_summary: params.human_summary?.trim() || undefined,
37
37
  research_brief: params.research_brief ?? undefined,
38
38
  options,
39
- displayMode: params.displayMode ?? "overlay",
39
+ displayMode: params.displayMode ?? "inline",
40
40
  };
41
41
  }
42
42