ultimate-pi 0.10.1 → 0.11.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 (50) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +3 -3
  2. package/.agents/skills/harness-orchestration/SKILL.md +19 -11
  3. package/.agents/skills/harness-plan/SKILL.md +15 -9
  4. package/.pi/agents/harness/planner.md +6 -47
  5. package/.pi/agents/harness/planning/decompose.md +84 -0
  6. package/.pi/agents/harness/planning/hypothesis-eval.md +59 -0
  7. package/.pi/agents/harness/planning/hypothesis.md +90 -0
  8. package/.pi/agents/harness/planning/plan-adversary.md +50 -0
  9. package/.pi/agents/harness/planning/planner.md +20 -0
  10. package/.pi/agents/harness/planning/scout-graphify.md +48 -0
  11. package/.pi/agents/harness/planning/scout-semantic.md +42 -0
  12. package/.pi/agents/harness/planning/scout-structure.md +44 -0
  13. package/.pi/extensions/harness-ask-user.ts +5 -0
  14. package/.pi/extensions/harness-plan-approval.ts +137 -3
  15. package/.pi/extensions/harness-run-context.ts +1 -1
  16. package/.pi/extensions/harness-subagents.ts +8 -3
  17. package/.pi/extensions/harness-web-tools.ts +2 -0
  18. package/.pi/extensions/lib/extension-load-guard.ts +39 -0
  19. package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +33 -5
  20. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +2 -175
  21. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +18 -0
  22. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +1 -5
  23. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -18
  24. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +1 -35
  25. package/.pi/extensions/lib/plan-approval/create-plan.ts +5 -0
  26. package/.pi/extensions/lib/plan-approval/plan-review.ts +393 -0
  27. package/.pi/extensions/lib/plan-approval/schema.ts +16 -1
  28. package/.pi/extensions/lib/plan-approval/types.ts +10 -0
  29. package/.pi/extensions/lib/plan-approval/validate.ts +2 -0
  30. package/.pi/extensions/policy-gate.ts +1 -1
  31. package/.pi/extensions/ultimate-pi-vcc.ts +5 -0
  32. package/.pi/harness/agents.manifest.json +114 -82
  33. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +3 -3
  34. package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +34 -0
  35. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +41 -0
  36. package/.pi/harness/docs/adrs/README.md +2 -0
  37. package/.pi/harness/specs/README.md +1 -1
  38. package/.pi/harness/specs/harness-spawn-context.schema.json +2 -1
  39. package/.pi/harness/specs/plan-adversary-brief.schema.json +45 -0
  40. package/.pi/harness/specs/plan-decomposition-brief.schema.json +108 -0
  41. package/.pi/harness/specs/plan-hypothesis-brief.schema.json +96 -0
  42. package/.pi/harness/specs/plan-hypothesis-eval.schema.json +61 -0
  43. package/.pi/lib/harness-run-context.ts +12 -0
  44. package/.pi/prompts/harness-auto.md +1 -1
  45. package/.pi/prompts/harness-plan.md +111 -28
  46. package/.pi/prompts/harness-setup.md +1 -1
  47. package/.pi/scripts/harness-resolve-up-pkg.mjs +13 -0
  48. package/CHANGELOG.md +12 -0
  49. package/biome.json +4 -1
  50. package/package.json +2 -2
@@ -17,10 +17,7 @@ import {
17
17
  } from "@earendil-works/pi-coding-agent";
18
18
  import { Text } from "@earendil-works/pi-tui";
19
19
  import { Type } from "@sinclair/typebox";
20
- import {
21
- getLatestRunContext,
22
- syncPlannerApprovalsToParent,
23
- } from "../../../../lib/harness-run-context.js";
20
+ import { getLatestRunContext } from "../../../../lib/harness-run-context.js";
24
21
  import { getDriftReport } from "../agent-manifest.js";
25
22
  import { Blackboard } from "../blackboard.js";
