ultimate-pi 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/agt-prompt-guard.ts +20 -6
- package/.pi/extensions/harness-auto-compact.ts +94 -0
- package/.pi/extensions/harness-debate-tools.ts +26 -2
- package/.pi/extensions/harness-live-widget.ts +19 -2
- package/.pi/extensions/harness-plan-approval.ts +62 -19
- package/.pi/extensions/harness-plan-orchestration.ts +140 -0
- package/.pi/extensions/harness-run-context.ts +457 -48
- package/.pi/extensions/harness-web-tools.ts +1 -0
- package/.pi/extensions/policy-gate.ts +9 -0
- package/.pi/harness/agents.manifest.json +1 -1
- package/.pi/harness/docs/adrs/0056-agent-native-speed-wiring.md +26 -0
- package/.pi/harness/env.harness.template +7 -1
- package/.pi/lib/harness-auto-approve.ts +140 -0
- package/.pi/lib/harness-auto-compact-policy.ts +85 -0
- package/.pi/lib/harness-phase-telemetry.ts +7 -0
- package/.pi/lib/harness-phase-worker.ts +23 -0
- package/.pi/lib/harness-plan-fsm.ts +162 -0
- package/.pi/lib/harness-plan-route.ts +134 -0
- package/.pi/lib/harness-posthog.ts +4 -1
- package/.pi/lib/harness-remediation.ts +79 -0
- package/.pi/lib/harness-repair-brief.ts +2 -2
- package/.pi/lib/harness-review-parallel.ts +18 -0
- package/.pi/lib/harness-run-context.ts +119 -72
- package/.pi/lib/harness-spawn-budget.ts +32 -4
- package/.pi/lib/harness-spawn-topology.ts +36 -1
- package/.pi/lib/harness-subagent-precheck.ts +3 -2
- package/.pi/lib/harness-subagent-progress.ts +8 -5
- package/.pi/lib/harness-subagents-bridge.ts +14 -12
- package/.pi/lib/harness-vcc-settings.ts +36 -0
- package/.pi/lib/plan-approval-readiness.ts +9 -5
- package/.pi/lib/plan-debate-eligibility-snapshot.ts +90 -0
- package/.pi/lib/plan-debate-eligibility.ts +12 -7
- package/.pi/lib/plan-debate-focus.ts +23 -11
- package/.pi/lib/plan-debate-gate.ts +71 -29
- package/.pi/lib/plan-debate-round-status.ts +23 -8
- package/.pi/lib/plan-headless-ux.ts +598 -0
- package/.pi/lib/plan-human-gates.ts +24 -85
- package/.pi/lib/plan-messenger.ts +3 -3
- package/.pi/lib/plan-review-gate.ts +56 -0
- package/.pi/prompts/harness-abort.md +1 -0
- package/.pi/prompts/harness-auto.md +1 -1
- package/.pi/prompts/harness-clear.md +6 -6
- package/.pi/prompts/harness-plan.md +15 -2
- package/.pi/prompts/harness-review.md +2 -2
- package/.pi/scripts/harness-project-toggle.mjs +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/package.json +1 -1
|
@@ -4,21 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { PromptDefenseEvaluator } from "@microsoft/agent-governance-sdk";
|
|
7
|
+
import { isHarnessNonInteractive } from "../lib/ask-user/policy.js";
|
|
7
8
|
import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
|
|
8
|
-
import {
|
|
9
|
+
import { harnessSlashCommandLineForPolicy } from "../lib/harness-run-context.js";
|
|
9
10
|
|
|
10
11
|
const evaluator = new PromptDefenseEvaluator({ minGrade: "D" });
|
|
11
12
|
|
|
12
13
|
export default function agtPromptGuard(pi: ExtensionAPI) {
|
|
13
14
|
if (!isHarnessProjectEnabled()) return;
|
|
14
15
|
|
|
15
|
-
pi.on("before_agent_start", async (event) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
17
|
+
const commandLine = harnessSlashCommandLineForPolicy(
|
|
18
|
+
event.prompt,
|
|
19
|
+
ctx.sessionManager.getEntries(),
|
|
20
|
+
);
|
|
21
|
+
if (!commandLine) return undefined;
|
|
19
22
|
|
|
20
|
-
const report = evaluator.evaluate(
|
|
23
|
+
const report = evaluator.evaluate(commandLine);
|
|
21
24
|
if (report.isBlocking("D")) {
|
|
25
|
+
if (isHarnessNonInteractive()) {
|
|
26
|
+
pi.appendEntry("harness-policy-violation", {
|
|
27
|
+
source: "agt-prompt-guard",
|
|
28
|
+
display: false,
|
|
29
|
+
grade: report.grade,
|
|
30
|
+
score: report.score,
|
|
31
|
+
missing: report.missing,
|
|
32
|
+
advisory: true,
|
|
33
|
+
});
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
22
36
|
return {
|
|
23
37
|
message: {
|
|
24
38
|
customType: "harness-policy-violation",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness auto-compact at 50% context usage (VCC-backed).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { claimExtensionLoad } from "../lib/extension-load-guard.js";
|
|
7
|
+
import {
|
|
8
|
+
type CompactGateState,
|
|
9
|
+
createCompactGateState,
|
|
10
|
+
evaluateAutoCompactGate,
|
|
11
|
+
onCompactCancel,
|
|
12
|
+
onSessionCompact,
|
|
13
|
+
} from "../lib/harness-auto-compact-policy.js";
|
|
14
|
+
import { captureHarnessEvent } from "../lib/harness-debate-core-deps.js";
|
|
15
|
+
|
|
16
|
+
// @ts-expect-error pi extensions run as ESM
|
|
17
|
+
const MODULE_URL = import.meta.url;
|
|
18
|
+
|
|
19
|
+
const gateBySession = new Map<string, CompactGateState>();
|
|
20
|
+
|
|
21
|
+
function gateForSession(sessionId: string): CompactGateState {
|
|
22
|
+
let state = gateBySession.get(sessionId);
|
|
23
|
+
if (!state) {
|
|
24
|
+
state = createCompactGateState();
|
|
25
|
+
gateBySession.set(sessionId, state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function harnessAutoCompact(pi: ExtensionAPI): void {
|
|
31
|
+
if (!claimExtensionLoad("harness-auto-compact", MODULE_URL)) return;
|
|
32
|
+
|
|
33
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
34
|
+
if (event.toolName !== "subagent") return;
|
|
35
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
36
|
+
if (!sessionId) return;
|
|
37
|
+
gateForSession(sessionId).subagentSpawnPending = true;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
41
|
+
if (event.toolName !== "subagent") return;
|
|
42
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
43
|
+
if (!sessionId) return;
|
|
44
|
+
gateForSession(sessionId).subagentSpawnPending = false;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
48
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
49
|
+
if (!sessionId) return;
|
|
50
|
+
onSessionCompact(gateForSession(sessionId));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("agent_end", async (_message, ctx) => {
|
|
54
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
55
|
+
const state = gateForSession(sessionId);
|
|
56
|
+
if (state.cooldownTurns > 0) {
|
|
57
|
+
state.cooldownTurns -= 1;
|
|
58
|
+
}
|
|
59
|
+
const usage = ctx.getContextUsage();
|
|
60
|
+
if (!usage) return;
|
|
61
|
+
const isSubagent = process.env.PI_HARNESS_SUBPROCESS === "1";
|
|
62
|
+
const decision = evaluateAutoCompactGate(
|
|
63
|
+
{
|
|
64
|
+
percent: usage.percent ?? 0,
|
|
65
|
+
tokens: usage.tokens ?? undefined,
|
|
66
|
+
contextWindow: usage.contextWindow ?? undefined,
|
|
67
|
+
},
|
|
68
|
+
state,
|
|
69
|
+
{ isSubagent },
|
|
70
|
+
);
|
|
71
|
+
if (!decision.shouldCompact) return;
|
|
72
|
+
state.inFlight = true;
|
|
73
|
+
try {
|
|
74
|
+
await ctx.compact({
|
|
75
|
+
onComplete: (result) => {
|
|
76
|
+
const cancelled =
|
|
77
|
+
(result as { cancel?: boolean } | undefined)?.cancel === true;
|
|
78
|
+
if (cancelled) {
|
|
79
|
+
onCompactCancel(state);
|
|
80
|
+
}
|
|
81
|
+
captureHarnessEvent(sessionId, "harness_auto_compact", {
|
|
82
|
+
percent: usage.percent ?? 0,
|
|
83
|
+
tokens_before: usage.tokens ?? undefined,
|
|
84
|
+
context_window: usage.contextWindow ?? undefined,
|
|
85
|
+
compactor: "ultimate-pi-vcc",
|
|
86
|
+
cancelled,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
} catch {
|
|
91
|
+
state.inFlight = false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -51,10 +51,18 @@ import {
|
|
|
51
51
|
validateIntegratorDraft,
|
|
52
52
|
withReviewRoundYamlWrite,
|
|
53
53
|
} from "../lib/harness-debate-workflow-deps.js";
|
|
54
|
+
import {
|
|
55
|
+
loadPlanDebateEligibilitySnapshot,
|
|
56
|
+
writePlanDebateEligibilitySnapshot,
|
|
57
|
+
} from "../lib/plan-debate-eligibility-snapshot.js";
|
|
54
58
|
import {
|
|
55
59
|
checkDebateWallClock,
|
|
56
60
|
debateWallClockRecoveryHint,
|
|
57
61
|
} from "../lib/plan-debate-wall-clock.js";
|
|
62
|
+
import {
|
|
63
|
+
planReviewGateModeForProfile,
|
|
64
|
+
planReviewGateStrategyFromEligibility,
|
|
65
|
+
} from "../lib/plan-review-gate.js";
|
|
58
66
|
|
|
59
67
|
// @ts-expect-error pi extensions run as ESM
|
|
60
68
|
const MODULE_URL = import.meta.url;
|
|
@@ -237,6 +245,7 @@ function registerHarnessDebateHandler2(pi: ExtensionAPI) {
|
|
|
237
245
|
),
|
|
238
246
|
};
|
|
239
247
|
const result = harnessPlanDebateEligibility(input);
|
|
248
|
+
await writePlanDebateEligibilitySnapshot(rd, result);
|
|
240
249
|
const lines = [
|
|
241
250
|
`profile: ${result.profile}`,
|
|
242
251
|
`review_gate_mode: ${result.review_gate_strategy.mode}`,
|
|
@@ -272,6 +281,11 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
272
281
|
Type.String({ description: "spec | wbs | schedule | quality" }),
|
|
273
282
|
),
|
|
274
283
|
),
|
|
284
|
+
review_gate_mode: Type.Optional(
|
|
285
|
+
Type.String({
|
|
286
|
+
description: "consolidated | threaded | parallel_probes",
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
275
289
|
}),
|
|
276
290
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
277
291
|
const runId = getRunId(ctx);
|
|
@@ -280,6 +294,7 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
280
294
|
debate_id?: string;
|
|
281
295
|
debate_profile?: string;
|
|
282
296
|
required_focuses?: string[];
|
|
297
|
+
review_gate_mode?: string;
|
|
283
298
|
};
|
|
284
299
|
const raw = String(p.debate_id ?? "");
|
|
285
300
|
const { debateId, corrected, warning } = normalizePlanDebateId(
|
|
@@ -296,14 +311,23 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
296
311
|
const required_focuses = (p.required_focuses ?? []).filter((f) =>
|
|
297
312
|
["spec", "wbs", "schedule", "quality"].includes(f),
|
|
298
313
|
) as Array<"spec" | "wbs" | "schedule" | "quality">;
|
|
314
|
+
const rd = runDir(projectRoot, runId);
|
|
315
|
+
const eligibilitySnapshot = await loadPlanDebateEligibilitySnapshot(rd);
|
|
299
316
|
const opened = await openDebateBus(runId, debateId, debateHooks(pi), {
|
|
300
317
|
debate_profile: profile,
|
|
301
318
|
required_focuses:
|
|
302
319
|
required_focuses.length > 0 ? required_focuses : undefined,
|
|
303
320
|
});
|
|
321
|
+
const explicitMode = p.review_gate_mode;
|
|
304
322
|
const review_gate_mode =
|
|
305
|
-
|
|
306
|
-
|
|
323
|
+
explicitMode === "consolidated" ||
|
|
324
|
+
explicitMode === "threaded" ||
|
|
325
|
+
explicitMode === "parallel_probes"
|
|
326
|
+
? explicitMode
|
|
327
|
+
: eligibilitySnapshot
|
|
328
|
+
? planReviewGateStrategyFromEligibility(eligibilitySnapshot).mode
|
|
329
|
+
: planReviewGateModeForProfile(profile);
|
|
330
|
+
await initPlanMessenger(rd, {
|
|
307
331
|
runId,
|
|
308
332
|
debateId,
|
|
309
333
|
debate_profile: profile,
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
|
|
6
6
|
import { evaluateCrossSessionResume } from "../lib/harness-run-context.js";
|
|
7
|
+
import { buildHarnessProgressStatusLine } from "../lib/harness-subagent-progress.js";
|
|
7
8
|
import {
|
|
8
9
|
deriveHarnessStatusHint,
|
|
9
10
|
formatHarnessPhaseLabel,
|
|
@@ -312,11 +313,11 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
312
313
|
});
|
|
313
314
|
|
|
314
315
|
pi.events.on("harness-progress:updated", () => {
|
|
315
|
-
if (mountCtx)
|
|
316
|
+
if (mountCtx) scheduleProgressRefresh(mountCtx);
|
|
316
317
|
});
|
|
317
318
|
|
|
318
319
|
pi.events.on("harness-waiting-for-user", () => {
|
|
319
|
-
if (mountCtx)
|
|
320
|
+
if (mountCtx) scheduleProgressRefresh(mountCtx);
|
|
320
321
|
});
|
|
321
322
|
|
|
322
323
|
pi.events.on("harness-cross-session-resume", (payload: unknown) => {
|
|
@@ -366,9 +367,25 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
366
367
|
flowSubstate: state.flowSubstate,
|
|
367
368
|
nextRecommendedCommand: state.nextRecommendedCommand,
|
|
368
369
|
crossSessionResumeCommand: state.crossSessionResumeCommand,
|
|
370
|
+
progressLine: buildHarnessProgressStatusLine(),
|
|
369
371
|
});
|
|
370
372
|
}
|
|
371
373
|
|
|
374
|
+
/** Re-render widget when elapsed-time progress changes (bypasses hash short-circuit). */
|
|
375
|
+
function scheduleProgressRefresh(ctx: ExtensionContext): void {
|
|
376
|
+
if (!isHarnessProjectEnabled()) {
|
|
377
|
+
clearHarnessWidget(ctx);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const state = stateStore.refresh(ctx);
|
|
381
|
+
const hash = computeRenderHash(state);
|
|
382
|
+
updateStatusFallback(ctx, state);
|
|
383
|
+
lastRenderHash = hash;
|
|
384
|
+
if (component) component.setData(state);
|
|
385
|
+
component?.invalidate();
|
|
386
|
+
tuiHandle?.requestRender();
|
|
387
|
+
}
|
|
388
|
+
|
|
372
389
|
function scheduleRefresh(ctx: ExtensionContext): void {
|
|
373
390
|
if (refreshQueued) return;
|
|
374
391
|
refreshQueued = true;
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* harness-plan-approval — PlanPacket approval UI and transcript renderer for parent sessions.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
8
|
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
9
|
+
import { tryAutoApprovePlan } from "../lib/harness-auto-approve.js";
|
|
8
10
|
import type { PlanPacketLike } from "../lib/harness-run-context.js";
|
|
9
11
|
import {
|
|
10
12
|
appendPlanApprovalIfNew,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
validateApprovePlanParams,
|
|
48
50
|
} from "../lib/plan-approval/validate.js";
|
|
49
51
|
import { validatePlanApprovalReadiness } from "../lib/plan-approval-readiness.js";
|
|
52
|
+
import { loadPlanDebateEligibilitySnapshot } from "../lib/plan-debate-eligibility-snapshot.js";
|
|
50
53
|
import { validatePlanDebateGate } from "../lib/plan-debate-gate.js";
|
|
51
54
|
|
|
52
55
|
// @ts-expect-error pi extensions run as ESM
|
|
@@ -166,40 +169,58 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
166
169
|
const risk = String(
|
|
167
170
|
validated.plan_packet.risk_level ?? "med",
|
|
168
171
|
).toLowerCase();
|
|
172
|
+
let readinessResult: Awaited<
|
|
173
|
+
ReturnType<typeof validatePlanApprovalReadiness>
|
|
174
|
+
> | null = null;
|
|
175
|
+
let debateGateResult: Awaited<
|
|
176
|
+
ReturnType<typeof validatePlanDebateGate>
|
|
177
|
+
> | null = null;
|
|
169
178
|
if (runCtx?.run_id) {
|
|
170
|
-
|
|
179
|
+
readinessResult = await validatePlanApprovalReadiness(
|
|
171
180
|
projectRoot,
|
|
172
181
|
runCtx.run_id,
|
|
173
182
|
{ risk_level: risk },
|
|
174
183
|
);
|
|
175
|
-
if (!
|
|
184
|
+
if (!readinessResult.ok) {
|
|
176
185
|
return {
|
|
177
186
|
content: [
|
|
178
187
|
{
|
|
179
188
|
type: "text",
|
|
180
|
-
text: `approve_plan blocked — plan phase not ready:\n- ${
|
|
189
|
+
text: `approve_plan blocked — plan phase not ready:\n- ${readinessResult.errors.join("\n- ")}`,
|
|
181
190
|
},
|
|
182
191
|
],
|
|
183
192
|
details: {
|
|
184
193
|
plan_packet: validated.plan_packet,
|
|
185
|
-
readiness,
|
|
194
|
+
readiness: readinessResult,
|
|
186
195
|
cancelled: true,
|
|
187
196
|
},
|
|
188
197
|
isError: true,
|
|
189
198
|
};
|
|
190
199
|
}
|
|
191
|
-
implWarnings.push(...
|
|
200
|
+
implWarnings.push(...readinessResult.warnings);
|
|
192
201
|
}
|
|
193
202
|
if (runCtx?.run_id) {
|
|
194
|
-
const
|
|
195
|
-
|
|
203
|
+
const runDir = join(
|
|
204
|
+
projectRoot,
|
|
205
|
+
".pi",
|
|
206
|
+
"harness",
|
|
207
|
+
"runs",
|
|
208
|
+
runCtx.run_id,
|
|
209
|
+
);
|
|
210
|
+
const eligibility = await loadPlanDebateEligibilitySnapshot(runDir);
|
|
211
|
+
debateGateResult = await validatePlanDebateGate(
|
|
212
|
+
projectRoot,
|
|
213
|
+
runCtx.run_id,
|
|
214
|
+
eligibility ?? undefined,
|
|
215
|
+
);
|
|
216
|
+
if (!debateGateResult.ok) {
|
|
196
217
|
const { buildPlanDebateGateRecovery } = await import(
|
|
197
218
|
"../lib/plan-debate-gate.js"
|
|
198
219
|
);
|
|
199
220
|
const recovery = await buildPlanDebateGateRecovery(
|
|
200
221
|
projectRoot,
|
|
201
222
|
runCtx.run_id,
|
|
202
|
-
|
|
223
|
+
debateGateResult,
|
|
203
224
|
);
|
|
204
225
|
return {
|
|
205
226
|
content: [
|
|
@@ -210,13 +231,24 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
210
231
|
],
|
|
211
232
|
details: {
|
|
212
233
|
plan_packet: validated.plan_packet,
|
|
213
|
-
debate_gate:
|
|
234
|
+
debate_gate: debateGateResult,
|
|
214
235
|
cancelled: true,
|
|
215
236
|
},
|
|
216
237
|
isError: true,
|
|
217
238
|
};
|
|
218
239
|
}
|
|
219
240
|
}
|
|
241
|
+
const autoOutcome =
|
|
242
|
+
runCtx?.run_id && readinessResult && debateGateResult
|
|
243
|
+
? await tryAutoApprovePlan({
|
|
244
|
+
projectRoot,
|
|
245
|
+
runId: runCtx.run_id,
|
|
246
|
+
riskLevel: risk,
|
|
247
|
+
readiness: readinessResult,
|
|
248
|
+
debateGate: debateGateResult,
|
|
249
|
+
})
|
|
250
|
+
: { approved: false, reasons: [] };
|
|
251
|
+
|
|
220
252
|
const reviewPath = await writePlanReviewMarkdown(
|
|
221
253
|
projectRoot,
|
|
222
254
|
runCtx,
|
|
@@ -224,7 +256,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
224
256
|
{
|
|
225
257
|
human_summary: validated.human_summary,
|
|
226
258
|
research_brief: validated.research_brief,
|
|
227
|
-
status: "draft",
|
|
259
|
+
status: autoOutcome.approved ? "approved" : "draft",
|
|
228
260
|
},
|
|
229
261
|
);
|
|
230
262
|
const planMarkdown = buildPlanApprovalMarkdown(validated);
|
|
@@ -247,16 +279,27 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
247
279
|
},
|
|
248
280
|
});
|
|
249
281
|
|
|
250
|
-
setHarnessWaitingForUser("approve_plan");
|
|
251
|
-
pi.events.emit("harness-waiting-for-user", { gate: "approve_plan" });
|
|
252
282
|
let outcome: PlanApprovalDialogResult;
|
|
253
|
-
|
|
254
|
-
outcome =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
283
|
+
if (autoOutcome.approved) {
|
|
284
|
+
outcome = {
|
|
285
|
+
response: {
|
|
286
|
+
kind: "selection",
|
|
287
|
+
selections: ["Approve"],
|
|
288
|
+
},
|
|
289
|
+
cancelled: false,
|
|
290
|
+
ui_backend: "headless",
|
|
291
|
+
};
|
|
292
|
+
} else {
|
|
293
|
+
setHarnessWaitingForUser("approve_plan");
|
|
294
|
+
pi.events.emit("harness-waiting-for-user", { gate: "approve_plan" });
|
|
295
|
+
try {
|
|
296
|
+
outcome = await runPlanApprovalDialog(ctx.ui, validated, {
|
|
297
|
+
hasUI: ctx.hasUI,
|
|
298
|
+
});
|
|
299
|
+
} finally {
|
|
300
|
+
setHarnessWaitingForUser(null);
|
|
301
|
+
pi.events.emit("harness-waiting-for-user", { gate: null });
|
|
302
|
+
}
|
|
260
303
|
}
|
|
261
304
|
|
|
262
305
|
const details = toApprovePlanToolDetails(
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan orchestration tools — FSM next action + synthesis route.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
8
|
+
import {
|
|
9
|
+
captureHarnessEvent,
|
|
10
|
+
getRunIdFromSession,
|
|
11
|
+
Type,
|
|
12
|
+
} from "../lib/harness-debate-core-deps.js";
|
|
13
|
+
import { derivePlanNextAction } from "../lib/harness-plan-fsm.js";
|
|
14
|
+
import {
|
|
15
|
+
derivePlanRouteSpawns,
|
|
16
|
+
planSynthesisPath,
|
|
17
|
+
} from "../lib/harness-plan-route.js";
|
|
18
|
+
|
|
19
|
+
// @ts-expect-error pi extensions run as ESM
|
|
20
|
+
const MODULE_URL = import.meta.url;
|
|
21
|
+
|
|
22
|
+
function getRunId(ctx: {
|
|
23
|
+
sessionManager: { getEntries(): unknown[]; getSessionId(): string };
|
|
24
|
+
}): string {
|
|
25
|
+
return (
|
|
26
|
+
getRunIdFromSession(
|
|
27
|
+
ctx.sessionManager.getEntries(),
|
|
28
|
+
ctx.sessionManager.getSessionId(),
|
|
29
|
+
) ?? ctx.sessionManager.getSessionId()
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runDir(projectRoot: string, runId: string): string {
|
|
34
|
+
return join(projectRoot, ".pi", "harness", "runs", runId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function harnessPlanOrchestration(pi: ExtensionAPI) {
|
|
38
|
+
if (!claimHarnessGovernanceLoad("harness-plan-orchestration", MODULE_URL))
|
|
39
|
+
return;
|
|
40
|
+
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: "harness_plan_next_action",
|
|
43
|
+
label: "Plan Next Action",
|
|
44
|
+
description:
|
|
45
|
+
"Deterministic plan-phase FSM: returns the next spawn, tool, gate, or wait_user action. Call before improvising orchestration steps.",
|
|
46
|
+
parameters: Type.Object({
|
|
47
|
+
quick: Type.Optional(Type.Boolean()),
|
|
48
|
+
task_summary: Type.Optional(Type.String()),
|
|
49
|
+
last_outcome: Type.Optional(Type.String()),
|
|
50
|
+
}),
|
|
51
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
52
|
+
const runId = getRunId(ctx);
|
|
53
|
+
const projectRoot = process.cwd();
|
|
54
|
+
const p = params as {
|
|
55
|
+
quick?: boolean;
|
|
56
|
+
task_summary?: string;
|
|
57
|
+
last_outcome?: string | null;
|
|
58
|
+
};
|
|
59
|
+
const entries = ctx.sessionManager.getEntries();
|
|
60
|
+
const next = await derivePlanNextAction({
|
|
61
|
+
projectRoot,
|
|
62
|
+
runId,
|
|
63
|
+
entries,
|
|
64
|
+
quick: p.quick,
|
|
65
|
+
taskSummary: p.task_summary,
|
|
66
|
+
lastOutcome: p.last_outcome,
|
|
67
|
+
});
|
|
68
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
69
|
+
captureHarnessEvent(sessionId, "harness_plan_fsm", {
|
|
70
|
+
run_id: runId,
|
|
71
|
+
phase: next.phase,
|
|
72
|
+
action: next.action,
|
|
73
|
+
review_gate_mode: next.review_gate_mode,
|
|
74
|
+
synthesis_route: next.synthesis_route,
|
|
75
|
+
});
|
|
76
|
+
const lines = [
|
|
77
|
+
`phase: ${next.phase}`,
|
|
78
|
+
`action: ${next.action}`,
|
|
79
|
+
next.tool ? `tool: ${next.tool}` : null,
|
|
80
|
+
next.agents?.length ? `agents: ${next.agents.join(", ")}` : null,
|
|
81
|
+
next.review_gate_mode
|
|
82
|
+
? `review_gate_mode: ${next.review_gate_mode}`
|
|
83
|
+
: null,
|
|
84
|
+
next.synthesis_route
|
|
85
|
+
? `synthesis_route: ${next.synthesis_route}`
|
|
86
|
+
: null,
|
|
87
|
+
...next.rationale.map((r) => `- ${r}`),
|
|
88
|
+
].filter(Boolean);
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
91
|
+
details: next,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
pi.registerTool({
|
|
97
|
+
name: "harness_plan_route",
|
|
98
|
+
label: "Plan Synthesis Route",
|
|
99
|
+
description:
|
|
100
|
+
"Returns sequential vs plan-synthesizer routing and next planning spawns from disk artifacts.",
|
|
101
|
+
parameters: Type.Object({
|
|
102
|
+
risk_level: Type.Optional(Type.String()),
|
|
103
|
+
material_fork: Type.Optional(Type.Boolean()),
|
|
104
|
+
}),
|
|
105
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
106
|
+
const runId = getRunId(ctx);
|
|
107
|
+
const rd = runDir(process.cwd(), runId);
|
|
108
|
+
const p = params as { risk_level?: string; material_fork?: boolean };
|
|
109
|
+
const route = await planSynthesisPath(rd, {
|
|
110
|
+
risk_level: p.risk_level,
|
|
111
|
+
material_fork: p.material_fork,
|
|
112
|
+
});
|
|
113
|
+
const spawns = await derivePlanRouteSpawns(rd, {
|
|
114
|
+
risk_level: p.risk_level,
|
|
115
|
+
material_fork: p.material_fork,
|
|
116
|
+
});
|
|
117
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
118
|
+
captureHarnessEvent(sessionId, "harness_plan_route", {
|
|
119
|
+
run_id: runId,
|
|
120
|
+
route,
|
|
121
|
+
agents: spawns.agents,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: [
|
|
128
|
+
`route: ${route}`,
|
|
129
|
+
spawns.agents.length
|
|
130
|
+
? `next_agents: ${spawns.agents.join(", ")}`
|
|
131
|
+
: "next_agents: (none — advance to debate or approval)",
|
|
132
|
+
...spawns.rationale.map((r) => `- ${r}`),
|
|
133
|
+
].join("\n"),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
details: spawns,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|