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
@@ -4,14 +4,25 @@
4
4
 
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { Text } from "@earendil-works/pi-tui";
7
+ import { Type } from "@sinclair/typebox";
8
+ import type { PlanPacketLike } from "../lib/harness-run-context.js";
7
9
  import {
8
10
  appendPlanApprovalIfNew,
9
11
  getLatestRunContext,
10
12
  hasPlanUserApproval,
11
13
  parsePlanApprovalFromMessage,
14
+ planPacketSummary,
12
15
  } from "../lib/harness-run-context.js";
16
+ import { claimExtensionLoad } from "./lib/extension-load-guard.js";
17
+ import {
18
+ CREATE_PLAN_GUIDELINES,
19
+ CREATE_PLAN_SNIPPET,
20
+ executeCreatePlan,
21
+ formatCreatePlanResultText,
22
+ } from "./lib/plan-approval/create-plan.js";
13
23
  import { runPlanApprovalDialog } from "./lib/plan-approval/dialog.js";
14
24
  import { runPlanApprovalFallback } from "./lib/plan-approval/fallback.js";
25
+ import { writePlanReviewMarkdown } from "./lib/plan-approval/plan-review.js";
15
26
  import {
16
27
  renderApprovePlanCall,
17
28
  renderApprovePlanResult,
@@ -32,7 +43,21 @@ import {
32
43
  validateApprovePlanParams,
33
44
  } from "./lib/plan-approval/validate.js";
34
45
 
46
+ // @ts-expect-error pi extensions run as ESM
47
+ const MODULE_URL = import.meta.url;
48
+
49
+ const CreatePlanParamsSchema = Type.Object({
50
+ plan_packet: Type.Object(
51
+ {},
52
+ {
53
+ description:
54
+ "Approved PlanPacket to persist (same object as approve_plan).",
55
+ },
56
+ ),
57
+ });
58
+
35
59
  export default function harnessPlanApproval(pi: ExtensionAPI) {
60
+ if (!claimExtensionLoad("harness-plan-approval", MODULE_URL)) return;
36
61
  pi.registerMessageRenderer(
37
62
  "harness-plan-draft",
38
63
  (message, _options, theme) => {
@@ -61,7 +86,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
61
86
  name: "approve_plan",
62
87
  label: "Approve Plan",
63
88
  description:
64
- "Present a PlanPacket for user approval with a scrollable plan view. Planners should prefer the subagent bridge; this registers the tool on parent sessions for non-interactive fallback.",
89
+ "Present a PlanPacket for user approval with a scrollable plan view. Parent /harness-plan orchestrator calls this after decomposition, hypothesis, and parallel reviews.",
65
90
  promptSnippet: PROMPT_SNIPPET,
66
91
  promptGuidelines: PROMPT_GUIDELINES,
67
92
  parameters: ApprovePlanParamsSchema,
@@ -92,7 +117,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
92
117
  content: [
93
118
  {
94
119
  type: "text",
95
- text: `Plan ${planId} already approved in this harness run (planner subagent). Proceed with /harness-run.`,
120
+ text: `Plan ${planId} already approved in this harness run. Proceed with /harness-run.`,
96
121
  },
97
122
  ],
98
123
  details: {
@@ -111,14 +136,32 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
111
136
  const summary =
112
137
  validated.human_summary?.trim() ||
113
138
  `Plan ${planId} — pending your approval`;
139
+ const runCtx = getLatestRunContext(entries);
140
+ const projectRoot = process.cwd();
141
+ const reviewPath = await writePlanReviewMarkdown(
142
+ projectRoot,
143
+ runCtx,
144
+ validated.plan_packet,
145
+ {
146
+ human_summary: validated.human_summary,
147
+ research_brief: validated.research_brief,
148
+ status: "draft",
149
+ },
150
+ );
151
+ const draftContent =
152
+ reviewPath != null
153
+ ? `${summary}\nEditor review: ${reviewPath}`
154
+ : summary;
114
155
  pi.sendMessage({
115
156
  customType: "harness-plan-draft",
116
- content: summary,
157
+ content: draftContent,
117
158
  display: true,
118
159
  details: {
119
160
  schema_version: "1.0.0",
120
161
  plan_packet: validated.plan_packet,
121
162
  human_summary: validated.human_summary ?? null,
163
+ research_brief: validated.research_brief ?? null,
164
+ plan_review_path: reviewPath,
122
165
  shown_at: new Date().toISOString(),
123
166
  },
124
167
  });
@@ -153,6 +196,23 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
153
196
  );
154
197
  }
155
198
 
199
+ const approved =
200
+ !outcome.cancelled &&
201
+ outcome.response?.kind === "selection" &&
202
+ /^approve/i.test(outcome.response.selections[0] ?? "");
203
+ if (approved && runCtx) {
204
+ await writePlanReviewMarkdown(
205
+ projectRoot,
206
+ runCtx,
207
+ validated.plan_packet,
208
+ {
209
+ human_summary: validated.human_summary,
210
+ research_brief: validated.research_brief,
211
+ status: "approved",
212
+ },
213
+ );
214
+ }
215
+
156
216
  const text = formatApprovePlanResultText(
157
217
  outcome.response,
158
218
  outcome.cancelled,
@@ -171,4 +231,78 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
171
231
  return renderApprovePlanResult(result, options, theme);
172
232
  },
173
233
  });
234
+
235
+ pi.registerTool({
236
+ name: "create_plan",
237
+ label: "Create Plan",
238
+ description:
239
+ "Write the approved PlanPacket to plan-packet.json for this harness run. Call only after approve_plan (Approve). Do not use write/edit.",
240
+ promptSnippet: CREATE_PLAN_SNIPPET,
241
+ promptGuidelines: CREATE_PLAN_GUIDELINES,
242
+ parameters: CreatePlanParamsSchema,
243
+
244
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
245
+ const validated = validateApprovePlanParams(params as ApprovePlanParams);
246
+ if (typeof validated === "string") {
247
+ return {
248
+ content: [{ type: "text", text: validated }],
249
+ details: { error: validated },
250
+ isError: true,
251
+ };
252
+ }
253
+
254
+ const entries = ctx.sessionManager.getEntries();
255
+ const runCtx = getLatestRunContext(entries);
256
+ const projectRoot = process.cwd();
257
+ const result = await executeCreatePlan(validated.plan_packet, {
258
+ projectRoot,
259
+ getParentEntries: () => entries,
260
+ getSubagentEntries: () => entries,
261
+ getParentRunContext: () => runCtx,
262
+ onCommitted: (updated, packet, planPath) => {
263
+ pi.appendEntry("harness-run-context", updated);
264
+ pi.appendEntry(
265
+ "harness-plan-packet",
266
+ planPacketSummary(packet, planPath, "ready"),
267
+ );
268
+ },
269
+ });
270
+
271
+ const text = formatCreatePlanResultText(result);
272
+ return {
273
+ content: [{ type: "text", text }],
274
+ details: result.ok
275
+ ? { plan_path: result.planPath, plan_id: result.planId }
276
+ : { error: result.error },
277
+ isError: !result.ok,
278
+ };
279
+ },
280
+
281
+ renderCall(args, theme) {
282
+ const packet = (args as { plan_packet?: PlanPacketLike }).plan_packet;
283
+ const id = packet?.plan_id ?? "?";
284
+ return new Text(theme.fg("accent", `create_plan: ${id}`), 0, 0);
285
+ },
286
+
287
+ renderResult(result, _options, theme) {
288
+ const details = result.details as
289
+ | { plan_path?: string; error?: string }
290
+ | undefined;
291
+ if (details?.error) {
292
+ return new Text(
293
+ theme.fg("error", details.error ?? "create_plan failed"),
294
+ 0,
295
+ 0,
296
+ );
297
+ }
298
+ return new Text(
299
+ theme.fg(
300
+ "success",
301
+ `Wrote ${details?.plan_path ?? "plan-packet.json"}`,
302
+ ),
303
+ 0,
304
+ 0,
305
+ );
306
+ },
307
+ });
174
308
  }
