ultimate-pi 0.10.0 → 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-live-widget.ts +48 -28
- package/.pi/extensions/harness-plan-approval.ts +192 -24
- package/.pi/extensions/harness-run-context.ts +24 -15
- 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 -171
- 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 +4 -36
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +2 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +5 -0
- package/.pi/extensions/lib/plan-approval/dialog.ts +231 -147
- 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 +116 -20
- package/.pi/prompts/harness-setup.md +1 -1
- package/.pi/scripts/harness-resolve-up-pkg.mjs +13 -0
- package/CHANGELOG.md +18 -0
- package/biome.json +4 -1
- package/package.json +2 -2
|
@@ -329,6 +329,52 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
329
329
|
let component: HarnessWidgetComponent | null = null;
|
|
330
330
|
let refreshQueued = false;
|
|
331
331
|
let lastRenderHash = "";
|
|
332
|
+
let mountCtx: ExtensionContext | null = null;
|
|
333
|
+
|
|
334
|
+
function mountHarnessWidget(ctx: ExtensionContext): void {
|
|
335
|
+
if (!ctx.hasUI) return;
|
|
336
|
+
const state = stateStore.refresh(ctx);
|
|
337
|
+
const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
|
|
338
|
+
lastRenderHash = computeRenderHash(state, inFlight);
|
|
339
|
+
|
|
340
|
+
ctx.ui.setWidget(
|
|
341
|
+
"harness-live",
|
|
342
|
+
(tui, theme) => {
|
|
343
|
+
widgetMounted = true;
|
|
344
|
+
tuiHandle = tui;
|
|
345
|
+
component = new HarnessWidgetComponent(
|
|
346
|
+
stateStore.snapshot(),
|
|
347
|
+
inFlight,
|
|
348
|
+
theme,
|
|
349
|
+
);
|
|
350
|
+
return {
|
|
351
|
+
render(width: number): string[] {
|
|
352
|
+
component?.setTheme(theme);
|
|
353
|
+
return component?.render(width) ?? [];
|
|
354
|
+
},
|
|
355
|
+
invalidate(): void {
|
|
356
|
+
component?.invalidate();
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
{ placement: "aboveEditor" },
|
|
361
|
+
);
|
|
362
|
+
updateStatusFallback(ctx, state);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function remountHarnessLiveWidget(ctx: ExtensionContext): void {
|
|
366
|
+
if (!ctx.hasUI || !widgetMounted) return;
|
|
367
|
+
ctx.ui.setWidget("harness-live", undefined);
|
|
368
|
+
mountHarnessWidget(ctx);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
pi.events.on("subagents:agents-widget-mounted", () => {
|
|
372
|
+
if (mountCtx) remountHarnessLiveWidget(mountCtx);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
pi.events.on("plan-approval:mounted", () => {
|
|
376
|
+
if (mountCtx) remountHarnessLiveWidget(mountCtx);
|
|
377
|
+
});
|
|
332
378
|
|
|
333
379
|
function updateStatusFallback(
|
|
334
380
|
ctx: ExtensionContext,
|
|
@@ -385,34 +431,8 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
385
431
|
}
|
|
386
432
|
|
|
387
433
|
pi.on("session_start", (_event, ctx) => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
|
|
391
|
-
lastRenderHash = computeRenderHash(state, inFlight);
|
|
392
|
-
|
|
393
|
-
ctx.ui.setWidget(
|
|
394
|
-
"harness-live",
|
|
395
|
-
(tui, theme) => {
|
|
396
|
-
widgetMounted = true;
|
|
397
|
-
tuiHandle = tui;
|
|
398
|
-
component = new HarnessWidgetComponent(
|
|
399
|
-
stateStore.snapshot(),
|
|
400
|
-
inFlight,
|
|
401
|
-
theme,
|
|
402
|
-
);
|
|
403
|
-
return {
|
|
404
|
-
render(width: number): string[] {
|
|
405
|
-
component?.setTheme(theme);
|
|
406
|
-
return component?.render(width) ?? [];
|
|
407
|
-
},
|
|
408
|
-
invalidate(): void {
|
|
409
|
-
component?.invalidate();
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
},
|
|
413
|
-
{ placement: "aboveEditor" },
|
|
414
|
-
);
|
|
415
|
-
updateStatusFallback(ctx, state);
|
|
434
|
+
mountCtx = ctx;
|
|
435
|
+
mountHarnessWidget(ctx);
|
|
416
436
|
});
|
|
417
437
|
|
|
418
438
|
pi.on("context", (_event, ctx) => {
|
|
@@ -4,13 +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,
|
|
12
|
+
hasPlanUserApproval,
|
|
10
13
|
parsePlanApprovalFromMessage,
|
|
14
|
+
planPacketSummary,
|
|
11
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";
|
|
12
23
|
import { runPlanApprovalDialog } from "./lib/plan-approval/dialog.js";
|
|
13
24
|
import { runPlanApprovalFallback } from "./lib/plan-approval/fallback.js";
|
|
25
|
+
import { writePlanReviewMarkdown } from "./lib/plan-approval/plan-review.js";
|
|
14
26
|
import {
|
|
15
27
|
renderApprovePlanCall,
|
|
16
28
|
renderApprovePlanResult,
|
|
@@ -31,33 +43,50 @@ import {
|
|
|
31
43
|
validateApprovePlanParams,
|
|
32
44
|
} from "./lib/plan-approval/validate.js";
|
|
33
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
|
+
|
|
34
59
|
export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
if (!claimExtensionLoad("harness-plan-approval", MODULE_URL)) return;
|
|
61
|
+
pi.registerMessageRenderer(
|
|
62
|
+
"harness-plan-draft",
|
|
63
|
+
(message, _options, theme) => {
|
|
64
|
+
const data = message.details as
|
|
65
|
+
| {
|
|
66
|
+
plan_packet?: unknown;
|
|
67
|
+
human_summary?: string | null;
|
|
68
|
+
}
|
|
69
|
+
| undefined;
|
|
70
|
+
if (!data?.plan_packet) return undefined;
|
|
71
|
+
const lines = renderHarnessPlanDraft(
|
|
72
|
+
{
|
|
73
|
+
plan_packet: data.plan_packet as Parameters<
|
|
74
|
+
typeof renderHarnessPlanDraft
|
|
75
|
+
>[0]["plan_packet"],
|
|
76
|
+
human_summary: data.human_summary,
|
|
77
|
+
},
|
|
78
|
+
80,
|
|
79
|
+
theme,
|
|
80
|
+
);
|
|
81
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
55
84
|
|
|
56
85
|
pi.registerTool({
|
|
57
86
|
name: "approve_plan",
|
|
58
87
|
label: "Approve Plan",
|
|
59
88
|
description:
|
|
60
|
-
"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.",
|
|
61
90
|
promptSnippet: PROMPT_SNIPPET,
|
|
62
91
|
promptGuidelines: PROMPT_GUIDELINES,
|
|
63
92
|
parameters: ApprovePlanParamsSchema,
|
|
@@ -76,25 +105,74 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
76
105
|
};
|
|
77
106
|
}
|
|
78
107
|
|
|
108
|
+
const entries = ctx.sessionManager.getEntries();
|
|
109
|
+
if (
|
|
110
|
+
hasPlanUserApproval(entries, {
|
|
111
|
+
sincePlanCommand: true,
|
|
112
|
+
planId: validated.plan_packet.plan_id ?? null,
|
|
113
|
+
})
|
|
114
|
+
) {
|
|
115
|
+
const planId = String(validated.plan_packet.plan_id ?? "plan");
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: `Plan ${planId} already approved in this harness run. Proceed with /harness-run.`,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
details: {
|
|
124
|
+
plan_packet: validated.plan_packet,
|
|
125
|
+
options: validated.options,
|
|
126
|
+
response: {
|
|
127
|
+
kind: "selection",
|
|
128
|
+
selections: ["Approve"],
|
|
129
|
+
},
|
|
130
|
+
cancelled: false,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
const planId = String(validated.plan_packet.plan_id ?? "plan");
|
|
80
136
|
const summary =
|
|
81
137
|
validated.human_summary?.trim() ||
|
|
82
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;
|
|
83
155
|
pi.sendMessage({
|
|
84
156
|
customType: "harness-plan-draft",
|
|
85
|
-
content:
|
|
157
|
+
content: draftContent,
|
|
86
158
|
display: true,
|
|
87
159
|
details: {
|
|
88
160
|
schema_version: "1.0.0",
|
|
89
161
|
plan_packet: validated.plan_packet,
|
|
90
162
|
human_summary: validated.human_summary ?? null,
|
|
163
|
+
research_brief: validated.research_brief ?? null,
|
|
164
|
+
plan_review_path: reviewPath,
|
|
91
165
|
shown_at: new Date().toISOString(),
|
|
92
166
|
},
|
|
93
167
|
});
|
|
94
168
|
|
|
95
169
|
let outcome: PlanApprovalDialogResult;
|
|
96
170
|
if (ctx.hasUI) {
|
|
97
|
-
outcome = await runPlanApprovalDialog(ctx.ui, validated
|
|
171
|
+
outcome = await runPlanApprovalDialog(ctx.ui, validated, {
|
|
172
|
+
onMounted: () => {
|
|
173
|
+
pi.events.emit("plan-approval:mounted", {});
|
|
174
|
+
},
|
|
175
|
+
});
|
|
98
176
|
} else {
|
|
99
177
|
outcome = await runPlanApprovalFallback(ctx.ui, validated);
|
|
100
178
|
}
|
|
@@ -109,7 +187,6 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
109
187
|
details,
|
|
110
188
|
});
|
|
111
189
|
if (approval) {
|
|
112
|
-
const entries = ctx.sessionManager.getEntries();
|
|
113
190
|
const runCtx = getLatestRunContext(entries);
|
|
114
191
|
appendPlanApprovalIfNew(
|
|
115
192
|
(type, data) => pi.appendEntry(type, data),
|
|
@@ -119,6 +196,23 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
119
196
|
);
|
|
120
197
|
}
|
|
121
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
|
+
|
|
122
216
|
const text = formatApprovePlanResultText(
|
|
123
217
|
outcome.response,
|
|
124
218
|
outcome.cancelled,
|
|
@@ -137,4 +231,78 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
137
231
|
return renderApprovePlanResult(result, options, theme);
|
|
138
232
|
},
|
|
139
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
|
+
});
|
|
140
308
|
}
|
|
@@ -671,20 +671,29 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
671
671
|
});
|
|
672
672
|
|
|
673
673
|
pi.on("tool_call", async (event, ctx) => {
|
|
674
|
-
if (
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
674
|
+
if (activeCtx?.plan_packet_path) {
|
|
675
|
+
const entries = getEntries(ctx);
|
|
676
|
+
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
677
|
+
if (event.toolName === "approve_plan") {
|
|
678
|
+
return {
|
|
679
|
+
block: true,
|
|
680
|
+
reason:
|
|
681
|
+
"harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (event.toolName === "ask_user") {
|
|
685
|
+
const input = event.input as {
|
|
686
|
+
question?: string;
|
|
687
|
+
options?: unknown[];
|
|
688
|
+
};
|
|
689
|
+
if (isPlanApprovalAskUser(input)) {
|
|
690
|
+
return {
|
|
691
|
+
block: true,
|
|
692
|
+
reason:
|
|
693
|
+
"harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
}
|
|
688
697
|
}
|
|
689
698
|
}
|
|
690
699
|
if (!activeCtx?.plan_packet_path) return undefined;
|
|
@@ -807,7 +816,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
807
816
|
})
|
|
808
817
|
) {
|
|
809
818
|
const msg =
|
|
810
|
-
"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.";
|
|
811
820
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
812
821
|
return;
|
|
813
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":
|