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.
- package/.agents/skills/harness-debate-plan/SKILL.md +42 -22
- package/.agents/skills/harness-orchestration/SKILL.md +3 -3
- package/.agents/skills/harness-plan/SKILL.md +10 -8
- package/.pi/agents/harness/planning/decompose.md +4 -2
- package/.pi/agents/harness/planning/execution-plan-author.md +25 -14
- package/.pi/agents/harness/planning/hypothesis-validator.md +21 -5
- package/.pi/agents/harness/planning/implementation-researcher.md +42 -0
- package/.pi/agents/harness/planning/plan-adversary.md +20 -4
- package/.pi/agents/harness/planning/plan-evaluator.md +28 -5
- package/.pi/agents/harness/planning/review-integrator.md +25 -9
- package/.pi/agents/harness/planning/scout-graphify.md +1 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +19 -4
- package/.pi/agents/harness/planning/stack-researcher.md +19 -10
- package/.pi/extensions/debate-orchestrator.ts +39 -435
- package/.pi/extensions/harness-debate-tools.ts +741 -0
- package/.pi/extensions/harness-live-widget.ts +39 -159
- package/.pi/extensions/harness-plan-approval.ts +88 -22
- package/.pi/extensions/harness-run-context.ts +18 -0
- package/.pi/extensions/lib/debate-bus-core.ts +488 -0
- package/.pi/extensions/lib/debate-bus-state.ts +64 -0
- package/.pi/extensions/lib/harness-spawn-budget.ts +5 -25
- package/.pi/extensions/lib/plan-approval/dialog.ts +33 -272
- package/.pi/extensions/lib/plan-approval/format-plan.ts +12 -85
- package/.pi/extensions/lib/plan-approval/plan-review.ts +62 -6
- package/.pi/extensions/lib/plan-approval/render.ts +6 -0
- package/.pi/extensions/lib/plan-approval/types.ts +1 -0
- package/.pi/extensions/lib/plan-approval/validate.ts +1 -1
- package/.pi/extensions/lib/plan-debate-eligibility.ts +214 -0
- package/.pi/extensions/lib/plan-debate-envelope.ts +2 -0
- package/.pi/extensions/lib/plan-debate-focus.ts +151 -0
- package/.pi/extensions/lib/plan-debate-gate.ts +198 -0
- package/.pi/extensions/lib/plan-debate-id.ts +39 -0
- package/.pi/extensions/lib/plan-debate-lane.ts +220 -0
- package/.pi/extensions/lib/plan-debate-lanes.ts +44 -0
- package/.pi/extensions/lib/plan-debate-round-status.ts +137 -0
- package/.pi/extensions/lib/plan-debate-write-guard.ts +20 -0
- package/.pi/extensions/lib/plan-messenger.ts +352 -0
- package/.pi/extensions/lib/plan-review-integrator-rules.ts +119 -0
- package/.pi/extensions/lib/plan-scope-guard.ts +89 -0
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/harness/README.md +1 -1
- package/.pi/harness/agents.manifest.json +16 -12
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +1 -3
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +13 -5
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +51 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r1.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r2.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r2.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r3.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +29 -0
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +97 -16
- package/.pi/harness/specs/plan-implementation-research-brief.schema.json +128 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/round-result.schema.json +15 -2
- package/.pi/lib/harness-ui-state.ts +92 -0
- package/.pi/prompts/harness-plan.md +90 -30
- package/.pi/prompts/planning-rubrics.md +31 -0
- package/CHANGELOG.md +23 -0
- package/package.json +3 -3
- 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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
? `${
|
|
154
|
-
:
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 =
|
|
217
|
-
outcome.response,
|
|
218
|
-
|
|
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);
|