ultimate-pi 0.11.0 → 0.12.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 (122) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
  2. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -28
  4. package/.agents/skills/harness-plan/SKILL.md +15 -20
  5. package/.pi/agents/harness/adversary.md +0 -1
  6. package/.pi/agents/harness/evaluator.md +0 -1
  7. package/.pi/agents/harness/executor.md +1 -2
  8. package/.pi/agents/harness/incident-recorder.md +0 -1
  9. package/.pi/agents/harness/meta-optimizer.md +0 -1
  10. package/.pi/agents/harness/planning/decompose.md +3 -4
  11. package/.pi/agents/harness/planning/execution-plan-author.md +30 -0
  12. package/.pi/agents/harness/planning/hypothesis-validator.md +23 -0
  13. package/.pi/agents/harness/planning/hypothesis.md +3 -4
  14. package/.pi/agents/harness/planning/plan-adversary.md +10 -42
  15. package/.pi/agents/harness/planning/plan-evaluator.md +18 -0
  16. package/.pi/agents/harness/planning/review-integrator.md +23 -0
  17. package/.pi/agents/harness/planning/scout-graphify.md +11 -5
  18. package/.pi/agents/harness/planning/scout-semantic.md +11 -6
  19. package/.pi/agents/harness/planning/scout-structure.md +12 -6
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +18 -0
  21. package/.pi/agents/harness/planning/stack-researcher.md +24 -0
  22. package/.pi/agents/harness/tie-breaker.md +0 -1
  23. package/.pi/agents/harness/trace-librarian.md +0 -1
  24. package/.pi/extensions/debate-orchestrator.ts +90 -53
  25. package/.pi/extensions/harness-plan-approval.ts +2 -2
  26. package/.pi/extensions/harness-run-context.ts +145 -5
  27. package/.pi/extensions/harness-subagents.ts +2 -2
  28. package/.pi/extensions/lib/harness-posthog.ts +6 -1
  29. package/.pi/extensions/lib/harness-spawn-budget.ts +75 -0
  30. package/.pi/extensions/lib/harness-subagent-auth.ts +123 -0
  31. package/.pi/extensions/lib/{harness-subagents/harness-subagent-policy.ts → harness-subagent-policy.ts} +3 -6
  32. package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
  33. package/.pi/extensions/lib/harness-subagents-bridge.ts +176 -0
  34. package/.pi/extensions/lib/plan-approval/create-plan.ts +4 -7
  35. package/.pi/extensions/lib/plan-approval/plan-review.ts +1 -1
  36. package/.pi/extensions/lib/plan-approval/types.ts +7 -1
  37. package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
  38. package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +1 -0
  39. package/.pi/extensions/policy-gate.ts +1 -1
  40. package/.pi/extensions/review-integrity.ts +48 -29
  41. package/.pi/harness/agents.manifest.json +37 -25
  42. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +4 -3
  43. package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +1 -1
  44. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -0
  45. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r1.yaml +25 -0
  46. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r4.yaml +26 -0
  47. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/sprint-audit-r4.yaml +5 -0
  48. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-packet.yaml +196 -0
  49. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-review.md +14 -0
  50. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +32 -0
  51. package/.pi/harness/evals/smoke/run-context.fixture.json +1 -1
  52. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +88 -0
  53. package/.pi/harness/specs/harness-posthog-event.schema.json +6 -1
  54. package/.pi/harness/specs/plan-execution-plan-brief.schema.json +13 -0
  55. package/.pi/harness/specs/plan-execution-plan.schema.json +255 -0
  56. package/.pi/harness/specs/plan-packet.schema.json +14 -5
  57. package/.pi/harness/specs/plan-review-round-draft.schema.json +68 -0
  58. package/.pi/harness/specs/plan-sprint-audit-turn.schema.json +29 -0
  59. package/.pi/harness/specs/plan-stack-brief.schema.json +65 -0
  60. package/.pi/harness/specs/plan-validation-turn.schema.json +42 -0
  61. package/.pi/harness/specs/round-result.schema.json +16 -9
  62. package/.pi/lib/debate-orchestrator-types.ts +38 -0
  63. package/.pi/lib/harness-agent-discovery.mjs +81 -0
  64. package/.pi/lib/harness-run-context.ts +64 -38
  65. package/.pi/lib/harness-yaml.mjs +73 -0
  66. package/.pi/lib/harness-yaml.ts +90 -0
  67. package/.pi/prompts/harness-auto.md +13 -11
  68. package/.pi/prompts/harness-critic.md +2 -2
  69. package/.pi/prompts/harness-eval.md +3 -3
  70. package/.pi/prompts/harness-incident.md +2 -2
  71. package/.pi/prompts/harness-plan.md +79 -93
  72. package/.pi/prompts/harness-review.md +2 -2
  73. package/.pi/prompts/harness-router-tune.md +1 -1
  74. package/.pi/prompts/harness-run.md +2 -2
  75. package/.pi/prompts/harness-setup.md +15 -6
  76. package/.pi/prompts/harness-trace.md +2 -2
  77. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  78. package/.pi/scripts/harness-verify.mjs +28 -19
  79. package/.pi/scripts/validate-plan-dag.mjs +258 -0
  80. package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
  81. package/CHANGELOG.md +12 -0
  82. package/THIRD_PARTY_NOTICES.md +8 -0
  83. package/biome.json +2 -2
  84. package/package.json +6 -4
  85. package/.pi/agents/harness/planner.md +0 -13
  86. package/.pi/agents/harness/planning/hypothesis-eval.md +0 -59
  87. package/.pi/agents/harness/planning/planner.md +0 -20
  88. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +0 -126
  89. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +0 -119
  90. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +0 -87
  91. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +0 -118
  92. package/.pi/extensions/lib/harness-subagents/blackboard.ts +0 -175
  93. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +0 -10
  94. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +0 -137
  95. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -77
  96. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +0 -27
  97. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +0 -558
  98. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -666
  99. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +0 -175
  100. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +0 -59
  101. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +0 -134
  102. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +0 -5
  103. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +0 -123
  104. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +0 -43
  105. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +0 -144
  106. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +0 -2460
  107. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +0 -52
  108. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +0 -182
  109. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +0 -92
  110. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +0 -115
  111. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +0 -103
  112. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +0 -177
  113. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +0 -416
  114. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +0 -210
  115. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +0 -108
  116. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +0 -187
  117. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +0 -639
  118. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +0 -324
  119. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +0 -110
  120. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +0 -71
  121. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +0 -195
  122. /package/.pi/extensions/{00-ultimate-pi-system-prompt.ts → custom-system-prompt.ts} +0 -0
