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
@@ -3,12 +3,14 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
+ deriveHarnessStatusHint,
7
+ formatHarnessPhaseLabel,
8
+ type HarnessStatusSeverity,
6
9
  type HarnessUiState,
7
10
  HarnessUiStateStore,
11
+ nextHarnessPhase,
8
12
  } from "../lib/harness-ui-state";
9
13
 
10
- type Severity = "accent" | "warning" | "error";
11
-
12
14
  type TuiLike = { requestRender(): void };
13
15
  type ThemeLike = {
14
16
  fg(
@@ -164,31 +166,25 @@ function composeZones(left: string, right: string, width: number): string {
164
166
  return fitToWidth(`${leftFit}${" ".repeat(minGap)}${rightFit}`, width);
165
167
  }
166
168
 
167
- type InFlightState = {
168
- toolCount: number;
169
- lastToolName: string | null;
170
- };
169
+ function themeSeverityColor(
170
+ severity: HarnessStatusSeverity,
171
+ ): "accent" | "warning" | "error" | "success" | "muted" {
172
+ return severity;
173
+ }
171
174
 
172
175
  class HarnessWidgetComponent {
173
176
  private widthCache?: number;
174
177
  private linesCache?: string[];
175
178
  private state: HarnessUiState;
176
- private inFlight: InFlightState;
177
179
  private themeRef: ThemeLike;
178
180
 
179
- constructor(
180
- state: HarnessUiState,
181
- inFlight: InFlightState,
182
- theme: ThemeLike,
183
- ) {
181
+ constructor(state: HarnessUiState, theme: ThemeLike) {
184
182
  this.state = state;
185
- this.inFlight = inFlight;
186
183
  this.themeRef = theme;
187
184
  }
188
185
 
189
- public setData(state: HarnessUiState, inFlight: InFlightState): void {
186
+ public setData(state: HarnessUiState): void {
190
187
  this.state = state;
191
- this.inFlight = inFlight;
192
188
  this.invalidate();
193
189
  }
194
190
 
@@ -201,109 +197,23 @@ class HarnessWidgetComponent {
201
197
  if (this.linesCache && this.widthCache === width) return this.linesCache;
202
198
  const theme = this.themeRef;
203
199
  const rowWidth = Math.max(1, width - TERMINAL_WIDTH_SAFETY_MARGIN);
204
- const showDebateRow =
205
- this.state.phase === "adversary" || this.state.phase === "merge";
206
-
207
- const substateColor: Severity =
208
- this.state.flowSubstate === "blocked"
209
- ? "error"
210
- : this.state.flowSubstate === "severity-policy" ||
211
- this.state.flowSubstate === "human-required"
212
- ? "warning"
213
- : "accent";
214
- const policyColor =
215
- this.state.policyDecision === "pass"
216
- ? "success"
217
- : this.state.policyDecision === "conditional_pass"
218
- ? "warning"
219
- : this.state.policyDecision === "block" ||
220
- this.state.policyDecision === "human_required"
221
- ? "error"
222
- : "muted";
223
-
224
- const policyDisplay = this.state.policyDecision ?? "pending";
225
-
226
- const phaseToken = `${theme.fg("dim", "phase:")}${theme.fg("accent", this.state.phase)}`;
227
- const flowToken = `${theme.fg("dim", "flow:")}${theme.fg(substateColor, this.state.flowSubstate)}`;
228
- const policyToken = `${theme.fg("dim", "policy:")}${theme.fg(policyColor, policyDisplay)}`;
229
- const row1 = composeZones(
230
- `${theme.bold("Harness")} ${phaseToken} ${flowToken}`,
231
- policyToken,
232
- rowWidth,
233
- );
234
200
 
235
- const debateProgress =
236
- this.state.debateMaxRounds != null
237
- ? `${this.state.debateRound}/${this.state.debateMaxRounds}`
238
- : String(this.state.debateRound);
239
- const budgetDisplay =
240
- this.state.debateBudgetUsed != null && this.state.debateBudgetCap != null
241
- ? `${this.state.debateBudgetUsed}/${this.state.debateBudgetCap}`
242
- : this.state.debateBudgetUsed != null
243
- ? String(this.state.debateBudgetUsed)
244
- : "n/a";
245
- const consensusTrend =
246
- this.state.consensusDelta == null
247
- ? "flat"
248
- : this.state.consensusDelta > 0
249
- ? "up"
250
- : this.state.consensusDelta < 0
251
- ? "down"
252
- : "flat";
253
- const trendColor =
254
- consensusTrend === "up"
255
- ? "success"
256
- : consensusTrend === "down"
257
- ? "warning"
258
- : "muted";
259
-
260
- const sev = this.state.severity;
261
- const severityCompact =
262
- sev.correctness == null &&
263
- sev.security == null &&
264
- sev.architecture == null &&
265
- sev.testIntegrity == null
266
- ? theme.fg("muted", "sev:n/a")
267
- : `${theme.fg("dim", "sev")} ${theme.fg("accent", `c:${sev.correctness ?? "-"}`)} ${theme.fg("accent", `s:${sev.security ?? "-"}`)} ${theme.fg("accent", `a:${sev.architecture ?? "-"}`)} ${theme.fg("accent", `t:${sev.testIntegrity ?? "-"}`)}`;
268
-
269
- const planFlag = this.state.planApproved
270
- ? `${theme.fg("dim", "📋 Plan:")}${theme.fg("success", "OK")}`
271
- : `${theme.fg("dim", "📋 Plan:")}${theme.fg("error", "NO")}`;
272
- const reviewFlag = this.state.reviewIsolationOk
273
- ? `${theme.fg("dim", "🧪 Review:")}${theme.fg("success", "OK")}`
274
- : `${theme.fg("dim", "🧪 Review:")}${theme.fg("warning", "ISO")}`;
275
- const budgetFlag = this.state.budgetExhausted
276
- ? `${theme.fg("dim", "💰 Budget:")}${theme.fg("error", "HIT")}`
277
- : `${theme.fg("dim", "💰 Budget:")}${theme.fg("success", "OK")}`;
278
- const testsFlag =
279
- this.state.testIntegritySeverity === "high"
280
- ? `${theme.fg("dim", "🛡 Tests:")}${theme.fg("error", "HIGH")}`
281
- : this.state.testIntegritySeverity === "medium"
282
- ? `${theme.fg("dim", "🛡 Tests:")}${theme.fg("warning", "MED")}`
283
- : `${theme.fg("dim", "🛡 Tests:")}${theme.fg("success", "OK")}`;
284
-
285
- const toolDisplay = this.inFlight.lastToolName
286
- ? `${this.inFlight.toolCount}:${this.inFlight.lastToolName}`
287
- : String(this.inFlight.toolCount);
288
- const nextDisplay =
289
- this.state.nextRecommendedCommand != null
290
- ? this.state.nextRecommendedCommand.length > 36
291
- ? `${this.state.nextRecommendedCommand.slice(0, 33)}...`
292
- : this.state.nextRecommendedCommand
293
- : null;
294
- const row3Left = `${planFlag} ${reviewFlag} ${budgetFlag} ${testsFlag}`;
295
- const row3Right = nextDisplay
296
- ? `${theme.fg("dim", "inFlight:")}${theme.fg("accent", toolDisplay)} ${theme.fg("dim", "next:")}${theme.fg("accent", nextDisplay)}`
297
- : `${theme.fg("dim", "inFlight:")}${theme.fg("accent", toolDisplay)}`;
298
- const row3 = composeZones(row3Left, row3Right, rowWidth);
299
-
300
- const lines: string[] = [truncateToWidth(row1, rowWidth)];
301
- if (showDebateRow) {
302
- const debateLeft = `${theme.fg("dim", "Debate")} ${theme.fg("accent", `rounds:${debateProgress}`)} ${theme.fg("dim", "trend:")}${theme.fg(trendColor, consensusTrend)} ${theme.fg("dim", "budget:")}${theme.fg("accent", budgetDisplay)}`;
303
- const row2 = composeZones(debateLeft, severityCompact, rowWidth);
304
- lines.push(truncateToWidth(row2, rowWidth));
305
- }
306
- lines.push(truncateToWidth(row3, rowWidth));
201
+ const currentLabel = formatHarnessPhaseLabel(this.state.phase);
202
+ const nextPhase = nextHarnessPhase(this.state.phase);
203
+ const nowToken = `${theme.fg("dim", "now:")}${theme.fg("accent", currentLabel)}`;
204
+ const phaseToken =
205
+ nextPhase != null
206
+ ? `${nowToken} ${theme.fg("dim", "→")} ${theme.fg("accent", formatHarnessPhaseLabel(nextPhase))}`
207
+ : nowToken;
208
+
209
+ const status = deriveHarnessStatusHint(this.state);
210
+ const statusColor = themeSeverityColor(status.severity);
211
+ const statusToken = theme.fg(statusColor, status.text);
212
+
213
+ const left = `${theme.bold("Harness")} ${phaseToken}`;
214
+ const row = composeZones(left, statusToken, rowWidth);
215
+
216
+ const lines = [truncateToWidth(row, rowWidth)];
307
217
  this.widthCache = width;
308
218
  this.linesCache = lines;
309
219
  return lines;
@@ -316,14 +226,16 @@ class HarnessWidgetComponent {
316
226
  }
317
227
 
318
228
  function statusToken(state: HarnessUiState): string {
319
- const decision = state.policyDecision ?? "pending";
320
- return `h:${state.phase}/${state.flowSubstate}/${decision}`;
229
+ const current = formatHarnessPhaseLabel(state.phase);
230
+ const next = nextHarnessPhase(state.phase);
231
+ const phasePart =
232
+ next != null ? `${current}→${formatHarnessPhaseLabel(next)}` : current;
233
+ const hint = deriveHarnessStatusHint(state).text;
234
+ return `h:${phasePart}|${hint}`;
321
235
  }
322
236
 
323
237
  export default function harnessLiveWidget(pi: ExtensionAPI) {
324
238
  const stateStore = new HarnessUiStateStore();
325
- const inFlightCalls = new Set<string>();
326
- let lastToolName: string | null = null;
327
239
  let widgetMounted = false;
328
240
  let tuiHandle: TuiLike | null = null;
329
241
  let component: HarnessWidgetComponent | null = null;
@@ -334,19 +246,14 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
334
246
  function mountHarnessWidget(ctx: ExtensionContext): void {
335
247
  if (!ctx.hasUI) return;
336
248
  const state = stateStore.refresh(ctx);
337
- const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
338
- lastRenderHash = computeRenderHash(state, inFlight);
249
+ lastRenderHash = computeRenderHash(state);
339
250
 
340
251
  ctx.ui.setWidget(
341
252
  "harness-live",
342
253
  (tui, theme) => {
343
254
  widgetMounted = true;
344
255
  tuiHandle = tui;
345
- component = new HarnessWidgetComponent(
346
- stateStore.snapshot(),
347
- inFlight,
348
- theme,
349
- );
256
+ component = new HarnessWidgetComponent(stateStore.snapshot(), theme);
350
257
  return {
351
258
  render(width: number): string[] {
352
259
  component?.setTheme(theme);
@@ -388,26 +295,15 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
388
295
  ctx.ui.setStatus("harness-mode", undefined);
389
296
  }
390
297
 
391
- function computeRenderHash(
392
- state: HarnessUiState,
393
- inFlight: InFlightState,
394
- ): string {
298
+ function computeRenderHash(state: HarnessUiState): string {
395
299
  return JSON.stringify({
396
300
  phase: state.phase,
397
- flowSubstate: state.flowSubstate,
398
301
  planApproved: state.planApproved,
399
- reviewIsolationOk: state.reviewIsolationOk,
400
302
  budgetExhausted: state.budgetExhausted,
401
303
  testIntegritySeverity: state.testIntegritySeverity,
402
- debateRound: state.debateRound,
403
- debateMaxRounds: state.debateMaxRounds,
404
- debateBudgetUsed: state.debateBudgetUsed,
405
- debateBudgetCap: state.debateBudgetCap,
406
304
  policyDecision: state.policyDecision,
407
- consensusDelta: state.consensusDelta,
408
- severity: state.severity,
305
+ flowSubstate: state.flowSubstate,
409
306
  nextRecommendedCommand: state.nextRecommendedCommand,
410
- inFlight,
411
307
  });
412
308
  }
413
309
 
@@ -417,15 +313,11 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
417
313
  queueMicrotask(() => {
418
314
  refreshQueued = false;
419
315
  const state = stateStore.refresh(ctx);
420
- const inFlight: InFlightState = {
421
- toolCount: inFlightCalls.size,
422
- lastToolName,
423
- };
424
- const hash = computeRenderHash(state, inFlight);
316
+ const hash = computeRenderHash(state);
425
317
  updateStatusFallback(ctx, state);
426
318
  if (hash === lastRenderHash) return;
427
319
  lastRenderHash = hash;
428
- if (component) component.setData(state, inFlight);
320
+ if (component) component.setData(state);
429
321
  tuiHandle?.requestRender();
430
322
  });
431
323
  }
@@ -450,16 +342,4 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
450
342
  pi.on("agent_end", (_event, ctx) => {
451
343
  scheduleRefresh(ctx);
452
344
  });
453
-
454
- pi.on("tool_execution_start", (event, ctx) => {
455
- inFlightCalls.add(event.toolCallId);
456
- lastToolName = event.toolName;
457
- scheduleRefresh(ctx);
458
- });
459
-
460
- pi.on("tool_result", (event, ctx) => {
461
- inFlightCalls.delete(event.toolCallId);
462
- if (inFlightCalls.size === 0) lastToolName = null;
463
- scheduleRefresh(ctx);
464
- });
465
345
  }
@@ -2,6 +2,9 @@
2
2
  * harness-plan-approval — PlanPacket approval UI and transcript renderer for parent sessions.
3
3
  */
4
4
 
5
+ import { constants } from "node:fs";
6
+ import { access } from "node:fs/promises";
7
+ import { join } from "node:path";
5
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
9
  import { Text } from "@earendil-works/pi-tui";
7
10
  import { Type } from "@sinclair/typebox";
@@ -20,8 +23,10 @@ import {
20
23
  executeCreatePlan,
21
24
  formatCreatePlanResultText,
22
25
  } from "./lib/plan-approval/create-plan.js";
23
- import { runPlanApprovalDialog } from "./lib/plan-approval/dialog.js";
24
- import { runPlanApprovalFallback } from "./lib/plan-approval/fallback.js";
26
+ import {
27
+ buildPlanApprovalMarkdown,
28
+ runPlanApprovalDialog,
29
+ } from "./lib/plan-approval/dialog.js";
25
30
  import { writePlanReviewMarkdown } from "./lib/plan-approval/plan-review.js";
26
31
  import {
27
32
  renderApprovePlanCall,
@@ -42,6 +47,7 @@ import {
42
47
  toApprovePlanToolDetails,
43
48
  validateApprovePlanParams,
44
49
  } from "./lib/plan-approval/validate.js";
50
+ import { validatePlanDebateGate } from "./lib/plan-debate-gate.js";
45
51
 
46
52
  // @ts-expect-error pi extensions run as ESM
47
53
  const MODULE_URL = import.meta.url;
@@ -65,18 +71,23 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
65
71
  | {
66
72
  plan_packet?: unknown;
67
73
  human_summary?: string | null;
74
+ plan_markdown?: string | null;
68
75
  }
69
76
  | undefined;
70
77
  if (!data?.plan_packet) return undefined;
78
+ const contentText =
79
+ typeof message.content === "string" ? message.content : null;
71
80
  const lines = renderHarnessPlanDraft(
72
81
  {
73
82
  plan_packet: data.plan_packet as Parameters<
74
83
  typeof renderHarnessPlanDraft
75
84
  >[0]["plan_packet"],
76
85
  human_summary: data.human_summary,
86
+ plan_markdown: data.plan_markdown,
77
87
  },
78
- 80,
88
+ 120,
79
89
  theme,
90
+ contentText,
80
91
  );
81
92
  return new Text(lines.join("\n"), 0, 0);
82
93
  },
@@ -86,7 +97,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
86
97
  name: "approve_plan",
87
98
  label: "Approve Plan",
88
99
  description:
89
- "Present a PlanPacket for user approval with a scrollable plan view. Parent /harness-plan orchestrator calls this after decomposition, hypothesis, and parallel reviews.",
100
+ "Present a PlanPacket for user approval: full plan markdown in the transcript, then Approve / Request changes / Cancel via the same prompt as ask_user. Parent /harness-plan orchestrator calls this after decomposition, hypothesis, and parallel reviews.",
90
101
  promptSnippet: PROMPT_SNIPPET,
91
102
  promptGuidelines: PROMPT_GUIDELINES,
92
103
  parameters: ApprovePlanParamsSchema,
@@ -133,11 +144,67 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
133
144
  }
134
145
 
135
146
  const planId = String(validated.plan_packet.plan_id ?? "plan");
136
- const summary =
147
+ const _summary =
137
148
  validated.human_summary?.trim() ||
138
149
  `Plan ${planId} — pending your approval`;
139
150
  const runCtx = getLatestRunContext(entries);
140
151
  const projectRoot = process.cwd();
152
+ const implWarnings: string[] = [];
153
+ if (runCtx?.run_id) {
154
+ const implPath = join(
155
+ projectRoot,
156
+ ".pi",
157
+ "harness",
158
+ "runs",
159
+ runCtx.run_id,
160
+ "artifacts",
161
+ "implementation-research.yaml",
162
+ );
163
+ let implExists = false;
164
+ try {
165
+ await access(implPath, constants.R_OK);
166
+ implExists = true;
167
+ } catch {
168
+ implExists = false;
169
+ }
170
+ const risk = String(
171
+ validated.plan_packet.risk_level ?? "med",
172
+ ).toLowerCase();
173
+ if (!implExists) {
174
+ const msg =
175
+ "approve_plan: missing artifacts/implementation-research.yaml (Phase 3.5 required)";
176
+ if (risk === "high") {
177
+ return {
178
+ content: [{ type: "text", text: msg }],
179
+ details: {
180
+ plan_packet: validated.plan_packet,
181
+ cancelled: true,
182
+ },
183
+ isError: true,
184
+ };
185
+ }
186
+ implWarnings.push(msg);
187
+ }
188
+ }
189
+ if (runCtx?.run_id) {
190
+ const gate = await validatePlanDebateGate(projectRoot, runCtx.run_id);
191
+ if (!gate.ok) {
192
+ return {
193
+ content: [
194
+ {
195
+ type: "text",
196
+ text: `approve_plan blocked — plan debate gate incomplete:\n- ${gate.errors.join("\n- ")}`,
197
+ },
198
+ ],
199
+ details: {
200
+ plan_packet: validated.plan_packet,
201
+ debate_gate: gate,
202
+ cancelled: true,
203
+ },
204
+ isError: true,
205
+ };
206
+ }
207
+ }
141
208
  const reviewPath = await writePlanReviewMarkdown(
142
209
  projectRoot,
143
210
  runCtx,
@@ -148,10 +215,11 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
148
215
  status: "draft",
149
216
  },
150
217
  );
218
+ const planMarkdown = buildPlanApprovalMarkdown(validated);
151
219
  const draftContent =
152
220
  reviewPath != null
153
- ? `${summary}\nEditor review: ${reviewPath}`
154
- : summary;
221
+ ? `${planMarkdown}\n\n---\n\nEditor copy: \`${reviewPath}\``
222
+ : planMarkdown;
155
223
  pi.sendMessage({
156
224
  customType: "harness-plan-draft",
157
225
  content: draftContent,
@@ -162,20 +230,16 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
162
230
  human_summary: validated.human_summary ?? null,
163
231
  research_brief: validated.research_brief ?? null,
164
232
  plan_review_path: reviewPath,
233
+ plan_markdown: planMarkdown,
165
234
  shown_at: new Date().toISOString(),
166
235
  },
167
236
  });
168
237
 
169
- let outcome: PlanApprovalDialogResult;
170
- if (ctx.hasUI) {
171
- outcome = await runPlanApprovalDialog(ctx.ui, validated, {
172
- onMounted: () => {
173
- pi.events.emit("plan-approval:mounted", {});
174
- },
175
- });
176
- } else {
177
- outcome = await runPlanApprovalFallback(ctx.ui, validated);
178
- }
238
+ const outcome: PlanApprovalDialogResult = await runPlanApprovalDialog(
239
+ ctx.ui,
240
+ validated,
241
+ { hasUI: ctx.hasUI },
242
+ );
179
243
 
180
244
  const details = toApprovePlanToolDetails(
181
245
  validated,
@@ -213,13 +277,15 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
213
277
  );
214
278
  }
215
279
 
216
- const text = formatApprovePlanResultText(
217
- outcome.response,
218
- outcome.cancelled,
219
- );
280
+ const text = [
281
+ formatApprovePlanResultText(outcome.response, outcome.cancelled),
282
+ ...implWarnings,
283
+ ]
284
+ .filter(Boolean)
285
+ .join("\n\n");
220
286
  return {
221
287
  content: [{ type: "text", text }],
222
- details,
288
+ details: { ...details, implementation_warnings: implWarnings },
223
289
  };
224
290
  },
225
291
 
@@ -56,6 +56,8 @@ import {
56
56
  writeYamlFile,
57
57
  } from "../lib/harness-yaml.js";
58
58
  import { claimExtensionLoad } from "./lib/extension-load-guard.js";
59
+ import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
60
+ import { isReviewRoundYamlWriteAllowed } from "./lib/plan-debate-write-guard.js";
59
61
 
60
62
  // @ts-expect-error pi extensions run as ESM
61
63
  const MODULE_URL = import.meta.url;
@@ -987,6 +989,22 @@ export default function harnessRunContext(pi: ExtensionAPI) {
987
989
  isError: true,
988
990
  };
989
991
  }
992
+ const relForGate = pathArg.replace(/\\/g, "/");
993
+ if (
994
+ isReviewRoundArtifactPath(relForGate) &&
995
+ !isReviewRoundYamlWriteAllowed()
996
+ ) {
997
+ return {
998
+ content: [
999
+ {
1000
+ type: "text",
1001
+ text: `Blocked: ${pathArg} must be written via harness_debate_submit_round after lane YAML + messenger thread are complete. Parent sessions cannot author review-round files directly.`,
1002
+ },
1003
+ ],
1004
+ details: { path: pathArg },
1005
+ isError: true,
1006
+ };
1007
+ }
990
1008
  let doc: unknown;
991
1009
  try {
992
1010
  doc = parseStructuredDocument(content, pathArg);