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.
Files changed (48) hide show
  1. package/.pi/extensions/agt-prompt-guard.ts +20 -6
  2. package/.pi/extensions/harness-auto-compact.ts +94 -0
  3. package/.pi/extensions/harness-debate-tools.ts +26 -2
  4. package/.pi/extensions/harness-live-widget.ts +19 -2
  5. package/.pi/extensions/harness-plan-approval.ts +62 -19
  6. package/.pi/extensions/harness-plan-orchestration.ts +140 -0
  7. package/.pi/extensions/harness-run-context.ts +457 -48
  8. package/.pi/extensions/harness-web-tools.ts +1 -0
  9. package/.pi/extensions/policy-gate.ts +9 -0
  10. package/.pi/harness/agents.manifest.json +1 -1
  11. package/.pi/harness/docs/adrs/0056-agent-native-speed-wiring.md +26 -0
  12. package/.pi/harness/env.harness.template +7 -1
  13. package/.pi/lib/harness-auto-approve.ts +140 -0
  14. package/.pi/lib/harness-auto-compact-policy.ts +85 -0
  15. package/.pi/lib/harness-phase-telemetry.ts +7 -0
  16. package/.pi/lib/harness-phase-worker.ts +23 -0
  17. package/.pi/lib/harness-plan-fsm.ts +162 -0
  18. package/.pi/lib/harness-plan-route.ts +134 -0
  19. package/.pi/lib/harness-posthog.ts +4 -1
  20. package/.pi/lib/harness-remediation.ts +79 -0
  21. package/.pi/lib/harness-repair-brief.ts +2 -2
  22. package/.pi/lib/harness-review-parallel.ts +18 -0
  23. package/.pi/lib/harness-run-context.ts +119 -72
  24. package/.pi/lib/harness-spawn-budget.ts +32 -4
  25. package/.pi/lib/harness-spawn-topology.ts +36 -1
  26. package/.pi/lib/harness-subagent-precheck.ts +3 -2
  27. package/.pi/lib/harness-subagent-progress.ts +8 -5
  28. package/.pi/lib/harness-subagents-bridge.ts +14 -12
  29. package/.pi/lib/harness-vcc-settings.ts +36 -0
  30. package/.pi/lib/plan-approval-readiness.ts +9 -5
  31. package/.pi/lib/plan-debate-eligibility-snapshot.ts +90 -0
  32. package/.pi/lib/plan-debate-eligibility.ts +12 -7
  33. package/.pi/lib/plan-debate-focus.ts +23 -11
  34. package/.pi/lib/plan-debate-gate.ts +71 -29
  35. package/.pi/lib/plan-debate-round-status.ts +23 -8
  36. package/.pi/lib/plan-headless-ux.ts +598 -0
  37. package/.pi/lib/plan-human-gates.ts +24 -85
  38. package/.pi/lib/plan-messenger.ts +3 -3
  39. package/.pi/lib/plan-review-gate.ts +56 -0
  40. package/.pi/prompts/harness-abort.md +1 -0
  41. package/.pi/prompts/harness-auto.md +1 -1
  42. package/.pi/prompts/harness-clear.md +6 -6
  43. package/.pi/prompts/harness-plan.md +15 -2
  44. package/.pi/prompts/harness-review.md +2 -2
  45. package/.pi/scripts/harness-project-toggle.mjs +1 -1
  46. package/CHANGELOG.md +10 -0
  47. package/README.md +2 -2
  48. 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 { userVisiblePromptSlice } from "../lib/harness-run-context.js";
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 prompt = userVisiblePromptSlice(event.prompt);
17
- if (!prompt.trim()) return undefined;
18
- if (!/\/harness-/.test(prompt)) return undefined;
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(prompt);
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
- profile === "fast" ? ("consolidated" as const) : ("threaded" as const);
306
- await initPlanMessenger(runDir(projectRoot, runId), {
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) scheduleRefresh(mountCtx);
316
+ if (mountCtx) scheduleProgressRefresh(mountCtx);
316
317
  });
317
318
 
318
319
  pi.events.on("harness-waiting-for-user", () => {
319
- if (mountCtx) scheduleRefresh(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
- const readiness = await validatePlanApprovalReadiness(
179
+ readinessResult = await validatePlanApprovalReadiness(
171
180
  projectRoot,
172
181
  runCtx.run_id,
173
182
  { risk_level: risk },
174
183
  );
175
- if (!readiness.ok) {
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- ${readiness.errors.join("\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(...readiness.warnings);
200
+ implWarnings.push(...readinessResult.warnings);
192
201
  }
193
202
  if (runCtx?.run_id) {
194
- const gate = await validatePlanDebateGate(projectRoot, runCtx.run_id);
195
- if (!gate.ok) {
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
- gate,
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: 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
- try {
254
- outcome = await runPlanApprovalDialog(ctx.ui, validated, {
255
- hasUI: ctx.hasUI,
256
- });
257
- } finally {
258
- setHarnessWaitingForUser(null);
259
- pi.events.emit("harness-waiting-for-user", { gate: null });
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
+ }