@@ -14,16 +14,20 @@
14
14
  * }
15
15
  */
16
16
 
17
- import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
17
+ import { appendFile, mkdir, writeFile } from "node:fs/promises";
18
18
  import { join } from "node:path";
19
19
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+ import {
21
+ type DebateParticipant,
22
+ debatePhaseFromId,
23
+ isPlanDebateId,
24
+ PLAN_DEBATE_PARTICIPANTS,
25
+ POST_EXECUTE_DEBATE_PARTICIPANTS,
26
+ } from "../lib/debate-orchestrator-types.js";
20
27
  import { getRunIdFromSession } from "../lib/harness-run-context.js";
21
28
 
22
- type DebateParticipant =
23
- | "EvaluatorAgent"
24
- | "AdversaryAgent"
25
- | "TieBreakerAgent";
26
29
  type PolicyDecision = "pass" | "conditional_pass" | "block" | "human_required";
30
+ type DebatePhase = "plan" | "post_execute";
27
31
 
28
32
  interface RoundPayload {
29
33
  participants: DebateParticipant[];
@@ -46,11 +50,13 @@ interface RoundPayload {
46
50
  interface DebateState {
47
51
  run_id: string;
48
52
  debate_id: string;
53
+ debate_phase: DebatePhase;
49
54
  round_count: number;
50
55
  budget_used: number;
51
56
  max_rounds: number;
52
57
  round_token_cap: number;
53
58
  debate_global_cap: number;
59
+ last_review_gate_ready?: boolean;
54
60
  }
55
61
 
56
62
  interface BusEnvelope<T = unknown> {
@@ -104,46 +110,39 @@ function getRunId(ctx: {
104
110
  );
105
111
  }
106
112
 
107
- async function readRoundCapsFromSchema(): Promise<{
113
+ const PLAN_BUDGET = {
114
+ max_rounds: 4,
115
+ round_token_cap: 2000,
116
+ debate_global_cap: 12000,
117
+ } as const;
118
+
119
+ const AGGRESSIVE_BUDGET = {
120
+ max_rounds: 6,
121
+ round_token_cap: 2500,
122
+ debate_global_cap: 35000,
123
+ } as const;
124
+
125
+ function capsForDebate(debateId: string): {
126
+ name: "plan" | "aggressive";
108
127
  max_rounds: number;
109
128
  round_token_cap: number;
110
129
  debate_global_cap: number;
111
- }> {
112
- try {
113
- const roundSchemaPath = join(
114
- process.cwd(),
115
- ".pi",
116
- "harness",
117
- "specs",
118
- "round-result.schema.json",
130
+ } {
131
+ if (isPlanDebateId(debateId)) {
132
+ return { name: "plan", ...PLAN_BUDGET };
133
+ }
134
+ return { name: "aggressive", ...AGGRESSIVE_BUDGET };
135
+ }
136
+
137
+ function participantAllowed(participant: string, phase: DebatePhase): boolean {
138
+ if (phase === "plan") {
139
+ return (PLAN_DEBATE_PARTICIPANTS as readonly string[]).includes(
140
+ participant,
119
141
  );
120
- const parsed = JSON.parse(await readFile(roundSchemaPath, "utf-8")) as {
121
- properties?: {
122
- budget_profile?: {
123
- properties?: {
124
- max_rounds?: { const?: number };
125
- round_token_cap?: { const?: number };
126
- debate_global_cap?: { const?: number };
127
- };
128
- };
129
- };
130
- };
131
- return {
132
- max_rounds: Number(
133
- parsed?.properties?.budget_profile?.properties?.max_rounds?.const ?? 6,
134
- ),
135
- round_token_cap: Number(
136
- parsed?.properties?.budget_profile?.properties?.round_token_cap
137
- ?.const ?? 2500,
138
- ),
139
- debate_global_cap: Number(
140
- parsed?.properties?.budget_profile?.properties?.debate_global_cap
141
- ?.const ?? 35000,
142
- ),
143
- };
144
- } catch {
145
- return { max_rounds: 6, round_token_cap: 2500, debate_global_cap: 35000 };
146
142
  }
143
+ return (POST_EXECUTE_DEBATE_PARTICIPANTS as readonly string[]).includes(
144
+ participant,
145
+ );
147
146
  }
148
147
 
149
148
  async function writeDebateEvent(
@@ -197,13 +196,18 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
197
196
  let lastSeverity = defaultSeverity();
198
197
 
199
198
  async function openDebate(runId: string, debateId: string): Promise<void> {
200
- const caps = await readRoundCapsFromSchema();
199
+ const caps = capsForDebate(debateId);
200
+ const debate_phase = debatePhaseFromId(debateId);
201
201
  state = {
202
202
  run_id: runId,
203
203
  debate_id: debateId,
204
+ debate_phase,
204
205
  round_count: 0,
205
206
  budget_used: 0,
206
- ...caps,
207
+ max_rounds: caps.max_rounds,
208
+ round_token_cap: caps.round_token_cap,
209
+ debate_global_cap: caps.debate_global_cap,
210
+ last_review_gate_ready: false,
207
211
  };
208
212
  pi.appendEntry("harness-debate-state", state);
209
213
  const envelope: BusEnvelope = {
@@ -216,7 +220,8 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
216
220
  },
217
221
  payload: {
218
222
  opened_at: nowIso(),
219
- budget_profile: "aggressive",
223
+ debate_phase,
224
+ budget_profile: caps.name,
220
225
  },
221
226
  };
222
227
  pi.appendEntry("harness-debate-envelope", envelope);
@@ -267,6 +272,15 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
267
272
  return { ok: false, reason: "debate id mismatch" };
268
273
  }
269
274
 
275
+ for (const p of envelope.payload.participants ?? []) {
276
+ if (!participantAllowed(p, state.debate_phase)) {
277
+ return {
278
+ ok: false,
279
+ reason: `participant ${p} invalid for debate_phase=${state.debate_phase}`,
280
+ };
281
+ }
282
+ }
283
+
270
284
  const nextRound = state.round_count + 1;
271
285
  if (nextRound > state.max_rounds) {
272
286
  await emitBudgetExhausted("max_rounds_reached");
@@ -310,6 +324,11 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
310
324
  };
311
325
  }
312
326
 
327
+ const profileName =
328
+ state.debate_phase === "plan"
329
+ ? ("plan" as const)
330
+ : ("aggressive" as const);
331
+
313
332
  const roundRecord = {
314
333
  schema_version: "1.0.0",
315
334
  contract_version: "1.0.0",
@@ -322,7 +341,7 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
322
341
  evidence_refs: envelope.payload.evidence_refs,
323
342
  token_usage: envelope.payload.token_usage,
324
343
  budget_profile: {
325
- name: "aggressive",
344
+ name: profileName,
326
345
  max_rounds: state.max_rounds,
327
346
  round_token_cap: state.round_token_cap,
328
347
  debate_global_cap: state.debate_global_cap,
@@ -354,12 +373,20 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
354
373
  ),
355
374
  );
356
375
  const decision = decidePolicy(lastSeverity, evidenceScore);
376
+ const planPhase = state.debate_phase === "plan";
377
+ const evaluatorPassed = planPhase
378
+ ? Boolean(state.last_review_gate_ready)
379
+ : true;
380
+ const debateComplete = planPhase
381
+ ? state.round_count >= state.max_rounds
382
+ : state.round_count > 0;
357
383
 
358
384
  const consensus = {
359
385
  schema_version: "1.0.0",
360
386
  contract_version: "1.0.0",
361
387
  run_id: state.run_id,
362
388
  debate_id: state.debate_id,
389
+ debate_phase: state.debate_phase,
363
390
  round_count: state.round_count,
364
391
  budget_used: state.budget_used,
365
392
  severity_scores: lastSeverity,
@@ -371,15 +398,25 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
371
398
  },
372
399
  confidence_weights: WEIGHTS,
373
400
  evidence_refs: [],
374
- strict_gate_prerequisites: {
375
- plan_gate_passed: true,
376
- execution_completed: true,
377
- evaluator_passed: true,
378
- adversarial_debate_completed: state.round_count > 0,
379
- severity_policy_ok: decision !== "block",
380
- benchmark_delta_checks_passed: false,
381
- rollback_artifacts_generated: false,
382
- },
401
+ strict_gate_prerequisites: planPhase
402
+ ? {
403
+ plan_gate_passed: false,
404
+ execution_completed: false,
405
+ evaluator_passed: evaluatorPassed,
406
+ adversarial_debate_completed: debateComplete,
407
+ severity_policy_ok: decision !== "block",
408
+ benchmark_delta_checks_passed: false,
409
+ rollback_artifacts_generated: false,
410
+ }
411
+ : {
412
+ plan_gate_passed: true,
413
+ execution_completed: true,
414
+ evaluator_passed: true,
415
+ adversarial_debate_completed: debateComplete,
416
+ severity_policy_ok: decision !== "block",
417
+ benchmark_delta_checks_passed: false,
418
+ rollback_artifacts_generated: false,
419
+ },
383
420
  policy_decision: decision,
384
421
  rationale,
385
422
  };
@@ -236,7 +236,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
236
236
  name: "create_plan",
237
237
  label: "Create Plan",
238
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.",
239
+ "Write the approved PlanPacket to plan-packet.yaml for this harness run. Call only after approve_plan (Approve). Do not use write/edit.",
240
240
  promptSnippet: CREATE_PLAN_SNIPPET,
241
241
  promptGuidelines: CREATE_PLAN_GUIDELINES,
242
242
  parameters: CreatePlanParamsSchema,
@@ -298,7 +298,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
298
298
  return new Text(
299
299
  theme.fg(
300
300
  "success",
301
- `Wrote ${details?.plan_path ?? "plan-packet.json"}`,
301
+ `Wrote ${details?.plan_path ?? "plan-packet.yaml"}`,
302
302
  ),
303
303
  0,
304
304
  0,
@@ -5,13 +5,16 @@
5
5
  * in before_agent_start so trace-recorder reuses it on agent_start.
6
6
  */
7
7
 
8
- import { readFile, writeFile } from "node:fs/promises";
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { dirname } from "node:path";
9
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { Type } from "@sinclair/typebox";
10
12
  import {
11
13
  canonicalPlanPath,
12
14
  createFreshRunContext,
13
15
  driftGateActive,
14
16
  extractCompletionStatuses,
17
+ extractWritePathFromToolInput,
15
18
  formatActivePlanBlock,
16
19
  formatPlanContextBlock,
17
20
  getLatestHarnessTurn,
@@ -27,10 +30,12 @@ import {
27
30
  isHarnessBootstrapPrompt,
28
31
  isNewTaskPlanBlocked,
29
32
  isPlanApprovalAskUser,
33
+ isPlanPhaseScopedWrite,
30
34
  isStaleActiveRunPointer,
31
35
  loadProjectActiveRun,
32
36
  loadRunContextFromDisk,
33
37
  nextStepAfterOutcome,
38
+ normalizeHarnessPath,
34
39
  nowIso,
35
40
  type PlanPacketSummary,
36
41
  parseHarnessSlashInput,
@@ -45,6 +50,11 @@ import {
45
50
  validatePlanOverridePath,
46
51
  validatePlanPacket,
47
52
  } from "../lib/harness-run-context.js";
53
+ import {
54
+ normalizeHarnessYamlContent,
55
+ parseStructuredDocument,
56
+ writeYamlFile,
57
+ } from "../lib/harness-yaml.js";
48
58
 
49
59
  interface SessionEntryLike {
50
60
  type?: string;
@@ -84,6 +94,32 @@ function appendHarnessTurn(pi: ExtensionAPI, turn: HarnessTurnEntry): void {
84
94
  });
85
95
  }
86
96
 
97
+ async function coerceScopedHarnessYamlWrite(
98
+ event: { toolName: string; input: Record<string, unknown> },
99
+ runCtx: HarnessRunContext,
100
+ projectRoot: string,
101
+ ): Promise<{ block: true; reason: string } | undefined> {
102
+ if (event.toolName !== "write") return undefined;
103
+ const target = extractWritePathFromToolInput(event.input);
104
+ if (!target.endsWith(".yaml") && !target.endsWith(".yml")) return undefined;
105
+ const scoped = await isPlanPhaseScopedWrite(target, runCtx, projectRoot);
106
+ if (!scoped) return undefined;
107
+ const content = event.input.content;
108
+ if (typeof content !== "string") return undefined;
109
+ try {
110
+ event.input.content = normalizeHarnessYamlContent(content, target);
111
+ } catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ return {
114
+ block: true,
115
+ reason:
116
+ `harness-run-context: ${target} must be canonical YAML, not embedded JSON. ` +
117
+ `Use write_harness_yaml with the subagent JSON/YAML block, or paste valid YAML. (${msg})`,
118
+ };
119
+ }
120
+ return undefined;
121
+ }
122
+
87
123
  function syncPolicyFromPlan(
88
124
  pi: ExtensionAPI,
89
125
  entries: unknown[],
@@ -583,7 +619,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
583
619
  activeCtx.last_outcome = "needs_clarification";
584
620
  activeCtx.last_completed_step = "plan";
585
621
  const msg =
586
- "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.json.";
622
+ "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
587
623
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
588
624
  else
589
625
  pi.sendMessage({
@@ -671,6 +707,18 @@ export default function harnessRunContext(pi: ExtensionAPI) {
671
707
  });
672
708
 
673
709
  pi.on("tool_call", async (event, ctx) => {
710
+ if (event.toolName === "write") {
711
+ const entries = getEntries(ctx);
712
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
713
+ if (runCtx) {
714
+ const blocked = await coerceScopedHarnessYamlWrite(
715
+ event,
716
+ runCtx,
717
+ process.cwd(),
718
+ );
719
+ if (blocked) return blocked;
720
+ }
721
+ }
674
722
  if (activeCtx?.plan_packet_path) {
675
723
  const entries = getEntries(ctx);
676
724
  if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
@@ -707,11 +755,11 @@ export default function harnessRunContext(pi: ExtensionAPI) {
707
755
  (event.input as { filePath?: string }).filePath ??
708
756
  "",
709
757
  );
710
- if (target.includes("plan-packet.json")) {
758
+ if (target.includes("plan-packet.yaml")) {
711
759
  return {
712
760
  block: true,
713
761
  reason:
714
- "harness-run-context: plan-packet.json is read-only in evaluate/adversary phases.",
762
+ "harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
715
763
  };
716
764
  }
717
765
  return undefined;
@@ -792,7 +840,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
792
840
 
793
841
  pi.registerCommand("harness-plan-commit", {
794
842
  description:
795
- "Write approved plan-packet.json to the active run (requires harness-plan-approval)",
843
+ "Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
796
844
  handler: async (args, ctx) => {
797
845
  const projectRoot = process.cwd();
798
846
  const entries = getEntries(ctx);
@@ -867,6 +915,98 @@ export default function harnessRunContext(pi: ExtensionAPI) {
867
915
  },
868
916
  });
869
917
 
918
+ pi.registerTool({
919
+ name: "write_harness_yaml",
920
+ label: "Write Harness YAML",
921
+ description:
922
+ "Write a plan-phase harness artifact as canonical YAML (parses subagent JSON or YAML, never embeds JSON in .yaml files).",
923
+ promptSnippet:
924
+ "Persist plan artifacts (decomposition, hypothesis, stack, review rounds) as real YAML.",
925
+ promptGuidelines: [
926
+ "Use write_harness_yaml for all artifacts/*.yaml and research-brief.yaml updates during /harness-plan.",
927
+ "Pass the subagent fenced json or yaml block as content; the tool converts to YAML on disk.",
928
+ "Do not use write with stringified JSON for .yaml paths.",
929
+ "plan-packet.yaml after approval: prefer create_plan; write_harness_yaml is for drafts and side artifacts only.",
930
+ ],
931
+ parameters: Type.Object({
932
+ path: Type.String({
933
+ description:
934
+ "Path under the active run, e.g. artifacts/decomposition.yaml or research-brief.yaml",
935
+ }),
936
+ content: Type.String({
937
+ description:
938
+ "YAML or JSON document (fenced or raw) matching the artifact schema",
939
+ }),
940
+ }),
941
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
942
+ const entries = getEntries(ctx);
943
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
944
+ if (!runCtx?.run_id) {
945
+ return {
946
+ content: [
947
+ {
948
+ type: "text",
949
+ text: 'No active harness run. Run /harness-plan "<task>" first.',
950
+ },
951
+ ],
952
+ details: {},
953
+ isError: true,
954
+ };
955
+ }
956
+ const pathArg = String((params as { path?: string }).path ?? "").trim();
957
+ const content = String((params as { content?: string }).content ?? "");
958
+ if (!pathArg || !content.trim()) {
959
+ return {
960
+ content: [
961
+ {
962
+ type: "text",
963
+ text: "write_harness_yaml requires path and content.",
964
+ },
965
+ ],
966
+ details: {},
967
+ isError: true,
968
+ };
969
+ }
970
+ const projectRoot = process.cwd();
971
+ const absPath = normalizeHarnessPath(pathArg, projectRoot);
972
+ const scoped = await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot);
973
+ if (!scoped) {
974
+ return {
975
+ content: [
976
+ {
977
+ type: "text",
978
+ text: `Path not allowed: ${pathArg}. Must be under .pi/harness/runs/${runCtx.run_id}/ (artifacts/*.yaml, research-brief.yaml, etc.).`,
979
+ },
980
+ ],
981
+ details: { path: pathArg },
982
+ isError: true,
983
+ };
984
+ }
985
+ let doc: unknown;
986
+ try {
987
+ doc = parseStructuredDocument(content, pathArg);
988
+ } catch (err) {
989
+ const msg = err instanceof Error ? err.message : String(err);
990
+ return {
991
+ content: [{ type: "text", text: msg }],
992
+ details: { path: pathArg },
993
+ isError: true,
994
+ };
995
+ }
996
+ await mkdir(dirname(absPath), { recursive: true });
997
+ await writeYamlFile(absPath, doc);
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: `Wrote ${pathArg} as canonical YAML.`,
1003
+ },
1004
+ ],
1005
+ details: { path: absPath },
1006
+ };
1007
+ },
1008
+ });
1009
+
870
1010
  pi.registerCommand("harness-use-run", {
871
1011
  description: "Point this session at an existing run directory (recovery)",
872
1012
  handler: async (args, ctx) => {
@@ -1,10 +1,10 @@
1
1
  /**
2
- * harness-subagents — package-resolved agents, blackboard, observation-bus handoffs.
2
+ * harness-subagents — vendored pi-subagents with ultimate-pi discovery and policy gates.
3
3
  */
4
4
 
5
5
  import { claimExtensionLoad } from "./lib/extension-load-guard.js";
6
6
  import { getHarnessPackageRoot } from "./lib/harness-paths.js";
7
- import { createHarnessSubagentsExtension } from "./lib/harness-subagents/vendored/index.js";
7
+ import { createHarnessSubagentsExtension } from "./lib/harness-subagents-bridge.js";
8
8
 
9
9
  // @ts-expect-error pi extensions run as ESM
10
10
  const MODULE_URL = import.meta.url;
@@ -22,7 +22,12 @@ export type HarnessPostHogEventName =
22
22
  | "harness_drift_report"
23
23
  | "harness_eval_verdict"
24
24
  | "harness_sentrux_signal"
25
- | "harness_observation";
25
+ | "harness_observation"
26
+ | "harness_subagent_spawned"
27
+ | "harness_subagent_completed"
28
+ | "harness_subagent_result_wait"
29
+ | "harness_subagent_setup"
30
+ | "harness_blackboard_op";
26
31
 
27
32
  const SCHEMA_VERSION = "1.0.0";
28
33
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Harness subagent spawn caps (subprocess model).
3
+ */
4
+
5
+ export const HARNESS_MAX_ACTIVE_SUBAGENTS = 8;
6
+ export const HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION = 12;
7
+
8
+ export function isHarnessAgentType(type: string): boolean {
9
+ return type.startsWith("harness/");
10
+ }
11
+
12
+ export interface SpawnBudgetState {
13
+ active: number;
14
+ totalHarnessSpawns: number;
15
+ }
16
+
17
+ export function createSpawnBudgetState(): SpawnBudgetState {
18
+ return { active: 0, totalHarnessSpawns: 0 };
19
+ }
20
+
21
+ export function countHarnessAgentsInRequest(params: {
22
+ agent?: string;
23
+ tasks?: { agent: string }[];
24
+ chain?: { agent: string }[];
25
+ aggregator?: { agent: string };
26
+ }): { harnessCount: number; agents: string[] } {
27
+ const agents: string[] = [];
28
+ if (params.agent) agents.push(params.agent);
29
+ if (params.tasks) for (const t of params.tasks) agents.push(t.agent);
30
+ if (params.chain) for (const c of params.chain) agents.push(c.agent);
31
+ if (params.aggregator) agents.push(params.aggregator.agent);
32
+ const harness = agents.filter(isHarnessAgentType);
33
+ return { harnessCount: harness.length, agents: harness };
34
+ }
35
+
36
+ export function checkHarnessSpawnBudget(
37
+ state: SpawnBudgetState,
38
+ incomingHarnessTasks: number,
39
+ ): { ok: boolean; message?: string } {
40
+ if (state.active + incomingHarnessTasks > HARNESS_MAX_ACTIVE_SUBAGENTS) {
41
+ return {
42
+ ok: false,
43
+ message:
44
+ `Harness subagent limit reached (${state.active} active + ${incomingHarnessTasks} requested > ${HARNESS_MAX_ACTIVE_SUBAGENTS}). ` +
45
+ `Wait for in-flight subagent calls to finish before spawning more.`,
46
+ };
47
+ }
48
+ if (
49
+ state.totalHarnessSpawns + incomingHarnessTasks >
50
+ HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION
51
+ ) {
52
+ return {
53
+ ok: false,
54
+ message:
55
+ `Harness subagent spawn cap reached (${state.totalHarnessSpawns + incomingHarnessTasks}/${HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION} this session). ` +
56
+ `Finish the current harness phase or start a new session.`,
57
+ };
58
+ }
59
+ return { ok: true };
60
+ }
61
+
62
+ export function recordSpawnStart(
63
+ state: SpawnBudgetState,
64
+ harnessCount: number,
65
+ ): void {
66
+ state.active += harnessCount;
67
+ state.totalHarnessSpawns += harnessCount;
68
+ }
69
+
70
+ export function recordSpawnEnd(
71
+ state: SpawnBudgetState,
72
+ harnessCount: number,
73
+ ): void {
74
+ state.active = Math.max(0, state.active - harnessCount);
75
+ }