26
23
  import {
@@ -1503,24 +1500,6 @@ Guidelines:
1503
1500
 
1504
1501
  clearInterval(spinnerInterval);
1505
1502
 
1506
- if (
1507
- subagentType === "harness/planner" &&
1508
- record.session &&
1509
- record.status !== "running" &&
1510
- record.status !== "queued"
1511
- ) {
1512
- const parentEntries = ctx.sessionManager.getEntries();
1513
- const runCtx = getLatestRunContext(parentEntries);
1514
- if (runCtx) {
1515
- syncPlannerApprovalsToParent(
1516
- (type, data) => pi.appendEntry(type, data),
1517
- parentEntries,
1518
- record.session.sessionManager.getEntries(),
1519
- runCtx,
1520
- );
1521
- }
1522
- }
1523
-
1524
1503
  // Clean up foreground agent from widget
1525
1504
  if (fgId) {
1526
1505
  agentActivity.delete(fgId);
@@ -1633,19 +1612,6 @@ Guidelines:
1633
1612
  cancelNudge(params.agent_id);
1634
1613
  }
1635
1614
 
1636
- if (record.session && record.status !== "running") {
1637
- const parentEntries = _ctx.sessionManager.getEntries();
1638
- const runCtx = getLatestRunContext(parentEntries);
1639
- if (runCtx) {
1640
- syncPlannerApprovalsToParent(
1641
- (type, data) => pi.appendEntry(type, data),
1642
- parentEntries,
1643
- record.session.sessionManager.getEntries(),
1644
- runCtx,
1645
- );
1646
- }
1647
- }
1648
-
1649
1615
  // Verbose: include full conversation
1650
1616
  if (params.verbose && record.session) {
1651
1617
  const conversation = getAgentConversation(record.session);
@@ -9,6 +9,7 @@ import {
9
9
  saveRunContextToDisk,
10
10
  validatePlanPacket,
11
11
  } from "../../../lib/harness-run-context.js";
12
+ import { writePlanReviewMarkdown } from "./plan-review.js";
12
13
 
13
14
  export const CREATE_PLAN_SNIPPET =
14
15
  "create_plan({ plan_packet: { ...approved PlanPacket } })";
@@ -116,6 +117,10 @@ export async function executeCreatePlan(
116
117
  /* disk mirror best-effort */
117
118
  }
118
119
 
120
+ await writePlanReviewMarkdown(deps.projectRoot, updated, planPacket, {
121
+ status: "committed",
122
+ });
123
+
119
124
  deps.onCommitted(updated, planPacket, planPath);
120
125
 
121
126
  return {
@@ -0,0 +1,393 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import {
4
+ canonicalPlanPath,
5
+ canonicalPlanReviewPath,
6
+ type HarnessRunContext,
7
+ type PlanPacketLike,
8
+ } from "../../../lib/harness-run-context.js";
9
+ import { formatPlanPacketLines } from "./format-plan.js";
10
+ import type { PlanResearchBrief } from "./types.js";
11
+
12
+ export {
13
+ canonicalPlanReviewPath,
14
+ PLAN_REVIEW_BASENAME,
15
+ } from "../../../lib/harness-run-context.js";
16
+
17
+ export type PlanReviewStatus = "draft" | "approved" | "committed";
18
+
19
+ function asRecord(value: unknown): Record<string, unknown> | null {
20
+ return value && typeof value === "object" && !Array.isArray(value)
21
+ ? (value as Record<string, unknown>)
22
+ : null;
23
+ }
24
+
25
+ function str(value: unknown): string | null {
26
+ return typeof value === "string" && value.trim() ? value.trim() : null;
27
+ }
28
+
29
+ function strList(value: unknown): string[] {
30
+ if (!Array.isArray(value)) return [];
31
+ return value
32
+ .map((item) => (typeof item === "string" ? item.trim() : null))
33
+ .filter((item): item is string => Boolean(item));
34
+ }
35
+
36
+ /** Render Darwin research sections for plan-review.md. */
37
+ export function formatResearchBriefMarkdown(
38
+ research: PlanResearchBrief | null | undefined,
39
+ ): string {
40
+ if (!research) return "";
41
+ const lines: string[] = [];
42
+ const decomp = asRecord(research.decomposition);
43
+ const hyp = asRecord(research.hypothesis);
44
+ const evalBrief = asRecord(research.eval);
45
+
46
+ if (decomp) {
47
+ lines.push("## Phase 1 — Problem decomposition");
48
+ lines.push("");
49
+ const restate = str(decomp.problem_restatement);
50
+ if (restate) {
51
+ lines.push("**What is being asked?**");
52
+ lines.push("");
53
+ lines.push(restate);
54
+ lines.push("");
55
+ }
56
+ const types = strList(decomp.problem_types);
57
+ if (types.length) {
58
+ lines.push(`**Problem type(s):** ${types.join(", ")}`);
59
+ lines.push("");
60
+ }
61
+ const scope = asRecord(decomp.scope);
62
+ if (scope) {
63
+ const focus = str(scope.narrowed_focus);
64
+ if (focus) {
65
+ lines.push("**Scope:**");
66
+ lines.push("");
67
+ lines.push(focus);
68
+ lines.push("");
69
+ }
70
+ const excluded = strList(scope.excluded);
71
+ if (excluded.length) {
72
+ lines.push("**Excluded:**");
73
+ for (const item of excluded) lines.push(`- ${item}`);
74
+ lines.push("");
75
+ }
76
+ }
77
+ for (const [label, key] of [
78
+ ["Hard constraints", "hard_constraints"],
79
+ ["Soft constraints", "soft_constraints"],
80
+ ["Success metrics", "success_metrics"],
81
+ ] as const) {
82
+ const items = strList(decomp[key]);
83
+ if (items.length) {
84
+ lines.push(`**${label}:**`);
85
+ for (const item of items) lines.push(`- ${item}`);
86
+ lines.push("");
87
+ }
88
+ }
89
+ const prior = asRecord(decomp.prior_art);
90
+ if (prior) {
91
+ lines.push("**Prior art:**");
92
+ lines.push("");
93
+ const best = str(prior.best_approach);
94
+ const gap = str(prior.gap);
95
+ if (best) lines.push(`- Best approach: ${best}`);
96
+ if (gap) lines.push(`- Gap: ${gap}`);
97
+ for (const dead of strList(prior.dead_ends)) {
98
+ lines.push(`- Dead end: ${dead}`);
99
+ }
100
+ lines.push("");
101
+ }
102
+ const core = str(decomp.core_tension);
103
+ if (core) {
104
+ lines.push("**Core tension:**");
105
+ lines.push("");
106
+ lines.push(core);
107
+ lines.push("");
108
+ }
109
+ }
110
+
111
+ if (hyp) {
112
+ lines.push("## Phase 2 — DARWIN hypothesis");
113
+ lines.push("");
114
+ const primary = asRecord(hyp.primary);
115
+ if (primary) {
116
+ for (const [label, key] of [
117
+ ["Claim", "claim"],
118
+ ["Mechanism", "mechanism"],
119
+ ["Prediction", "prediction"],
120
+ ["Experiment", "experiment"],
121
+ ["Resolves tension", "tension_resolution"],
122
+ ] as const) {
123
+ const text = str(primary[key]);
124
+ if (text) {
125
+ lines.push(`**${label}:** ${text}`);
126
+ lines.push("");
127
+ }
128
+ }
129
+ }
130
+ const fork = asRecord(hyp.dialectical_fork);
131
+ if (fork) {
132
+ const forkText = str(fork.fork);
133
+ if (forkText) {
134
+ lines.push(`**Dialectical fork:** ${forkText}`);
135
+ lines.push("");
136
+ }
137
+ const pathA = str(fork.path_a);
138
+ const pathB = str(fork.path_b);
139
+ if (pathA) lines.push(`- **Path A:** ${pathA}`);
140
+ if (pathB) lines.push(`- **Path B:** ${pathB}`);
141
+ lines.push("");
142
+ }
143
+ const alts = Array.isArray(hyp.alternatives) ? hyp.alternatives : [];
144
+ if (alts.length) {
145
+ lines.push("**Alternatives:**");
146
+ for (const alt of alts) {
147
+ const rec = asRecord(alt);
148
+ if (!rec) continue;
149
+ const claim = str(rec.claim);
150
+ const bet = str(rec.key_bet);
151
+ if (claim) lines.push(`- ${claim}${bet ? ` (bet: ${bet})` : ""}`);
152
+ }
153
+ lines.push("");
154
+ }
155
+ const steps = strList(hyp.recommended_next_steps);
156
+ if (steps.length) {
157
+ lines.push("**Recommended next steps:**");
158
+ for (const step of steps) lines.push(`1. ${step}`);
159
+ lines.push("");
160
+ }
161
+ }
162
+
163
+ if (evalBrief) {
164
+ lines.push("## Self-evaluation");
165
+ lines.push("");
166
+ lines.push("| Dimension | Score | Rationale |");
167
+ lines.push("|-----------|-------|-----------|");
168
+ const dims = asRecord(evalBrief.dimensions);
169
+ if (dims) {
170
+ for (const name of [
171
+ "novelty",
172
+ "coherence",
173
+ "testability",
174
+ "impact",
175
+ ] as const) {
176
+ const dim = asRecord(dims[name]);
177
+ if (!dim) continue;
178
+ const score = typeof dim.score === "number" ? String(dim.score) : "?";
179
+ const rationale = str(dim.rationale) ?? "";
180
+ lines.push(`| ${name} | ${score}/100 | ${rationale} |`);
181
+ }
182
+ }
183
+ const rel = asRecord(evalBrief.relevance);
184
+ if (rel) {
185
+ const passes = rel.passes === true ? "✓" : "✗";
186
+ const rationale = str(rel.rationale) ?? "";
187
+ lines.push(`| Relevance | ${passes} | ${rationale} |`);
188
+ }
189
+ lines.push("");
190
+ const summary = str(evalBrief.human_summary);
191
+ if (summary) {
192
+ lines.push(summary);
193
+ lines.push("");
194
+ }
195
+ }
196
+
197
+ return lines.length ? `${lines.join("\n")}\n` : "";
198
+ }
199
+
200
+ export function formatPlanPacketMarkdown(
201
+ packet: PlanPacketLike,
202
+ opts?: {
203
+ human_summary?: string | null;
204
+ status?: PlanReviewStatus;
205
+ plan_packet_path?: string | null;
206
+ research_brief?: PlanResearchBrief | null;
207
+ },
208
+ ): string {
209
+ const lines: string[] = [];
210
+ const status = opts?.status ?? "draft";
211
+ lines.push("# Harness plan");
212
+ lines.push("");
213
+ lines.push(`- **Status:** ${status}`);
214
+ lines.push(`- **plan_id:** ${packet.plan_id ?? "?"}`);
215
+ lines.push(`- **task_id:** ${packet.task_id ?? "?"}`);
216
+ lines.push(
217
+ `- **risk_level:** ${typeof packet.risk_level === "string" ? packet.risk_level : "med"}`,
218
+ );
219
+ if (opts?.plan_packet_path) {
220
+ lines.push(`- **canonical JSON:** \`${opts.plan_packet_path}\``);
221
+ }
222
+ lines.push("");
223
+ if (opts?.human_summary?.trim()) {
224
+ lines.push("## Summary");
225
+ lines.push("");
226
+ lines.push(opts.human_summary.trim());
227
+ lines.push("");
228
+ }
229
+ const researchMd = formatResearchBriefMarkdown(opts?.research_brief);
230
+ if (researchMd) {
231
+ lines.push(researchMd.trimEnd());
232
+ lines.push("");
233
+ }
234
+ lines.push("## Plan packet");
235
+ lines.push("");
236
+ lines.push("```text");
237
+ for (const line of formatPlanPacketLines(packet, 100)) {
238
+ lines.push(line);
239
+ }
240
+ lines.push("```");
241
+ lines.push("");
242
+ if (status === "draft") {
243
+ lines.push(
244
+ "Review this file in your editor, then return to the harness TUI to **Approve**, **Request changes**, or **Cancel**.",
245
+ );
246
+ } else if (status === "approved") {
247
+ lines.push(
248
+ "Approved in the harness TUI. Waiting for `create_plan` to write `plan-packet.json`, or run `/harness-plan-commit` if that step failed.",
249
+ );
250
+ } else {
251
+ lines.push(
252
+ "Plan committed. Next: `/harness-run` to execute (do not pass `--plan` on the happy path).",
253
+ );
254
+ }
255
+ lines.push("");
256
+ return `${lines.join("\n")}\n`;
257
+ }
258
+
259
+ export async function writePlanReviewMarkdown(
260
+ projectRoot: string,
261
+ runCtx: HarnessRunContext | null,
262
+ packet: PlanPacketLike,
263
+ opts?: {
264
+ human_summary?: string | null;
265
+ status?: PlanReviewStatus;
266
+ research_brief?: PlanResearchBrief | null;
267
+ },
268
+ ): Promise<string | null> {
269
+ const runId = runCtx?.run_id;
270
+ if (!runId) return null;
271
+ const reviewPath = canonicalPlanReviewPath(runId, projectRoot);
272
+ const planPacketPath =
273
+ runCtx.plan_packet_path ?? canonicalPlanPath(runId, projectRoot);
274
+ const body = formatPlanPacketMarkdown(packet, {
275
+ human_summary: opts?.human_summary,
276
+ status: opts?.status ?? "draft",
277
+ plan_packet_path: planPacketPath,
278
+ research_brief: opts?.research_brief,
279
+ });
280
+ try {
281
+ await mkdir(dirname(reviewPath), { recursive: true });
282
+ await writeFile(reviewPath, body, "utf-8");
283
+ return reviewPath;
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+
289
+ interface SessionEntryLike {
290
+ type?: string;
291
+ customType?: string;
292
+ data?: unknown;
293
+ message?: {
294
+ role?: string;
295
+ toolName?: string;
296
+ details?: unknown;
297
+ content?: { type?: string; text?: string }[];
298
+ };
299
+ }
300
+
301
+ /** Latest plan_packet from drafts, approve_plan tool results, or assistant JSON blocks. */
302
+ export function extractLatestPlanPacketFromEntries(
303
+ entries: unknown[],
304
+ ): { packet: PlanPacketLike; human_summary?: string | null } | null {
305
+ let found: { packet: PlanPacketLike; human_summary?: string | null } | null =
306
+ null;
307
+
308
+ const consider = (
309
+ packet: PlanPacketLike | undefined,
310
+ human_summary?: string | null,
311
+ ) => {
312
+ if (!packet || typeof packet !== "object") return;
313
+ if (!packet.plan_id && !packet.scope) return;
314
+ found = { packet, human_summary: human_summary ?? null };
315
+ };
316
+
317
+ for (let i = entries.length - 1; i >= 0; i--) {
318
+ const entry = entries[i] as SessionEntryLike;
319
+ if (entry.type === "custom" && entry.customType === "harness-plan-draft") {
320
+ const data = entry.data as {
321
+ plan_packet?: PlanPacketLike;
322
+ human_summary?: string | null;
323
+ };
324
+ consider(data.plan_packet, data.human_summary);
325
+ if (found) return found;
326
+ }
327
+ if (entry.type === "message" && entry.message?.role === "toolResult") {
328
+ const toolName = entry.message.toolName;
329
+ const details = entry.message.details as
330
+ | {
331
+ plan_packet?: PlanPacketLike;
332
+ human_summary?: string;
333
+ }
334
+ | undefined;
335
+ if (toolName === "approve_plan" && details?.plan_packet) {
336
+ consider(details.plan_packet, details.human_summary);
337
+ if (found) return found;
338
+ }
339
+ }
340
+ }
341
+
342
+ for (let i = entries.length - 1; i >= 0; i--) {
343
+ const entry = entries[i] as SessionEntryLike;
344
+ if (entry.type !== "message" || entry.message?.role !== "assistant") {
345
+ continue;
346
+ }
347
+ const blocks = entry.message.content ?? [];
348
+ for (const block of blocks) {
349
+ if (block.type !== "text" || !block.text) continue;
350
+ const match = block.text.match(/```json\s*([\s\S]*?)```/i);
351
+ if (!match) continue;
352
+ try {
353
+ const parsed = JSON.parse(match[1]) as {
354
+ plan_packet?: PlanPacketLike;
355
+ human_summary?: string;
356
+ };
357
+ if (parsed.plan_packet) {
358
+ consider(parsed.plan_packet, parsed.human_summary);
359
+ if (found) return found;
360
+ }
361
+ } catch {
362
+ /* ignore malformed assistant JSON */
363
+ }
364
+ }
365
+ }
366
+
367
+ return found;
368
+ }
369
+
370
+ export async function syncPlannerPlanReviewToDisk(
371
+ projectRoot: string,
372
+ runCtx: HarnessRunContext | null,
373
+ entries: unknown[],
374
+ _opts?: { agentStatus?: string },
375
+ ): Promise<string | null> {
376
+ const draft = extractLatestPlanPacketFromEntries(entries);
377
+ if (!draft) return null;
378
+ return writePlanReviewMarkdown(projectRoot, runCtx, draft.packet, {
379
+ human_summary: draft.human_summary,
380
+ status: "draft",
381
+ });
382
+ }
383
+
384
+ export function formatPlanReviewUserHint(reviewPath: string | null): string {
385
+ if (!reviewPath) {
386
+ return "No plan draft was captured yet. If the planner is still clarifying, answer in the subagent or re-run /harness-plan.";
387
+ }
388
+ const abs = resolve(reviewPath);
389
+ return (
390
+ `Full plan for editor review: ${abs}\n` +
391
+ `Open this markdown file in VS Code (or your editor), read the scope and acceptance checks, then return to the harness TUI to Approve / Request changes / Cancel.`
392
+ );
393
+ }
@@ -13,6 +13,21 @@ export const ApprovePlanParamsSchema = Type.Object({
13
13
  description: "Short summary shown above the plan body.",
14
14
  }),
15
15
  ),
16
+ research_brief: Type.Optional(
17
+ Type.Object(
18
+ {
19
+ decomposition: Type.Optional(
20
+ Type.Union([Type.Object({}), Type.Null()]),
21
+ ),
22
+ hypothesis: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
23
+ eval: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
24
+ },
25
+ {
26
+ description:
27
+ "Optional Darwin research: decomposition, hypothesis, eval (plan-review.md only).",
28
+ },
29
+ ),
30
+ ),
16
31
  options: Type.Optional(
17
32
  Type.Array(
18
33
  Type.Union([
@@ -30,7 +45,7 @@ export const ApprovePlanParamsSchema = Type.Object({
30
45
  });
31
46
 
32
47
  export const PROMPT_SNIPPET =
33
- "approve_plan({ plan_packet: { ...PlanPacket fields... }, human_summary?: string })";
48
+ "approve_plan({ plan_packet: { ...PlanPacket fields... }, human_summary?: string, research_brief?: { decomposition, hypothesis, eval } })";
34
49
 
35
50
  export const PROMPT_GUIDELINES = [
36
51
  "Call approve_plan once with the complete plan_packet when ready for user approval.",
@@ -7,9 +7,17 @@ export const DEFAULT_PLAN_APPROVAL_OPTIONS = [
7
7
  "Cancel",
8
8
  ] as const;
9
9
 
10
+ /** Optional Darwin research artifacts from /harness-plan (not persisted in plan-packet.json). */
11
+ export interface PlanResearchBrief {
12
+ decomposition?: Record<string, unknown> | null;
13
+ hypothesis?: Record<string, unknown> | null;
14
+ eval?: Record<string, unknown> | null;
15
+ }
16
+
10
17
  export interface ApprovePlanParams {
11
18
  plan_packet: PlanPacketLike;
12
19
  human_summary?: string;
20
+ research_brief?: PlanResearchBrief | null;
13
21
  options?: Array<string | { title: string; description?: string }>;
14
22
  displayMode?: "overlay" | "inline";
15
23
  }
@@ -17,6 +25,7 @@ export interface ApprovePlanParams {
17
25
  export interface ValidatedApprovePlanParams {
18
26
  plan_packet: PlanPacketLike;
19
27
  human_summary?: string;
28
+ research_brief?: PlanResearchBrief | null;
20
29
  options: { title: string; description?: string }[];
21
30
  displayMode: "overlay" | "inline";
22
31
  }
@@ -24,6 +33,7 @@ export interface ValidatedApprovePlanParams {
24
33
  export interface ApprovePlanToolDetails {
25
34
  plan_packet: PlanPacketLike;
26
35
  human_summary?: string;
36
+ research_brief?: PlanResearchBrief | null;
27
37
  options: string[];
28
38
  response: AskResponse | null;
29
39
  cancelled: boolean;
@@ -34,6 +34,7 @@ export function validateApprovePlanParams(
34
34
  return {
35
35
  plan_packet: packet as PlanPacketLike,
36
36
  human_summary: params.human_summary?.trim() || undefined,
37
+ research_brief: params.research_brief ?? undefined,
37
38
  options,
38
39
  displayMode: params.displayMode ?? "overlay",
39
40
  };
@@ -47,6 +48,7 @@ export function toApprovePlanToolDetails(
47
48
  return {
48
49
  plan_packet: validated.plan_packet,
49
50
  human_summary: validated.human_summary,
51
+ research_brief: validated.research_brief ?? null,
50
52
  options: validated.options.map((o) => o.title),
51
53
  response,
52
54
  cancelled,
@@ -243,7 +243,7 @@ export default function policyGate(pi: ExtensionAPI) {
243
243
 
244
244
  const planPhaseHint =
245
245
  state.phase === "plan"
246
- ? "\nPlan phase: present the full PlanPacket in chat, call ask_user (Approve / Request changes / Cancel), then write only the canonical plan-packet.json after Approve."
246
+ ? "\nPlan phase: scouts decompose → hypothesis via harness/planning/*; parent builds PlanPacket, ask_user on fork, parallel plan-adversary + hypothesis-eval, approve_plan (optional research_brief), then create_plan (never write plan-packet.json directly)."
247
247
  : "";
248
248
 
249
249
  return {
@@ -11,7 +11,12 @@
11
11
 
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import registerVcc from "../../vendor/pi-vcc/index.js";
14
+ import { claimExtensionLoad } from "./lib/extension-load-guard.js";
15
+
16
+ // @ts-expect-error pi extensions run as ESM
17
+ const MODULE_URL = import.meta.url;
14
18
 
15
19
  export default function ultimatePiVcc(pi: ExtensionAPI): void {
20
+ if (!claimExtensionLoad("ultimate-pi-vcc", MODULE_URL)) return;
16
21
  registerVcc(pi);
17
22
  }