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.
- package/.agents/skills/harness-decisions/SKILL.md +3 -3
- package/.agents/skills/harness-orchestration/SKILL.md +19 -11
- package/.agents/skills/harness-plan/SKILL.md +15 -9
- package/.pi/agents/harness/planner.md +6 -47
- package/.pi/agents/harness/planning/decompose.md +84 -0
- package/.pi/agents/harness/planning/hypothesis-eval.md +59 -0
- package/.pi/agents/harness/planning/hypothesis.md +90 -0
- package/.pi/agents/harness/planning/plan-adversary.md +50 -0
- package/.pi/agents/harness/planning/planner.md +20 -0
- package/.pi/agents/harness/planning/scout-graphify.md +48 -0
- package/.pi/agents/harness/planning/scout-semantic.md +42 -0
- package/.pi/agents/harness/planning/scout-structure.md +44 -0
- package/.pi/extensions/harness-ask-user.ts +5 -0
- package/.pi/extensions/harness-plan-approval.ts +137 -3
- package/.pi/extensions/harness-run-context.ts +1 -1
- package/.pi/extensions/harness-subagents.ts +8 -3
- package/.pi/extensions/harness-web-tools.ts +2 -0
- package/.pi/extensions/lib/extension-load-guard.ts +39 -0
- package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +33 -5
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +2 -175
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +18 -0
- package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +1 -5
- package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -18
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +1 -35
- package/.pi/extensions/lib/plan-approval/create-plan.ts +5 -0
- package/.pi/extensions/lib/plan-approval/plan-review.ts +393 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +16 -1
- package/.pi/extensions/lib/plan-approval/types.ts +10 -0
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -0
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/extensions/ultimate-pi-vcc.ts +5 -0
- package/.pi/harness/agents.manifest.json +114 -82
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +3 -3
- package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +34 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +41 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-spawn-context.schema.json +2 -1
- package/.pi/harness/specs/plan-adversary-brief.schema.json +45 -0
- package/.pi/harness/specs/plan-decomposition-brief.schema.json +108 -0
- package/.pi/harness/specs/plan-hypothesis-brief.schema.json +96 -0
- package/.pi/harness/specs/plan-hypothesis-eval.schema.json +61 -0
- package/.pi/lib/harness-run-context.ts +12 -0
- package/.pi/prompts/harness-auto.md +1 -1
- package/.pi/prompts/harness-plan.md +111 -28
- package/.pi/prompts/harness-setup.md +1 -1
- package/.pi/scripts/harness-resolve-up-pkg.mjs +13 -0
- package/CHANGELOG.md +12 -0
- package/biome.json +4 -1
- 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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
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:
|
|
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
|
|
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).
|
|
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
|
|
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
|
|