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
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0–P3 plan debate tools — bus + pi-messenger transport.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import type { DebateParticipant } from "../lib/debate-orchestrator-types.js";
|
|
10
|
+
import {
|
|
11
|
+
getLatestRunContext,
|
|
12
|
+
getRunIdFromSession,
|
|
13
|
+
} from "../lib/harness-run-context.js";
|
|
14
|
+
import { writeYamlFile } from "../lib/harness-yaml.js";
|
|
15
|
+
import {
|
|
16
|
+
acceptDebateRound,
|
|
17
|
+
finalizeDebateConsensus,
|
|
18
|
+
openDebateBus,
|
|
19
|
+
} from "./lib/debate-bus-core.js";
|
|
20
|
+
import { getDebateState } from "./lib/debate-bus-state.js";
|
|
21
|
+
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
22
|
+
import { captureHarnessEvent } from "./lib/harness-posthog.js";
|
|
23
|
+
import {
|
|
24
|
+
buildPlanReviewRoundEnvelope,
|
|
25
|
+
type PlanReviewRoundDraft,
|
|
26
|
+
} from "./lib/plan-debate-envelope.js";
|
|
27
|
+
import {
|
|
28
|
+
normalizePlanDebateId,
|
|
29
|
+
planDebateIdForRun,
|
|
30
|
+
} from "./lib/plan-debate-id.js";
|
|
31
|
+
import {
|
|
32
|
+
applyDebateLane,
|
|
33
|
+
type DebateLaneKind,
|
|
34
|
+
debateLaneForAgent,
|
|
35
|
+
formatApplyLaneMessage,
|
|
36
|
+
} from "./lib/plan-debate-lane.js";
|
|
37
|
+
import { getPlanDebateRoundStatus } from "./lib/plan-debate-round-status.js";
|
|
38
|
+
import { withReviewRoundYamlWrite } from "./lib/plan-debate-write-guard.js";
|
|
39
|
+
import {
|
|
40
|
+
formatTranscriptForSpawn,
|
|
41
|
+
getMessengerRoundState,
|
|
42
|
+
initPlanMessenger,
|
|
43
|
+
messengerRoundDebateReady,
|
|
44
|
+
postMessengerMessage,
|
|
45
|
+
readRoundTranscript,
|
|
46
|
+
} from "./lib/plan-messenger.js";
|
|
47
|
+
import {
|
|
48
|
+
loadValidationTurnYaml,
|
|
49
|
+
validateIntegratorDraft,
|
|
50
|
+
} from "./lib/plan-review-integrator-rules.js";
|
|
51
|
+
import { assessPlanScopeDrift } from "./lib/plan-scope-guard.js";
|
|
52
|
+
|
|
53
|
+
// @ts-expect-error pi extensions run as ESM
|
|
54
|
+
const MODULE_URL = import.meta.url;
|
|
55
|
+
|
|
56
|
+
function getRunId(ctx: {
|
|
57
|
+
sessionManager: { getEntries(): unknown[]; getSessionId(): string };
|
|
58
|
+
}): string {
|
|
59
|
+
return (
|
|
60
|
+
getRunIdFromSession(
|
|
61
|
+
ctx.sessionManager.getEntries(),
|
|
62
|
+
ctx.sessionManager.getSessionId(),
|
|
63
|
+
) ?? ctx.sessionManager.getSessionId()
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runDir(projectRoot: string, runId: string): string {
|
|
68
|
+
return join(projectRoot, ".pi", "harness", "runs", runId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function debateHooks(pi: ExtensionAPI) {
|
|
72
|
+
return {
|
|
73
|
+
appendEntry: (customType: string, data: unknown) =>
|
|
74
|
+
pi.appendEntry(customType, data),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function telemetryRound(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
props: Record<string, unknown>,
|
|
81
|
+
): void {
|
|
82
|
+
captureHarnessEvent(sessionId, "harness_debate_round", props);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function subagentResults(
|
|
86
|
+
details: unknown,
|
|
87
|
+
): Array<{ agent: string; finalOutput?: string }> {
|
|
88
|
+
const d = details as {
|
|
89
|
+
results?: Array<{ agent: string; finalOutput?: string }>;
|
|
90
|
+
};
|
|
91
|
+
return d?.results ?? [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
95
|
+
if (!claimExtensionLoad("harness-debate-tools", MODULE_URL)) return;
|
|
96
|
+
|
|
97
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
98
|
+
if (event.isError || event.toolName !== "subagent") return;
|
|
99
|
+
const runId = getRunId(ctx);
|
|
100
|
+
const projectRoot = process.cwd();
|
|
101
|
+
const rd = runDir(projectRoot, runId);
|
|
102
|
+
const entries = ctx.sessionManager.getEntries();
|
|
103
|
+
const runCtx = getLatestRunContext(entries);
|
|
104
|
+
if (!runCtx?.run_id || runCtx.run_id !== runId) return;
|
|
105
|
+
|
|
106
|
+
const applied: string[] = [];
|
|
107
|
+
let lastRound = 1;
|
|
108
|
+
for (const result of subagentResults(event.details)) {
|
|
109
|
+
const lane = debateLaneForAgent(result.agent ?? "");
|
|
110
|
+
if (!lane || !result.finalOutput?.trim()) continue;
|
|
111
|
+
const out = await applyDebateLane({
|
|
112
|
+
runDir: rd,
|
|
113
|
+
lane,
|
|
114
|
+
content: result.finalOutput,
|
|
115
|
+
});
|
|
116
|
+
if (out.round_index) lastRound = out.round_index;
|
|
117
|
+
pi.appendEntry("harness-debate-lane-applied", {
|
|
118
|
+
agent: result.agent,
|
|
119
|
+
...out,
|
|
120
|
+
});
|
|
121
|
+
applied.push(formatApplyLaneMessage(out));
|
|
122
|
+
}
|
|
123
|
+
if (applied.length === 0) return;
|
|
124
|
+
|
|
125
|
+
const status = await getPlanDebateRoundStatus(rd, lastRound);
|
|
126
|
+
pi.sendMessage({
|
|
127
|
+
customType: "harness-debate-next-step",
|
|
128
|
+
content: [
|
|
129
|
+
"**Debate lane auto-applied from subagent output**",
|
|
130
|
+
...applied,
|
|
131
|
+
"",
|
|
132
|
+
status.next_tool
|
|
133
|
+
? `**Required next tool (do not stop with prose only):** ${status.next_tool}`
|
|
134
|
+
: "Check harness_debate_round_status for this round.",
|
|
135
|
+
].join("\n"),
|
|
136
|
+
display: true,
|
|
137
|
+
details: { applied, status },
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.registerTool({
|
|
142
|
+
name: "harness_debate_open",
|
|
143
|
+
label: "Open Plan Debate",
|
|
144
|
+
description:
|
|
145
|
+
"Open plan-phase debate bus (plan-<run_id>) and initialize pi-messenger inboxes/threads. Call once before Review Gate rounds.",
|
|
146
|
+
parameters: Type.Object({
|
|
147
|
+
debate_id: Type.Optional(
|
|
148
|
+
Type.String({ description: "Optional; normalized to plan-<run_id>" }),
|
|
149
|
+
),
|
|
150
|
+
}),
|
|
151
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
152
|
+
const runId = getRunId(ctx);
|
|
153
|
+
const projectRoot = process.cwd();
|
|
154
|
+
const raw = String((params as { debate_id?: string }).debate_id ?? "");
|
|
155
|
+
const { debateId, corrected, warning } = normalizePlanDebateId(
|
|
156
|
+
raw,
|
|
157
|
+
runId,
|
|
158
|
+
);
|
|
159
|
+
const opened = await openDebateBus(runId, debateId, debateHooks(pi));
|
|
160
|
+
await initPlanMessenger(runDir(projectRoot, runId), {
|
|
161
|
+
runId,
|
|
162
|
+
debateId,
|
|
163
|
+
});
|
|
164
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
165
|
+
captureHarnessEvent(sessionId, "harness_debate_round", {
|
|
166
|
+
run_id: runId,
|
|
167
|
+
debate_id: debateId,
|
|
168
|
+
event: "open",
|
|
169
|
+
debate_phase: "plan",
|
|
170
|
+
corrected_id: corrected,
|
|
171
|
+
});
|
|
172
|
+
const lines = [
|
|
173
|
+
`Plan debate opened: ${debateId}`,
|
|
174
|
+
`Messenger: debate-messenger/ (inbox + threads/round-N/transcript.jsonl)`,
|
|
175
|
+
];
|
|
176
|
+
if (warning) lines.push(`Note: ${warning}`);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
179
|
+
details: { run_id: runId, debate_id: debateId, state: opened },
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
pi.registerTool({
|
|
185
|
+
name: "harness_messenger_post",
|
|
186
|
+
label: "Post Debate Messenger Message",
|
|
187
|
+
description:
|
|
188
|
+
"Post a claim/rebuttal/integrate message to the round thread and agent inbox (pi-messenger style). Evaluator posts claims first; adversary rebuts with in_reply_to claim ids.",
|
|
189
|
+
parameters: Type.Object({
|
|
190
|
+
round_index: Type.Number({ description: "1–4" }),
|
|
191
|
+
from: Type.String({
|
|
192
|
+
description:
|
|
193
|
+
"PlanEvaluatorAgent | PlanAdversaryAgent | ReviewIntegratorAgent | HypothesisValidatorAgent | SprintContractAuditorAgent",
|
|
194
|
+
}),
|
|
195
|
+
kind: Type.String({
|
|
196
|
+
description: "claim | rebuttal | integrate | audit | system",
|
|
197
|
+
}),
|
|
198
|
+
body: Type.String(),
|
|
199
|
+
to: Type.Optional(Type.Array(Type.String())),
|
|
200
|
+
in_reply_to: Type.Optional(Type.Array(Type.String())),
|
|
201
|
+
claim_ids: Type.Optional(Type.Array(Type.String())),
|
|
202
|
+
evidence_refs: Type.Optional(Type.Array(Type.String())),
|
|
203
|
+
artifact_path: Type.Optional(Type.String()),
|
|
204
|
+
}),
|
|
205
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
206
|
+
const runId = getRunId(ctx);
|
|
207
|
+
const p = params as {
|
|
208
|
+
round_index: number;
|
|
209
|
+
from: DebateParticipant;
|
|
210
|
+
kind: "claim" | "rebuttal" | "integrate" | "audit" | "system";
|
|
211
|
+
body: string;
|
|
212
|
+
to?: Array<DebateParticipant | "broadcast">;
|
|
213
|
+
in_reply_to?: string[];
|
|
214
|
+
claim_ids?: string[];
|
|
215
|
+
evidence_refs?: string[];
|
|
216
|
+
artifact_path?: string;
|
|
217
|
+
};
|
|
218
|
+
const msg = await postMessengerMessage(runDir(process.cwd(), runId), {
|
|
219
|
+
from: p.from,
|
|
220
|
+
kind: p.kind,
|
|
221
|
+
round_index: p.round_index,
|
|
222
|
+
to: p.to ?? ["broadcast"],
|
|
223
|
+
body: p.body,
|
|
224
|
+
in_reply_to: p.in_reply_to ?? [],
|
|
225
|
+
claim_ids: p.claim_ids ?? [],
|
|
226
|
+
evidence_refs: p.evidence_refs ?? [],
|
|
227
|
+
artifact_path: p.artifact_path,
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: `Posted ${msg.kind} from ${msg.from} (round ${msg.round_index}, id ${msg.id})`,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
details: { message: msg },
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
pi.registerTool({
|
|
242
|
+
name: "harness_messenger_read_round",
|
|
243
|
+
label: "Read Debate Round Transcript",
|
|
244
|
+
description:
|
|
245
|
+
"Return formatted messenger transcript for spawning adversary or integrator with full thread context.",
|
|
246
|
+
parameters: Type.Object({
|
|
247
|
+
round_index: Type.Number(),
|
|
248
|
+
}),
|
|
249
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
250
|
+
const runId = getRunId(ctx);
|
|
251
|
+
const roundIndex = Number(
|
|
252
|
+
(params as { round_index: number }).round_index,
|
|
253
|
+
);
|
|
254
|
+
const messages = await readRoundTranscript(
|
|
255
|
+
runDir(process.cwd(), runId),
|
|
256
|
+
roundIndex,
|
|
257
|
+
);
|
|
258
|
+
const text = formatTranscriptForSpawn(messages);
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: "text", text }],
|
|
261
|
+
details: { round_index: roundIndex, message_count: messages.length },
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
pi.registerTool({
|
|
267
|
+
name: "harness_debate_submit_round",
|
|
268
|
+
label: "Submit Plan Review Round",
|
|
269
|
+
description:
|
|
270
|
+
"Validate lane YAML + messenger thread, write review-round-rN.yaml, emit bus round envelope. Parent must not write review-round files directly.",
|
|
271
|
+
parameters: Type.Object({
|
|
272
|
+
round_index: Type.Number({ description: "1–4" }),
|
|
273
|
+
integrator_draft: Type.Record(Type.String(), Type.Unknown(), {
|
|
274
|
+
description: "ReviewIntegrator YAML object (review-round-rN fields)",
|
|
275
|
+
}),
|
|
276
|
+
}),
|
|
277
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
278
|
+
const runId = getRunId(ctx);
|
|
279
|
+
const projectRoot = process.cwd();
|
|
280
|
+
const roundIndex = Number(
|
|
281
|
+
(params as { round_index: number }).round_index,
|
|
282
|
+
);
|
|
283
|
+
const draft = (params as { integrator_draft: Record<string, unknown> })
|
|
284
|
+
.integrator_draft as unknown as PlanReviewRoundDraft;
|
|
285
|
+
draft.round_index = roundIndex;
|
|
286
|
+
if (!draft.schema_version) draft.schema_version = "1.0.0";
|
|
287
|
+
const debateId = planDebateIdForRun(runId);
|
|
288
|
+
const rd = runDir(projectRoot, runId);
|
|
289
|
+
const integratorBody =
|
|
290
|
+
(typeof draft.round_summary === "string" && draft.round_summary) ||
|
|
291
|
+
"Review integrator synthesis for this round.";
|
|
292
|
+
await postMessengerMessage(rd, {
|
|
293
|
+
from: "ReviewIntegratorAgent",
|
|
294
|
+
kind: "integrate",
|
|
295
|
+
round_index: roundIndex,
|
|
296
|
+
to: ["broadcast"],
|
|
297
|
+
body: integratorBody,
|
|
298
|
+
in_reply_to: [],
|
|
299
|
+
claim_ids: [],
|
|
300
|
+
evidence_refs: [`artifacts/review-round-r${roundIndex}.yaml`],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const roundState = await getMessengerRoundState(rd, roundIndex);
|
|
304
|
+
const mCheck = messengerRoundDebateReady(roundState, roundIndex === 4);
|
|
305
|
+
if (!mCheck.ok) {
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `Messenger gate failed:\n- ${mCheck.errors.join("\n- ")}`,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
details: { errors: mCheck.errors },
|
|
314
|
+
isError: true,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const validationTurn = await loadValidationTurnYaml(rd, roundIndex);
|
|
319
|
+
const integratorValidation = validateIntegratorDraft(
|
|
320
|
+
draft as unknown as Record<string, unknown>,
|
|
321
|
+
{ validationTurn },
|
|
322
|
+
);
|
|
323
|
+
if (!integratorValidation.ok) {
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: `Integrator rules failed:\n- ${integratorValidation.errors.join("\n- ")}`,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
details: { errors: integratorValidation.errors },
|
|
332
|
+
isError: true,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
draft.review_gate_ready = integratorValidation.review_gate_ready;
|
|
336
|
+
|
|
337
|
+
const relPath = `artifacts/review-round-r${roundIndex}.yaml`;
|
|
338
|
+
const absPath = join(rd, relPath);
|
|
339
|
+
await withReviewRoundYamlWrite(async () => {
|
|
340
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
341
|
+
await writeYamlFile(absPath, draft);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const envelope = buildPlanReviewRoundEnvelope(draft, {
|
|
345
|
+
runId,
|
|
346
|
+
debateId,
|
|
347
|
+
});
|
|
348
|
+
const busState = getDebateState();
|
|
349
|
+
if (!busState || busState.debate_id !== debateId) {
|
|
350
|
+
await openDebateBus(runId, debateId, debateHooks(pi));
|
|
351
|
+
}
|
|
352
|
+
const result = await acceptDebateRound(envelope, debateHooks(pi));
|
|
353
|
+
if (!result.ok) {
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `Bus round rejected: ${result.reason ?? "unknown"}`,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
details: { envelope },
|
|
362
|
+
isError: true,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
367
|
+
telemetryRound(sessionId, {
|
|
368
|
+
run_id: runId,
|
|
369
|
+
debate_id: debateId,
|
|
370
|
+
round_index: roundIndex,
|
|
371
|
+
review_gate_ready: draft.review_gate_ready,
|
|
372
|
+
messenger_messages: roundState?.claim_count,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
content: [
|
|
377
|
+
{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: `Round ${roundIndex} submitted to ${debateId} (review_gate_ready=${draft.review_gate_ready})`,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
details: {
|
|
383
|
+
path: relPath,
|
|
384
|
+
envelope,
|
|
385
|
+
review_gate_ready: draft.review_gate_ready,
|
|
386
|
+
warnings: integratorValidation.warnings,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
pi.registerTool({
|
|
393
|
+
name: "harness_debate_consensus",
|
|
394
|
+
label: "Finalize Plan Debate Consensus",
|
|
395
|
+
description:
|
|
396
|
+
"After 4 bus rounds, emit consensus packet to .pi/harness/debates/plan-<run_id>.consensus.json",
|
|
397
|
+
parameters: Type.Object({
|
|
398
|
+
rationale: Type.Optional(Type.String()),
|
|
399
|
+
}),
|
|
400
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
401
|
+
const runId = getRunId(ctx);
|
|
402
|
+
const rationale =
|
|
403
|
+
String((params as { rationale?: string }).rationale ?? "").trim() ||
|
|
404
|
+
"Plan Review Gate consensus after 4 messenger-backed rounds.";
|
|
405
|
+
const decision = await finalizeDebateConsensus(
|
|
406
|
+
rationale,
|
|
407
|
+
debateHooks(pi),
|
|
408
|
+
);
|
|
409
|
+
const debateId = planDebateIdForRun(runId);
|
|
410
|
+
captureHarnessEvent(
|
|
411
|
+
ctx.sessionManager.getSessionId(),
|
|
412
|
+
"harness_debate_consensus",
|
|
413
|
+
{
|
|
414
|
+
run_id: runId,
|
|
415
|
+
debate_id: debateId,
|
|
416
|
+
policy_decision: decision,
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
return {
|
|
420
|
+
content: [
|
|
421
|
+
{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: `Consensus: ${decision ?? "unknown"} (${debateId})`,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
details: { policy_decision: decision, debate_id: debateId },
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
pi.registerTool({
|
|
432
|
+
name: "harness_debate_apply_lane",
|
|
433
|
+
label: "Apply Debate Lane YAML + Messenger",
|
|
434
|
+
description:
|
|
435
|
+
"Parse subagent lane output, write artifacts/*-rN.yaml, and post evaluator claims / adversary rebuttals to messenger. Prefer letting subagent tool_result auto-apply; use this if auto-apply missed fenced YAML.",
|
|
436
|
+
parameters: Type.Object({
|
|
437
|
+
lane: Type.String({
|
|
438
|
+
description:
|
|
439
|
+
"hypothesis-validation | validation-turn | adversary-brief | sprint-audit",
|
|
440
|
+
}),
|
|
441
|
+
content: Type.String({ description: "Fenced YAML/JSON from subagent" }),
|
|
442
|
+
round_index: Type.Optional(Type.Number()),
|
|
443
|
+
}),
|
|
444
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
445
|
+
const runId = getRunId(ctx);
|
|
446
|
+
const p = params as {
|
|
447
|
+
lane: DebateLaneKind;
|
|
448
|
+
content: string;
|
|
449
|
+
round_index?: number;
|
|
450
|
+
};
|
|
451
|
+
const result = await applyDebateLane({
|
|
452
|
+
runDir: runDir(process.cwd(), runId),
|
|
453
|
+
lane: p.lane,
|
|
454
|
+
content: p.content,
|
|
455
|
+
roundIndex: p.round_index,
|
|
456
|
+
});
|
|
457
|
+
return {
|
|
458
|
+
content: [{ type: "text", text: formatApplyLaneMessage(result) }],
|
|
459
|
+
details: result,
|
|
460
|
+
isError: !result.ok,
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
pi.registerTool({
|
|
466
|
+
name: "harness_debate_round_status",
|
|
467
|
+
label: "Plan Debate Round Status",
|
|
468
|
+
description:
|
|
469
|
+
"List missing lane artifacts and messenger steps for a Review Gate round. Call when resuming after a stop.",
|
|
470
|
+
parameters: Type.Object({
|
|
471
|
+
round_index: Type.Number({ description: "1–4" }),
|
|
472
|
+
}),
|
|
473
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
474
|
+
const runId = getRunId(ctx);
|
|
475
|
+
const roundIndex = Number(
|
|
476
|
+
(params as { round_index: number }).round_index,
|
|
477
|
+
);
|
|
478
|
+
const status = await getPlanDebateRoundStatus(
|
|
479
|
+
runDir(process.cwd(), runId),
|
|
480
|
+
roundIndex,
|
|
481
|
+
);
|
|
482
|
+
const lines = [
|
|
483
|
+
`Round ${roundIndex}: ready_for_integrator=${status.ready_for_integrator}`,
|
|
484
|
+
status.missing.length
|
|
485
|
+
? `Missing:\n- ${status.missing.join("\n- ")}`
|
|
486
|
+
: "Lane + messenger prerequisites satisfied.",
|
|
487
|
+
status.next_tool ? `Next: ${status.next_tool}` : "",
|
|
488
|
+
].filter(Boolean);
|
|
489
|
+
return {
|
|
490
|
+
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
491
|
+
details: status,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
pi.registerTool({
|
|
497
|
+
name: "harness_plan_scope_check",
|
|
498
|
+
label: "Plan Scope Drift Check",
|
|
499
|
+
description:
|
|
500
|
+
"P2 guard: compare task_summary with decomposition text; returns material_drift when plan narrows to infra-only work.",
|
|
501
|
+
parameters: Type.Object({
|
|
502
|
+
task_summary: Type.String(),
|
|
503
|
+
decomposition_text: Type.String(),
|
|
504
|
+
}),
|
|
505
|
+
async execute(_id, params) {
|
|
506
|
+
const p = params as { task_summary: string; decomposition_text: string };
|
|
507
|
+
const result = assessPlanScopeDrift(p.task_summary, p.decomposition_text);
|
|
508
|
+
return {
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: "text",
|
|
512
|
+
text: `${result.summary}\nmaterial_drift=${result.material_drift} overlap=${result.overlap_score.toFixed(3)}`,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
details: result,
|
|
516
|
+
};
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
executeCreatePlan,
|
|
21
21
|
formatCreatePlanResultText,
|
|
22
22
|
} from "./lib/plan-approval/create-plan.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
buildPlanApprovalMarkdown,
|
|
25
|
+
runPlanApprovalDialog,
|
|
26
|
+
} from "./lib/plan-approval/dialog.js";
|
|
25
27
|
import { writePlanReviewMarkdown } from "./lib/plan-approval/plan-review.js";
|
|
26
28
|
import {
|
|
27
29
|
renderApprovePlanCall,
|
|
@@ -42,6 +44,7 @@ import {
|
|
|
42
44
|
toApprovePlanToolDetails,
|
|
43
45
|
validateApprovePlanParams,
|
|
44
46
|
} from "./lib/plan-approval/validate.js";
|
|
47
|
+
import { validatePlanDebateGate } from "./lib/plan-debate-gate.js";
|
|
45
48
|
|
|
46
49
|
// @ts-expect-error pi extensions run as ESM
|
|
47
50
|
const MODULE_URL = import.meta.url;
|
|
@@ -65,18 +68,23 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
65
68
|
| {
|
|
66
69
|
plan_packet?: unknown;
|
|
67
70
|
human_summary?: string | null;
|
|
71
|
+
plan_markdown?: string | null;
|
|
68
72
|
}
|
|
69
73
|
| undefined;
|
|
70
74
|
if (!data?.plan_packet) return undefined;
|
|
75
|
+
const contentText =
|
|
76
|
+
typeof message.content === "string" ? message.content : null;
|
|
71
77
|
const lines = renderHarnessPlanDraft(
|
|
72
78
|
{
|
|
73
79
|
plan_packet: data.plan_packet as Parameters<
|
|
74
80
|
typeof renderHarnessPlanDraft
|
|
75
81
|
>[0]["plan_packet"],
|
|
76
82
|
human_summary: data.human_summary,
|
|
83
|
+
plan_markdown: data.plan_markdown,
|
|
77
84
|
},
|
|
78
|
-
|
|
85
|
+
120,
|
|
79
86
|
theme,
|
|
87
|
+
contentText,
|
|
80
88
|
);
|
|
81
89
|
return new Text(lines.join("\n"), 0, 0);
|
|
82
90
|
},
|
|
@@ -86,7 +94,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
86
94
|
name: "approve_plan",
|
|
87
95
|
label: "Approve Plan",
|
|
88
96
|
description:
|
|
89
|
-
"Present a PlanPacket for user approval
|
|
97
|
+
"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
98
|
promptSnippet: PROMPT_SNIPPET,
|
|
91
99
|
promptGuidelines: PROMPT_GUIDELINES,
|
|
92
100
|
parameters: ApprovePlanParamsSchema,
|
|
@@ -133,11 +141,30 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
const planId = String(validated.plan_packet.plan_id ?? "plan");
|
|
136
|
-
const
|
|
144
|
+
const _summary =
|
|
137
145
|
validated.human_summary?.trim() ||
|
|
138
146
|
`Plan ${planId} — pending your approval`;
|
|
139
147
|
const runCtx = getLatestRunContext(entries);
|
|
140
148
|
const projectRoot = process.cwd();
|
|
149
|
+
if (runCtx?.run_id) {
|
|
150
|
+
const gate = await validatePlanDebateGate(projectRoot, runCtx.run_id);
|
|
151
|
+
if (!gate.ok) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `approve_plan blocked — plan debate gate incomplete:\n- ${gate.errors.join("\n- ")}`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
details: {
|
|
160
|
+
plan_packet: validated.plan_packet,
|
|
161
|
+
debate_gate: gate,
|
|
162
|
+
cancelled: true,
|
|
163
|
+
},
|
|
164
|
+
isError: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
141
168
|
const reviewPath = await writePlanReviewMarkdown(
|
|
142
169
|
projectRoot,
|
|
143
170
|
runCtx,
|
|
@@ -148,10 +175,11 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
148
175
|
status: "draft",
|
|
149
176
|
},
|
|
150
177
|
);
|
|
178
|
+
const planMarkdown = buildPlanApprovalMarkdown(validated);
|
|
151
179
|
const draftContent =
|
|
152
180
|
reviewPath != null
|
|
153
|
-
? `${
|
|
154
|
-
:
|
|
181
|
+
? `${planMarkdown}\n\n---\n\nEditor copy: \`${reviewPath}\``
|
|
182
|
+
: planMarkdown;
|
|
155
183
|
pi.sendMessage({
|
|
156
184
|
customType: "harness-plan-draft",
|
|
157
185
|
content: draftContent,
|
|
@@ -162,20 +190,16 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
162
190
|
human_summary: validated.human_summary ?? null,
|
|
163
191
|
research_brief: validated.research_brief ?? null,
|
|
164
192
|
plan_review_path: reviewPath,
|
|
193
|
+
plan_markdown: planMarkdown,
|
|
165
194
|
shown_at: new Date().toISOString(),
|
|
166
195
|
},
|
|
167
196
|
});
|
|
168
197
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
} else {
|
|
177
|
-
outcome = await runPlanApprovalFallback(ctx.ui, validated);
|
|
178
|
-
}
|
|
198
|
+
const outcome: PlanApprovalDialogResult = await runPlanApprovalDialog(
|
|
199
|
+
ctx.ui,
|
|
200
|
+
validated,
|
|
201
|
+
{ hasUI: ctx.hasUI },
|
|
202
|
+
);
|
|
179
203
|
|
|
180
204
|
const details = toApprovePlanToolDetails(
|
|
181
205
|
validated,
|
|
@@ -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);
|