ultimate-pi 0.23.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-ask-user.ts +14 -5
- package/.pi/extensions/harness-auto-compact.ts +94 -0
- package/.pi/extensions/harness-debate-tools.ts +59 -4
- package/.pi/extensions/harness-live-widget.ts +25 -0
- package/.pi/extensions/harness-plan-approval.ts +65 -15
- package/.pi/extensions/harness-plan-orchestration.ts +140 -0
- package/.pi/extensions/harness-run-context.ts +501 -48
- package/.pi/extensions/harness-telemetry.ts +1 -0
- package/.pi/extensions/harness-web-tools.ts +1 -0
- package/.pi/extensions/policy-gate.ts +9 -0
- package/.pi/extensions/trace-recorder.ts +1 -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 +14 -0
- package/.pi/harness/specs/harness-posthog-event.schema.json +2 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +1 -1
- package/.pi/lib/harness-auto-approve.ts +140 -0
- package/.pi/lib/harness-auto-compact-policy.ts +85 -0
- package/.pi/lib/harness-cocoindex-refresh.ts +82 -2
- package/.pi/lib/harness-phase-telemetry.ts +81 -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 +6 -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-stall-detector.ts +106 -0
- package/.pi/lib/harness-spawn-topology.ts +50 -1
- package/.pi/lib/harness-subagent-precheck.ts +41 -0
- package/.pi/lib/harness-subagent-progress.ts +119 -0
- package/.pi/lib/harness-subagent-timeout.ts +81 -0
- package/.pi/lib/harness-subagents-bridge.ts +94 -8
- package/.pi/lib/harness-ui-state.ts +5 -0
- 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 +16 -9
- package/.pi/lib/plan-debate-focus.ts +23 -11
- package/.pi/lib/plan-debate-gate.ts +94 -31
- package/.pi/lib/plan-debate-round-status.ts +23 -8
- package/.pi/lib/plan-debate-wall-clock.ts +57 -0
- 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 +26 -12
- package/.pi/scripts/harness-e2e-workflow.mjs +94 -0
- package/.pi/scripts/harness-project-toggle.mjs +1 -1
- package/.pi/scripts/harness-sentrux-cli.mjs +26 -1
- package/.pi/scripts/harness-sentrux-report.mjs +41 -6
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/vendor/pi-subagents/src/subagents.ts +41 -10
|
@@ -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",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../lib/ask-user/schema.js";
|
|
13
13
|
import type { AskUserParams } from "../lib/ask-user/types.js";
|
|
14
14
|
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
15
|
+
import { setHarnessWaitingForUser } from "../lib/harness-subagent-progress.js";
|
|
15
16
|
|
|
16
17
|
// @ts-expect-error pi extensions run as ESM
|
|
17
18
|
const MODULE_URL = import.meta.url;
|
|
@@ -28,11 +29,19 @@ export default function harnessAskUser(pi: ExtensionAPI) {
|
|
|
28
29
|
parameters: AskUserParamsSchema,
|
|
29
30
|
|
|
30
31
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
setHarnessWaitingForUser("ask_user");
|
|
33
|
+
pi.events.emit("harness-waiting-for-user", { gate: "ask_user" });
|
|
34
|
+
let result: Awaited<ReturnType<typeof runAskUser>>;
|
|
35
|
+
try {
|
|
36
|
+
result = await runAskUser(params as AskUserParams, {
|
|
37
|
+
ui: ctx.ui,
|
|
38
|
+
hasUI: ctx.hasUI,
|
|
39
|
+
sessionName: undefined,
|
|
40
|
+
});
|
|
41
|
+
} finally {
|
|
42
|
+
setHarnessWaitingForUser(null);
|
|
43
|
+
pi.events.emit("harness-waiting-for-user", { gate: null });
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
if ("error" in result) {
|
|
38
47
|
return {
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { mkdir, readFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { isHarnessNonInteractive } from "../lib/ask-user/policy.js";
|
|
8
9
|
import { claimHarnessGovernanceLoad } from "../lib/extension-load-guard.js";
|
|
9
10
|
import {
|
|
10
11
|
captureHarnessEvent,
|
|
@@ -50,7 +51,20 @@ import {
|
|
|
50
51
|
validateIntegratorDraft,
|
|
51
52
|
withReviewRoundYamlWrite,
|
|
52
53
|
} from "../lib/harness-debate-workflow-deps.js";
|
|
54
|
+
import {
|
|
55
|
+
loadPlanDebateEligibilitySnapshot,
|
|
56
|
+
writePlanDebateEligibilitySnapshot,
|
|
57
|
+
} from "../lib/plan-debate-eligibility-snapshot.js";
|
|
58
|
+
import {
|
|
59
|
+
checkDebateWallClock,
|
|
60
|
+
debateWallClockRecoveryHint,
|
|
61
|
+
} from "../lib/plan-debate-wall-clock.js";
|
|
62
|
+
import {
|
|
63
|
+
planReviewGateModeForProfile,
|
|
64
|
+
planReviewGateStrategyFromEligibility,
|
|
65
|
+
} from "../lib/plan-review-gate.js";
|
|
53
66
|
|
|
67
|
+
// @ts-expect-error pi extensions run as ESM
|
|
54
68
|
const MODULE_URL = import.meta.url;
|
|
55
69
|
|
|
56
70
|
function getRunId(ctx: {
|
|
@@ -231,6 +245,7 @@ function registerHarnessDebateHandler2(pi: ExtensionAPI) {
|
|
|
231
245
|
),
|
|
232
246
|
};
|
|
233
247
|
const result = harnessPlanDebateEligibility(input);
|
|
248
|
+
await writePlanDebateEligibilitySnapshot(rd, result);
|
|
234
249
|
const lines = [
|
|
235
250
|
`profile: ${result.profile}`,
|
|
236
251
|
`review_gate_mode: ${result.review_gate_strategy.mode}`,
|
|
@@ -266,6 +281,11 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
266
281
|
Type.String({ description: "spec | wbs | schedule | quality" }),
|
|
267
282
|
),
|
|
268
283
|
),
|
|
284
|
+
review_gate_mode: Type.Optional(
|
|
285
|
+
Type.String({
|
|
286
|
+
description: "consolidated | threaded | parallel_probes",
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
269
289
|
}),
|
|
270
290
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
271
291
|
const runId = getRunId(ctx);
|
|
@@ -274,6 +294,7 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
274
294
|
debate_id?: string;
|
|
275
295
|
debate_profile?: string;
|
|
276
296
|
required_focuses?: string[];
|
|
297
|
+
review_gate_mode?: string;
|
|
277
298
|
};
|
|
278
299
|
const raw = String(p.debate_id ?? "");
|
|
279
300
|
const { debateId, corrected, warning } = normalizePlanDebateId(
|
|
@@ -290,14 +311,23 @@ function registerHarnessDebateHandler3(pi: ExtensionAPI) {
|
|
|
290
311
|
const required_focuses = (p.required_focuses ?? []).filter((f) =>
|
|
291
312
|
["spec", "wbs", "schedule", "quality"].includes(f),
|
|
292
313
|
) as Array<"spec" | "wbs" | "schedule" | "quality">;
|
|
314
|
+
const rd = runDir(projectRoot, runId);
|
|
315
|
+
const eligibilitySnapshot = await loadPlanDebateEligibilitySnapshot(rd);
|
|
293
316
|
const opened = await openDebateBus(runId, debateId, debateHooks(pi), {
|
|
294
317
|
debate_profile: profile,
|
|
295
318
|
required_focuses:
|
|
296
319
|
required_focuses.length > 0 ? required_focuses : undefined,
|
|
297
320
|
});
|
|
321
|
+
const explicitMode = p.review_gate_mode;
|
|
298
322
|
const review_gate_mode =
|
|
299
|
-
|
|
300
|
-
|
|
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, {
|
|
301
331
|
runId,
|
|
302
332
|
debateId,
|
|
303
333
|
debate_profile: profile,
|
|
@@ -444,6 +474,24 @@ function registerHarnessDebateHandler6(pi: ExtensionAPI) {
|
|
|
444
474
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
445
475
|
const runId = getRunId(ctx);
|
|
446
476
|
const projectRoot = process.cwd();
|
|
477
|
+
const rd = runDir(projectRoot, runId);
|
|
478
|
+
const messenger = await loadMessengerState(rd);
|
|
479
|
+
const wall = checkDebateWallClock({
|
|
480
|
+
opened_at: messenger?.opened_at,
|
|
481
|
+
debate_profile: messenger?.debate_profile,
|
|
482
|
+
});
|
|
483
|
+
if (wall.exceeded && !isHarnessNonInteractive()) {
|
|
484
|
+
return {
|
|
485
|
+
content: [
|
|
486
|
+
{
|
|
487
|
+
type: "text",
|
|
488
|
+
text: debateWallClockRecoveryHint(wall),
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
details: { wall_clock: wall },
|
|
492
|
+
isError: true,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
447
495
|
const roundIndex = Number(
|
|
448
496
|
(params as { round_index: number }).round_index,
|
|
449
497
|
);
|
|
@@ -452,7 +500,6 @@ function registerHarnessDebateHandler6(pi: ExtensionAPI) {
|
|
|
452
500
|
draft.round_index = roundIndex;
|
|
453
501
|
if (!draft.schema_version) draft.schema_version = "1.0.0";
|
|
454
502
|
const debateId = planDebateIdForRun(runId);
|
|
455
|
-
const rd = runDir(projectRoot, runId);
|
|
456
503
|
const integratorBody =
|
|
457
504
|
(typeof draft.round_summary === "string" && draft.round_summary) ||
|
|
458
505
|
"Review integrator synthesis for this round.";
|
|
@@ -571,9 +618,17 @@ function registerHarnessDebateHandler7(pi: ExtensionAPI) {
|
|
|
571
618
|
}),
|
|
572
619
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
573
620
|
const runId = getRunId(ctx);
|
|
621
|
+
const projectRoot = process.cwd();
|
|
622
|
+
const messenger = await loadMessengerState(runDir(projectRoot, runId));
|
|
623
|
+
const wall = checkDebateWallClock({
|
|
624
|
+
opened_at: messenger?.opened_at,
|
|
625
|
+
debate_profile: messenger?.debate_profile,
|
|
626
|
+
});
|
|
574
627
|
const rationale =
|
|
575
628
|
String((params as { rationale?: string }).rationale ?? "").trim() ||
|
|
576
|
-
|
|
629
|
+
(wall.exceeded && isHarnessNonInteractive()
|
|
630
|
+
? "Debate truncated at wall-clock cap (non-interactive conditional_pass)."
|
|
631
|
+
: "Plan Review Gate consensus after focus coverage and messenger-backed rounds.");
|
|
577
632
|
const decision = await finalizeDebateConsensus(
|
|
578
633
|
rationale,
|
|
579
634
|
debateHooks(pi),
|
|
@@ -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,
|
|
@@ -311,6 +312,14 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
311
312
|
if (mountCtx) scheduleRefresh(mountCtx);
|
|
312
313
|
});
|
|
313
314
|
|
|
315
|
+
pi.events.on("harness-progress:updated", () => {
|
|
316
|
+
if (mountCtx) scheduleProgressRefresh(mountCtx);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.events.on("harness-waiting-for-user", () => {
|
|
320
|
+
if (mountCtx) scheduleProgressRefresh(mountCtx);
|
|
321
|
+
});
|
|
322
|
+
|
|
314
323
|
pi.events.on("harness-cross-session-resume", (payload: unknown) => {
|
|
315
324
|
const data =
|
|
316
325
|
payload && typeof payload === "object"
|
|
@@ -358,9 +367,25 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
|
|
|
358
367
|
flowSubstate: state.flowSubstate,
|
|
359
368
|
nextRecommendedCommand: state.nextRecommendedCommand,
|
|
360
369
|
crossSessionResumeCommand: state.crossSessionResumeCommand,
|
|
370
|
+
progressLine: buildHarnessProgressStatusLine(),
|
|
361
371
|
});
|
|
362
372
|
}
|
|
363
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
|
+
|
|
364
389
|
function scheduleRefresh(ctx: ExtensionContext): void {
|
|
365
390
|
if (refreshQueued) return;
|
|
366
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,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
parsePlanApprovalFromMessage,
|
|
14
16
|
planPacketSummary,
|
|
15
17
|
} from "../lib/harness-run-context.js";
|
|
18
|
+
import { setHarnessWaitingForUser } from "../lib/harness-subagent-progress.js";
|
|
16
19
|
import {
|
|
17
20
|
CREATE_PLAN_GUIDELINES,
|
|
18
21
|
CREATE_PLAN_SNIPPET,
|
|
@@ -46,6 +49,7 @@ import {
|
|
|
46
49
|
validateApprovePlanParams,
|
|
47
50
|
} from "../lib/plan-approval/validate.js";
|
|
48
51
|
import { validatePlanApprovalReadiness } from "../lib/plan-approval-readiness.js";
|
|
52
|
+
import { loadPlanDebateEligibilitySnapshot } from "../lib/plan-debate-eligibility-snapshot.js";
|
|
49
53
|
import { validatePlanDebateGate } from "../lib/plan-debate-gate.js";
|
|
50
54
|
|
|
51
55
|
// @ts-expect-error pi extensions run as ESM
|
|
@@ -165,40 +169,58 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
165
169
|
const risk = String(
|
|
166
170
|
validated.plan_packet.risk_level ?? "med",
|
|
167
171
|
).toLowerCase();
|
|
172
|
+
let readinessResult: Awaited<
|
|
173
|
+
ReturnType<typeof validatePlanApprovalReadiness>
|
|
174
|
+
> | null = null;
|
|
175
|
+
let debateGateResult: Awaited<
|
|
176
|
+
ReturnType<typeof validatePlanDebateGate>
|
|
177
|
+
> | null = null;
|
|
168
178
|
if (runCtx?.run_id) {
|
|
169
|
-
|
|
179
|
+
readinessResult = await validatePlanApprovalReadiness(
|
|
170
180
|
projectRoot,
|
|
171
181
|
runCtx.run_id,
|
|
172
182
|
{ risk_level: risk },
|
|
173
183
|
);
|
|
174
|
-
if (!
|
|
184
|
+
if (!readinessResult.ok) {
|
|
175
185
|
return {
|
|
176
186
|
content: [
|
|
177
187
|
{
|
|
178
188
|
type: "text",
|
|
179
|
-
text: `approve_plan blocked — plan phase not ready:\n- ${
|
|
189
|
+
text: `approve_plan blocked — plan phase not ready:\n- ${readinessResult.errors.join("\n- ")}`,
|
|
180
190
|
},
|
|
181
191
|
],
|
|
182
192
|
details: {
|
|
183
193
|
plan_packet: validated.plan_packet,
|
|
184
|
-
readiness,
|
|
194
|
+
readiness: readinessResult,
|
|
185
195
|
cancelled: true,
|
|
186
196
|
},
|
|
187
197
|
isError: true,
|
|
188
198
|
};
|
|
189
199
|
}
|
|
190
|
-
implWarnings.push(...
|
|
200
|
+
implWarnings.push(...readinessResult.warnings);
|
|
191
201
|
}
|
|
192
202
|
if (runCtx?.run_id) {
|
|
193
|
-
const
|
|
194
|
-
|
|
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) {
|
|
195
217
|
const { buildPlanDebateGateRecovery } = await import(
|
|
196
218
|
"../lib/plan-debate-gate.js"
|
|
197
219
|
);
|
|
198
220
|
const recovery = await buildPlanDebateGateRecovery(
|
|
199
221
|
projectRoot,
|
|
200
222
|
runCtx.run_id,
|
|
201
|
-
|
|
223
|
+
debateGateResult,
|
|
202
224
|
);
|
|
203
225
|
return {
|
|
204
226
|
content: [
|
|
@@ -209,13 +231,24 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
209
231
|
],
|
|
210
232
|
details: {
|
|
211
233
|
plan_packet: validated.plan_packet,
|
|
212
|
-
debate_gate:
|
|
234
|
+
debate_gate: debateGateResult,
|
|
213
235
|
cancelled: true,
|
|
214
236
|
},
|
|
215
237
|
isError: true,
|
|
216
238
|
};
|
|
217
239
|
}
|
|
218
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
|
+
|
|
219
252
|
const reviewPath = await writePlanReviewMarkdown(
|
|
220
253
|
projectRoot,
|
|
221
254
|
runCtx,
|
|
@@ -223,7 +256,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
223
256
|
{
|
|
224
257
|
human_summary: validated.human_summary,
|
|
225
258
|
research_brief: validated.research_brief,
|
|
226
|
-
status: "draft",
|
|
259
|
+
status: autoOutcome.approved ? "approved" : "draft",
|
|
227
260
|
},
|
|
228
261
|
);
|
|
229
262
|
const planMarkdown = buildPlanApprovalMarkdown(validated);
|
|
@@ -246,11 +279,28 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
246
279
|
},
|
|
247
280
|
});
|
|
248
281
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
let outcome: PlanApprovalDialogResult;
|
|
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
|
+
}
|
|
303
|
+
}
|
|
254
304
|
|
|
255
305
|
const details = toApprovePlanToolDetails(
|
|
256
306
|
validated,
|
|
@@ -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
|
+
}
|