ultimate-pi 0.13.0 → 0.14.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/{.pi → .agents}/skills/ccc/SKILL.md +1 -7
- package/.agents/skills/ccc/references/settings.md +126 -0
- package/.agents/skills/harness-debate-plan/SKILL.md +61 -21
- package/.agents/skills/harness-orchestration/SKILL.md +1 -1
- package/.pi/agents/harness/planning/plan-adversary.md +2 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +3 -1
- package/.pi/agents/harness/planning/review-integrator.md +4 -2
- package/.pi/extensions/debate-orchestrator.ts +39 -435
- package/.pi/extensions/harness-debate-tools.ts +519 -0
- package/.pi/extensions/harness-plan-approval.ts +41 -17
- package/.pi/extensions/harness-run-context.ts +18 -0
- package/.pi/extensions/lib/debate-bus-core.ts +434 -0
- package/.pi/extensions/lib/debate-bus-state.ts +58 -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 +6 -6
- package/.pi/extensions/lib/plan-approval/render.ts +6 -0
- package/.pi/extensions/lib/plan-approval/validate.ts +1 -1
- package/.pi/extensions/lib/plan-debate-envelope.ts +2 -0
- package/.pi/extensions/lib/plan-debate-gate.ts +155 -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-round-status.ts +94 -0
- package/.pi/extensions/lib/plan-debate-write-guard.ts +20 -0
- package/.pi/extensions/lib/plan-messenger.ts +276 -0
- package/.pi/extensions/lib/plan-review-integrator-rules.ts +119 -0
- package/.pi/extensions/lib/plan-scope-guard.ts +89 -0
- package/.pi/harness/agents.manifest.json +7 -7
- package/.pi/prompts/harness-plan.md +22 -12
- package/CHANGELOG.md +18 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/package.json +3 -3
- package/.agents/skills/ck-search/SKILL.md +0 -23
- package/.agents/skills/cocoindex-search/SKILL.md +0 -35
- package/.agents/skills/obsidian-bases/SKILL.md +0 -299
- package/.agents/skills/obsidian-markdown/SKILL.md +0 -237
- package/.pi/extensions/lib/plan-approval/fallback.ts +0 -50
- /package/{.pi → .agents}/skills/ccc/references/management.md +0 -0
|
@@ -1,291 +1,52 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
+
/** Full plan body shown in the transcript before the approval prompt. */
|
|
17
|
+
export function buildPlanApprovalMarkdown(
|
|
38
18
|
validated: ValidatedApprovePlanParams,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 {
|
|
9
|
+
import { formatPlanPacketYaml } from "./format-plan.js";
|
|
10
10
|
import type { PlanResearchBrief } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export {
|
|
@@ -217,7 +217,7 @@ export function formatPlanPacketMarkdown(
|
|
|
217
217
|
`- **risk_level:** ${typeof packet.risk_level === "string" ? packet.risk_level : "med"}`,
|
|
218
218
|
);
|
|
219
219
|
if (opts?.plan_packet_path) {
|
|
220
|
-
lines.push(`- **canonical
|
|
220
|
+
lines.push(`- **canonical YAML:** \`${opts.plan_packet_path}\``);
|
|
221
221
|
}
|
|
222
222
|
lines.push("");
|
|
223
223
|
if (opts?.human_summary?.trim()) {
|
|
@@ -233,15 +233,15 @@ export function formatPlanPacketMarkdown(
|
|
|
233
233
|
}
|
|
234
234
|
lines.push("## Plan packet");
|
|
235
235
|
lines.push("");
|
|
236
|
-
lines.push("```
|
|
237
|
-
for (const line of
|
|
236
|
+
lines.push("```yaml");
|
|
237
|
+
for (const line of formatPlanPacketYaml(packet).split("\n")) {
|
|
238
238
|
lines.push(line);
|
|
239
239
|
}
|
|
240
240
|
lines.push("```");
|
|
241
241
|
lines.push("");
|
|
242
242
|
if (status === "draft") {
|
|
243
243
|
lines.push(
|
|
244
|
-
"Review this
|
|
244
|
+
"Review this plan, then choose **Approve**, **Request changes**, or **Cancel** in the prompt below (same flow as `ask_user`).",
|
|
245
245
|
);
|
|
246
246
|
} else if (status === "approved") {
|
|
247
247
|
lines.push(
|
|
@@ -388,6 +388,6 @@ export function formatPlanReviewUserHint(reviewPath: string | null): string {
|
|
|
388
388
|
const abs = resolve(reviewPath);
|
|
389
389
|
return (
|
|
390
390
|
`Full plan for editor review: ${abs}\n` +
|
|
391
|
-
`Open this markdown file in
|
|
391
|
+
`Open this markdown file in your editor if you prefer; approval options appear in the harness prompt below.`
|
|
392
392
|
);
|
|
393
393
|
}
|
|
@@ -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) {
|
|
@@ -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 ?? "
|
|
39
|
+
displayMode: params.displayMode ?? "inline",
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -51,6 +51,7 @@ export function buildPlanReviewRoundEnvelope(
|
|
|
51
51
|
token_usage: { per_agent: Record<string, number>; round_total: number };
|
|
52
52
|
consensus_delta: number;
|
|
53
53
|
severity_scores?: PlanReviewRoundDraft["severity_scores"];
|
|
54
|
+
review_gate_ready?: boolean;
|
|
54
55
|
};
|
|
55
56
|
} {
|
|
56
57
|
const participants = (draft.participants ?? [
|
|
@@ -79,6 +80,7 @@ export function buildPlanReviewRoundEnvelope(
|
|
|
79
80
|
},
|
|
80
81
|
consensus_delta: draft.consensus_delta ?? 0,
|
|
81
82
|
severity_scores: draft.severity_scores,
|
|
83
|
+
review_gate_ready: draft.review_gate_ready,
|
|
82
84
|
},
|
|
83
85
|
};
|
|
84
86
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0 — plan debate artifact + bus gates before approve_plan.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access, readFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { planDebateIdForRun } from "./plan-debate-id.js";
|
|
9
|
+
import {
|
|
10
|
+
getMessengerRoundState,
|
|
11
|
+
loadMessengerState,
|
|
12
|
+
messengerRoundDebateReady,
|
|
13
|
+
} from "./plan-messenger.js";
|
|
14
|
+
|
|
15
|
+
const PLAN_ROUNDS = 4;
|
|
16
|
+
const FOCUS_BY_ROUND = ["spec", "wbs", "schedule", "quality"] as const;
|
|
17
|
+
|
|
18
|
+
function laneFilesForRound(roundIndex: number): string[] {
|
|
19
|
+
const n = roundIndex;
|
|
20
|
+
const lanes = [
|
|
21
|
+
`artifacts/validation-turn-r${n}.yaml`,
|
|
22
|
+
`artifacts/adversary-brief-r${n}.yaml`,
|
|
23
|
+
];
|
|
24
|
+
if (n === 1) {
|
|
25
|
+
lanes.unshift(`artifacts/hypothesis-validation-r${n}.yaml`);
|
|
26
|
+
}
|
|
27
|
+
if (n === 4) {
|
|
28
|
+
lanes.push(`artifacts/sprint-audit-r${n}.yaml`);
|
|
29
|
+
}
|
|
30
|
+
lanes.push(`artifacts/review-round-r${n}.yaml`);
|
|
31
|
+
return lanes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
await access(path, constants.R_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function countJsonlKinds(
|
|
44
|
+
debateJsonlPath: string,
|
|
45
|
+
): Promise<{ rounds: number; hasConsensus: boolean }> {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readFile(debateJsonlPath, "utf-8");
|
|
48
|
+
let rounds = 0;
|
|
49
|
+
let hasConsensus = false;
|
|
50
|
+
for (const line of raw.split("\n")) {
|
|
51
|
+
if (!line.trim()) continue;
|
|
52
|
+
const ev = JSON.parse(line) as { kind?: string };
|
|
53
|
+
if (ev.kind === "round") rounds += 1;
|
|
54
|
+
if (ev.kind === "consensus") hasConsensus = true;
|
|
55
|
+
}
|
|
56
|
+
return { rounds, hasConsensus };
|
|
57
|
+
} catch {
|
|
58
|
+
return { rounds: 0, hasConsensus: false };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlanDebateGateResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
errors: string[];
|
|
65
|
+
warnings: string[];
|
|
66
|
+
debateId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function validatePlanDebateGate(
|
|
70
|
+
projectRoot: string,
|
|
71
|
+
runId: string,
|
|
72
|
+
): Promise<PlanDebateGateResult> {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
const warnings: string[] = [];
|
|
75
|
+
const debateId = planDebateIdForRun(runId);
|
|
76
|
+
const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
|
|
77
|
+
const debatesDir = join(projectRoot, ".pi", "harness", "debates");
|
|
78
|
+
|
|
79
|
+
for (let r = 1; r <= PLAN_ROUNDS; r++) {
|
|
80
|
+
for (const rel of laneFilesForRound(r)) {
|
|
81
|
+
const abs = join(runDir, rel);
|
|
82
|
+
if (!(await fileExists(abs))) {
|
|
83
|
+
errors.push(`missing ${rel}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const roundState = await getMessengerRoundState(runDir, r);
|
|
87
|
+
const messengerCheck = messengerRoundDebateReady(roundState, r === 4);
|
|
88
|
+
if (!messengerCheck.ok) {
|
|
89
|
+
for (const e of messengerCheck.errors) {
|
|
90
|
+
errors.push(`round ${r} messenger: ${e}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const messenger = await loadMessengerState(runDir);
|
|
96
|
+
if (!messenger) {
|
|
97
|
+
errors.push(
|
|
98
|
+
"debate-messenger/state.json missing — call harness_debate_open",
|
|
99
|
+
);
|
|
100
|
+
} else if (messenger.debate_id !== debateId) {
|
|
101
|
+
errors.push(`messenger debate_id ${messenger.debate_id} !== ${debateId}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const jsonlPath = join(debatesDir, `${debateId}.jsonl`);
|
|
105
|
+
const { rounds, hasConsensus } = await countJsonlKinds(jsonlPath);
|
|
106
|
+
if (rounds < PLAN_ROUNDS) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`${debateId}.jsonl has ${rounds}/${PLAN_ROUNDS} round events — use harness_debate_submit_round each round`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!hasConsensus) {
|
|
112
|
+
errors.push(
|
|
113
|
+
`missing consensus on ${debateId} — call harness_debate_consensus`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const consensusPath = join(debatesDir, `${debateId}.consensus.json`);
|
|
118
|
+
if (!(await fileExists(consensusPath))) {
|
|
119
|
+
errors.push(`missing ${debateId}.consensus.json`);
|
|
120
|
+
} else {
|
|
121
|
+
try {
|
|
122
|
+
const raw = await readFile(consensusPath, "utf-8");
|
|
123
|
+
const packet = JSON.parse(raw) as { policy_decision?: string };
|
|
124
|
+
if (packet.policy_decision === "block") {
|
|
125
|
+
errors.push("consensus policy_decision is block — cannot approve");
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
errors.push("invalid consensus json");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let r = 0; r < FOCUS_BY_ROUND.length; r++) {
|
|
133
|
+
const focus = FOCUS_BY_ROUND[r];
|
|
134
|
+
const reviewPath = join(runDir, `artifacts/review-round-r${r + 1}.yaml`);
|
|
135
|
+
if (await fileExists(reviewPath)) {
|
|
136
|
+
const raw = await readFile(reviewPath, "utf-8");
|
|
137
|
+
if (!raw.includes(focus)) {
|
|
138
|
+
warnings.push(`review-round-r${r + 1} may not match focus ${focus}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
ok: errors.length === 0,
|
|
145
|
+
errors,
|
|
146
|
+
warnings,
|
|
147
|
+
debateId,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isReviewRoundArtifactPath(relPath: string): boolean {
|
|
152
|
+
return /^artifacts\/review-round-r\d+\.yaml$/i.test(
|
|
153
|
+
relPath.replace(/\\/g, "/"),
|
|
154
|
+
);
|
|
155
|
+
}
|