@@ -816,7 +816,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
816
816
  })
817
817
  ) {
818
818
  const msg =
819
- "Plan commit blocked: no user approval recorded. Approve via ask_user in the planner subagent first.";
819
+ "Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
820
820
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
821
821
  return;
822
822
  }
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * harness-subagents — package-resolved agents, blackboard, observation-bus handoffs.
3
3
  */
4
+
5
+ import { claimExtensionLoad } from "./lib/extension-load-guard.js";
4
6
  import { getHarnessPackageRoot } from "./lib/harness-paths.js";
5
7
  import { createHarnessSubagentsExtension } from "./lib/harness-subagents/vendored/index.js";
6
8
 
7
- export default createHarnessSubagentsExtension(
8
- getHarnessPackageRoot(import.meta.url),
9
- );
9
+ // @ts-expect-error pi extensions run as ESM
10
+ const MODULE_URL = import.meta.url;
11
+
12
+ export default claimExtensionLoad("harness-subagents", MODULE_URL)
13
+ ? createHarnessSubagentsExtension(getHarnessPackageRoot(MODULE_URL))
14
+ : () => {};
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { Type } from "@sinclair/typebox";
7
+ import { claimExtensionLoad } from "./lib/extension-load-guard.js";
7
8
  import {
8
9
  harnessWebContextLine,
9
10
  readTextExcerpt,
@@ -97,6 +98,7 @@ function sessionCwd(ctx: { cwd?: string }): string {
97
98
  }
98
99
 
99
100
  export default function harnessWebTools(pi: ExtensionAPI) {
101
+ if (!claimExtensionLoad("harness-web-tools", MODULE_URL)) return;
100
102
  pi.on("before_agent_start", async (event) => {
101
103
  return {
102
104
  systemPrompt: `${event.systemPrompt}\n\n${harnessWebContextLine()}`,
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const LOAD_GUARD_KEY = Symbol.for("ultimate-pi.extension-load-guard");
6
+
7
+ type LoadGuardRegistry = Set<string>;
8
+
9
+ function getRegistry(): LoadGuardRegistry {
10
+ const state = globalThis as typeof globalThis & {
11
+ [LOAD_GUARD_KEY]?: LoadGuardRegistry;
12
+ };
13
+ if (!state[LOAD_GUARD_KEY]) {
14
+ state[LOAD_GUARD_KEY] = new Set<string>();
15
+ }
16
+ return state[LOAD_GUARD_KEY];
17
+ }
18
+
19
+ function isSourceRepo(): boolean {
20
+ try {
21
+ const pkg = JSON.parse(
22
+ readFileSync(join(process.cwd(), "package.json"), "utf8"),
23
+ ) as { name?: string };
24
+ return pkg.name === "ultimate-pi";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export function claimExtensionLoad(key: string, moduleUrl: string): boolean {
31
+ const registry = getRegistry();
32
+ const modulePath = fileURLToPath(moduleUrl);
33
+ if (modulePath.includes("/node_modules/ultimate-pi/") && isSourceRepo()) {
34
+ return false;
35
+ }
36
+ if (registry.has(key)) return false;
37
+ registry.add(key);
38
+ return true;
39
+ }
@@ -20,6 +20,15 @@ export type HarnessAgentKind =
20
20
 
21
21
  const MUTATING_TOOLS = new Set(["write", "edit"]);
22
22
 
23
+ const PLANNING_BASH_DENY_PATTERNS = [
24
+ /\bgraphify\s+update\b/i,
25
+ /\bgraphify\s+extract\b/i,
26
+ /\bgraphify\s+install\b/i,
27
+ /\bpip\s+install\b/i,
28
+ /\buv\s+tool\s+install\b/i,
29
+ /\bnpm\s+install\b/i,
30
+ ];
31
+
23
32
  const BASH_MUTATION_PATTERNS = [
24
33
  /\brm\s+-/i,
25
34
  /\bmv\s+/i,
@@ -45,8 +54,16 @@ const READ_ONLY_KINDS = new Set<HarnessAgentKind>([
45
54
  "meta",
46
55
  ]);
47
56
 
57
+ export function isHarnessPlanningAgent(agentType: string): boolean {
58
+ const id = agentType.replace(/^harness\//, "");
59
+ return id === "planner" || id.startsWith("planning/");
60
+ }
61
+
48
62
  export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
49
63
  const id = agentType.replace(/^harness\//, "");
64
+ if (id.startsWith("planning/") || id === "planner") {
65
+ return "planner";
66
+ }
50
67
  switch (id) {
51
68
  case "planner":
52
69
  return "planner";
@@ -96,13 +113,10 @@ export function evaluateHarnessSubagentToolCall(
96
113
  return { action: "allow" };
97
114
  }
98
115
 
99
- if (toolName === "create_plan") {
100
- if (kind === "planner") {
101
- return { action: "allow" };
102
- }
116
+ if (toolName === "create_plan" || toolName === "approve_plan") {
103
117
  return {
104
118
  action: "block",
105
- reason: `harness-subagent-policy: create_plan is only for harness/planner.`,
119
+ reason: `harness-subagent-policy: ${toolName} is parent-orchestrator only (not available in subagents).`,
106
120
  };
107
121
  }
108
122
 
@@ -121,6 +135,17 @@ export function evaluateHarnessSubagentToolCall(
121
135
  reason: `harness-subagent-policy: mutating bash blocked for harness/${kind}.`,
122
136
  };
123
137
  }
138
+ if (
139
+ command &&
140
+ isHarnessPlanningAgent(agentType) &&
141
+ PLANNING_BASH_DENY_PATTERNS.some((p) => p.test(command))
142
+ ) {
143
+ return {
144
+ action: "block",
145
+ reason:
146
+ "harness-subagent-policy: planning scouts may use read-only graphify/sg/ck commands only.",
147
+ };
148
+ }
124
149
  }
125
150
 
126
151
  return { action: "allow" };
@@ -128,6 +153,9 @@ export function evaluateHarnessSubagentToolCall(
128
153
 
129
154
  /** Policy phase hint seeded into subagent system prompt appendix when extensions load policy-gate. */
130
155
  export function harnessSubagentPhaseHint(agentType: string): string | null {
156
+ if (isHarnessPlanningAgent(agentType)) {
157
+ return "plan";
158
+ }
131
159
  const kind = classifyHarnessAgent(agentType);
132
160
  switch (kind) {
133
161
  case "planner":
@@ -1,13 +1,11 @@
1
1
  /**
2
- * Registers ask_user and approve_plan in subagent sessions, delegating UI to the parent harness session.
2
+ * Registers ask_user in subagent sessions, delegating UI to the parent harness session.
3
3
  */
4
4
 
5
5
  import type {
6
6
  ExtensionAPI,
7
7
  ExtensionContext,
8
8
  } from "@earendil-works/pi-coding-agent";
9
- import { Text } from "@earendil-works/pi-tui";
10
- import { Type } from "@sinclair/typebox";
11
9
  import type {
12
10
  PlanPacketLike,
13
11
  PlanUserApproval,
@@ -27,32 +25,8 @@ import {
27
25
  toToolDetails,
28
26
  validateAskParams,
29
27
  } from "../ask-user/validate.js";
30
- import {
31
- CREATE_PLAN_GUIDELINES,
32
- CREATE_PLAN_SNIPPET,
33
- executeCreatePlan,
34
- formatCreatePlanResultText,
35
- } from "../plan-approval/create-plan.js";
36
- import { runPlanApprovalDialog } from "../plan-approval/dialog.js";
37
- import { runPlanApprovalFallback } from "../plan-approval/fallback.js";
38
- import {
39
- renderApprovePlanCall,
40
- renderApprovePlanResult,
41
- } from "../plan-approval/render.js";
42
- import {
43
- ApprovePlanParamsSchema,
44
- PROMPT_GUIDELINES as PLAN_PROMPT_GUIDELINES,
45
- PROMPT_SNIPPET as PLAN_PROMPT_SNIPPET,
46
- } from "../plan-approval/schema.js";
47
- import type { ApprovePlanParams } from "../plan-approval/types.js";
48
- import {
49
- formatApprovePlanResultText,
50
- toApprovePlanToolDetails,
51
- validateApprovePlanParams,
52
- } from "../plan-approval/validate.js";
53
28
 
54
29
  const HARNESS_UI_AGENT_TYPES = new Set([
55
- "harness/planner",
56
30
  "harness/evaluator",
57
31
  "harness/adversary",
58
32
  "harness/tie-breaker",
@@ -76,18 +50,6 @@ export interface ParentHarnessUiHooks {
76
50
  ) => void;
77
51
  }
78
52
 
79
- const CreatePlanParamsSchema = Type.Object({
80
- plan_packet: Type.Object(
81
- {},
82
- {
83
- description:
84
- "Approved PlanPacket to persist (same object as approve_plan).",
85
- },
86
- ),
87
- });
88
-
89
- const PLANNER_ONLY_AGENT = "harness/planner";
90
-
91
53
  export function agentTypeAllowsParentHarnessUi(agentType: string): boolean {
92
54
  return HARNESS_UI_AGENT_TYPES.has(agentType);
93
55
  }
@@ -120,7 +82,7 @@ export function createParentHarnessUiBridgeFactory(
120
82
  name: "ask_user",
121
83
  label: "Ask User",
122
84
  description:
123
- "Ask the user a structured question (parent session UI). Use for clarification not final plan approval (use approve_plan).",
85
+ "Ask the user a structured question (parent session UI). Plan approval uses approve_plan on the parent orchestrator only.",
124
86
  promptSnippet: ASK_PROMPT_SNIPPET,
125
87
  promptGuidelines: ASK_PROMPT_GUIDELINES,
126
88
  parameters: AskUserParamsSchema,
@@ -162,141 +124,6 @@ export function createParentHarnessUiBridgeFactory(
162
124
  return renderAskResult(result, options, theme);
163
125
  },
164
126
  });
165
-
166
- if (agentType !== PLANNER_ONLY_AGENT) {
167
- return;
168
- }
169
-
170
- pi.registerTool({
171
- name: "approve_plan",
172
- label: "Approve Plan",
173
- description:
174
- "Present the full PlanPacket for user approval in the parent TUI (scrollable overlay).",
175
- promptSnippet: PLAN_PROMPT_SNIPPET,
176
- promptGuidelines: PLAN_PROMPT_GUIDELINES,
177
- parameters: ApprovePlanParamsSchema,
178
- async execute(_toolCallId, params, _signal, _onUpdate) {
179
- const validated = validateApprovePlanParams(
180
- params as ApprovePlanParams,
181
- );
182
- if (typeof validated === "string") {
183
- return {
184
- content: [{ type: "text", text: validated }],
185
- details: {
186
- plan_packet: (params as ApprovePlanParams).plan_packet ?? {},
187
- options: [],
188
- response: null,
189
- cancelled: true,
190
- },
191
- };
192
- }
193
-
194
- hooks?.appendPlanDraft?.({
195
- plan_packet: validated.plan_packet,
196
- human_summary: validated.human_summary,
197
- });
198
-
199
- let outcome: DialogResult;
200
- if (parentCtx.hasUI) {
201
- outcome = await runPlanApprovalDialog(parentCtx.ui, validated, {
202
- onMounted: () => {
203
- pi.events.emit("plan-approval:mounted", {});
204
- },
205
- });
206
- } else {
207
- outcome = await runPlanApprovalFallback(parentCtx.ui, validated);
208
- }
209
- const details = toApprovePlanToolDetails(
210
- validated,
211
- outcome.response,
212
- outcome.cancelled,
213
- );
214
- notifyPlanApproval(hooks, details, "approve_plan");
215
- const text = formatApprovePlanResultText(
216
- outcome.response,
217
- outcome.cancelled,
218
- );
219
- return {
220
- content: [{ type: "text", text }],
221
- details,
222
- };
223
- },
224
- renderCall(args, theme) {
225
- return renderApprovePlanCall(args, theme);
226
- },
227
- renderResult(result, options, theme) {
228
- return renderApprovePlanResult(result, options, theme);
229
- },
230
- });
231
-
232
- pi.registerTool({
233
- name: "create_plan",
234
- label: "Create Plan",
235
- description:
236
- "Write the approved PlanPacket to the canonical plan-packet.json for this harness run. Requires approve_plan Approve first. Do not use write/edit.",
237
- promptSnippet: CREATE_PLAN_SNIPPET,
238
- promptGuidelines: CREATE_PLAN_GUIDELINES,
239
- parameters: CreatePlanParamsSchema,
240
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
241
- const validated = validateApprovePlanParams(
242
- params as ApprovePlanParams,
243
- );
244
- if (typeof validated === "string") {
245
- return {
246
- content: [{ type: "text", text: validated }],
247
- details: { error: validated },
248
- };
249
- }
250
- const projectRoot = hooks?.projectRoot ?? parentCtx.cwd;
251
- const parentEntries = hooks?.getParentEntries?.() ?? [];
252
- const subEntries = ctx.sessionManager.getEntries();
253
- const result = await executeCreatePlan(validated.plan_packet, {
254
- projectRoot,
255
- getParentEntries: () => parentEntries,
256
- getSubagentEntries: () => subEntries,
257
- getParentRunContext: () => hooks?.getParentRunContext?.() ?? null,
258
- onCommitted: (runCtx, packet, planPath) => {
259
- hooks?.onPlanCommitted?.(runCtx, packet, planPath);
260
- },
261
- });
262
- const text = formatCreatePlanResultText(result);
263
- return {
264
- content: [{ type: "text", text }],
265
- details: result.ok
266
- ? {
267
- plan_path: result.planPath,
268
- plan_id: result.planId,
269
- }
270
- : { error: result.error },
271
- isError: !result.ok,
272
- };
273
- },
274
- renderCall(args, theme) {
275
- const packet = (args as { plan_packet?: PlanPacketLike }).plan_packet;
276
- const id = packet?.plan_id ?? "?";
277
- return new Text(theme.fg("accent", `create_plan: ${id}`), 0, 0);
278
- },
279
- renderResult(result, _options, theme) {
280
- const details = result.details as
281
- | { plan_path?: string; error?: string }
282
- | undefined;
283
- if (details?.error) {
284
- return new Text(
285
- theme.fg("error", details.error ?? "create_plan failed"),
286
- 0,
287
- 0,
288
- );
289
- }
290
- return new Text(
291
- theme.fg(
292
- "success",
293
- `Wrote ${details?.plan_path ?? "plan-packet.json"}`,
294
- ),
295
- 0,
296
- 0,
297
- );
298
- },
299
- });
300
127
  };
301
128
  }
302
129
 
@@ -7,6 +7,7 @@ import {
7
7
  type PlanUserApproval,
8
8
  planPacketSummary,
9
9
  } from "../../../lib/harness-run-context.js";
10
+ import { writePlanReviewMarkdown } from "../plan-approval/plan-review.js";
10
11
  import type { ParentHarnessUiHooks } from "./parent-harness-ui-bridge.js";
11
12
 
12
13
  function persistRunContext(pi: ExtensionAPI, runCtx: HarnessRunContext): void {
@@ -26,6 +27,23 @@ export function createParentHarnessUiHooks(
26
27
  const planId = String(draft.plan_packet.plan_id ?? "plan");
27
28
  const summary =
28
29
  draft.human_summary?.trim() || `Plan ${planId} — pending your approval`;
30
+ const runCtx = getLatestRunContext(getParentEntries());
31
+ void writePlanReviewMarkdown(projectRoot, runCtx, draft.plan_packet, {
32
+ human_summary: draft.human_summary,
33
+ status: "draft",
34
+ }).then((reviewPath) => {
35
+ if (!reviewPath) return;
36
+ pi.sendMessage({
37
+ customType: "harness-plan-review-path",
38
+ content: `Editor review: ${reviewPath}`,
39
+ display: true,
40
+ details: {
41
+ schema_version: "1.0.0",
42
+ plan_review_path: reviewPath,
43
+ plan_id: planId,
44
+ },
45
+ });
46
+ });
29
47
  pi.sendMessage({
30
48
  customType: "harness-plan-draft",
31
49
  content: summary,
@@ -10,7 +10,6 @@ export const SUBAGENT_BLOCKED_TOOLS = new Set([
10
10
  ]);
11
11
 
12
12
  const ASK_USER_ALLOWED_AGENT_TYPES = new Set([
13
- "harness/planner",
14
13
  "harness/evaluator",
15
14
  "harness/adversary",
16
15
  "harness/tie-breaker",
@@ -42,12 +41,9 @@ export function evaluateSubagentToolCall(
42
41
  };
43
42
  }
44
43
  if (toolName === "approve_plan" || toolName === "create_plan") {
45
- if (agentType === "harness/planner") {
46
- return { action: "allow" };
47
- }
48
44
  return {
49
45
  action: "block",
50
- reason: `Tool "${toolName}" is only available for harness/planner.`,
46
+ reason: `Tool "${toolName}" is only available in the parent harness orchestrator session.`,
51
47
  };
52
48
  }
53
49
  return { action: "allow" };
@@ -420,13 +420,6 @@ export async function runAgent(
420
420
  names.filter((t) => {
421
421
  if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
422
422
  if (t === "ask_user" && harnessUiBridge) return true;
423
- if (
424
- (t === "approve_plan" || t === "create_plan") &&
425
- harnessUiBridge &&
426
- type === "harness/planner"
427
- ) {
428
- return true;
429
- }
430
423
  if (disallowedSet?.has(t)) return false;
431
424
  if (builtinToolNameSet.has(t)) return true;
432
425
  if (extensions === false) return false;
@@ -442,13 +435,6 @@ export async function runAgent(
442
435
  } else {
443
436
  const fallback = toolNames.filter((t) => {
444
437
  if (t === "ask_user" && harnessUiBridge) return true;
445
- if (
446
- (t === "approve_plan" || t === "create_plan") &&
447
- harnessUiBridge &&
448
- type === "harness/planner"
449
- ) {
450
- return true;
451
- }
452
438
  return !disallowedSet?.has(t);
453
439
  });
454
440
  session.setActiveToolsByName(fallback);
@@ -470,10 +456,6 @@ export async function runAgent(
470
456
  if (harnessUiBridge) {
471
457
  const withHarnessUi = new Set(session.getActiveToolNames());
472
458
  withHarnessUi.add("ask_user");
473
- if (type === "harness/planner") {
474
- withHarnessUi.add("approve_plan");
475
- withHarnessUi.add("create_plan");
476
- }
477
459
  session.setActiveToolsByName([...withHarnessUi]);
478
460
  }
479
461