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
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Resolve concrete LLM credentials for harness subagent subprocesses.
3
+ *
4
+ * Parent sessions often use `router/auto` (pi-model-router). Subagents run with
5
+ * `--no-extensions`, so they cannot use the logical router provider — they need
6
+ * a real provider/model plus that provider's API key.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { AgentConfig } from "../../../vendor/pi-subagents/src/agents.js";
12
+
13
+ const ROUTER_SENTINEL_KEY = "pi-model-router";
14
+ const SENTINEL_API_KEYS = new Set([ROUTER_SENTINEL_KEY, "<authenticated>"]);
15
+
16
+ type RouterTier = "high" | "medium" | "low";
17
+
18
+ interface ModelRouterJson {
19
+ defaultProfile?: string;
20
+ profiles?: Record<string, Partial<Record<RouterTier, { model?: string }>>>;
21
+ }
22
+
23
+ export function isUsableApiKey(key: string | undefined): key is string {
24
+ return Boolean(key && !SENTINEL_API_KEYS.has(key));
25
+ }
26
+
27
+ export function parseModelRef(
28
+ ref: string,
29
+ ): { provider: string; modelId: string } | null {
30
+ const slash = ref.indexOf("/");
31
+ if (slash <= 0) return null;
32
+ const provider = ref.slice(0, slash).trim();
33
+ const modelId = ref.slice(slash + 1).trim();
34
+ if (!provider || !modelId) return null;
35
+ return { provider, modelId };
36
+ }
37
+
38
+ export function thinkingToRouterTier(thinking?: string): RouterTier {
39
+ if (thinking === "high" || thinking === "xhigh") return "high";
40
+ if (thinking === "off" || thinking === "minimal" || thinking === "low") {
41
+ return "low";
42
+ }
43
+ return "medium";
44
+ }
45
+
46
+ /** Map router profile tier → concrete `provider/model` from `.pi/model-router.json`. */
47
+ export function resolveRouterConcreteModelRef(
48
+ cwd: string,
49
+ profileId: string,
50
+ tier: RouterTier,
51
+ ): string | undefined {
52
+ const path = join(cwd, ".pi", "model-router.json");
53
+ if (!existsSync(path)) return undefined;
54
+ let raw: ModelRouterJson;
55
+ try {
56
+ raw = JSON.parse(readFileSync(path, "utf8")) as ModelRouterJson;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ const profiles = raw.profiles;
61
+ if (!profiles) return undefined;
62
+ const profile =
63
+ profiles[profileId] ??
64
+ profiles[raw.defaultProfile ?? "auto"] ??
65
+ profiles.auto;
66
+ const model = profile?.[tier]?.model;
67
+ return typeof model === "string" && model.includes("/") ? model : undefined;
68
+ }
69
+
70
+ export interface ConcreteSubagentModel {
71
+ modelRef: string;
72
+ provider: string;
73
+ modelId: string;
74
+ routerProfile?: string;
75
+ routerTier?: RouterTier;
76
+ }
77
+
78
+ /**
79
+ * Pick the subprocess model ref before resolving API keys.
80
+ * Never returns `router/*` — always a concrete provider.
81
+ */
82
+ export function resolveConcreteSubagentModel(
83
+ cwd: string,
84
+ parentModel: { provider: string; id: string } | undefined,
85
+ agent: AgentConfig,
86
+ ): ConcreteSubagentModel | undefined {
87
+ if (agent.model && !agent.model.startsWith("router/")) {
88
+ const parsed = parseModelRef(agent.model);
89
+ if (parsed) {
90
+ return { modelRef: agent.model, ...parsed };
91
+ }
92
+ }
93
+
94
+ const parentIsRouter = parentModel?.provider === "router";
95
+ const agentIsRouter = Boolean(agent.model?.startsWith("router/"));
96
+
97
+ if (!parentIsRouter && !agentIsRouter) {
98
+ if (parentModel && parentModel.provider !== "router") {
99
+ return {
100
+ modelRef: `${parentModel.provider}/${parentModel.id}`,
101
+ provider: parentModel.provider,
102
+ modelId: parentModel.id,
103
+ };
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ const profileId =
109
+ agentIsRouter && agent.model
110
+ ? agent.model.slice("router/".length)
111
+ : (parentModel?.id ?? "auto");
112
+ const tier = thinkingToRouterTier(agent.thinking);
113
+ const concrete = resolveRouterConcreteModelRef(cwd, profileId, tier);
114
+ if (!concrete) return undefined;
115
+ const parsed = parseModelRef(concrete);
116
+ if (!parsed || parsed.provider === "router") return undefined;
117
+ return {
118
+ modelRef: concrete,
119
+ ...parsed,
120
+ routerProfile: profileId,
121
+ routerTier: tier,
122
+ };
123
+ }
@@ -56,17 +56,15 @@ const READ_ONLY_KINDS = new Set<HarnessAgentKind>([
56
56
 
57
57
  export function isHarnessPlanningAgent(agentType: string): boolean {
58
58
  const id = agentType.replace(/^harness\//, "");
59
- return id === "planner" || id.startsWith("planning/");
59
+ return id.startsWith("planning/");
60
60
  }
61
61
 
62
62
  export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
63
63
  const id = agentType.replace(/^harness\//, "");
64
- if (id.startsWith("planning/") || id === "planner") {
64
+ if (id.startsWith("planning/")) {
65
65
  return "planner";
66
66
  }
67
67
  switch (id) {
68
- case "planner":
69
- return "planner";
70
68
  case "executor":
71
69
  return "executor";
72
70
  case "evaluator":
@@ -123,7 +121,7 @@ export function evaluateHarnessSubagentToolCall(
123
121
  if (MUTATING_TOOLS.has(toolName)) {
124
122
  return {
125
123
  action: "block",
126
- reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent). Use create_plan after approve_plan instead of write/edit.`,
124
+ reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent).`,
127
125
  };
128
126
  }
129
127
 
@@ -151,7 +149,6 @@ export function evaluateHarnessSubagentToolCall(
151
149
  return { action: "allow" };
152
150
  }
153
151
 
154
- /** Policy phase hint seeded into subagent system prompt appendix when extensions load policy-gate. */
155
152
  export function harnessSubagentPhaseHint(agentType: string): string | null {
156
153
  if (isHarnessPlanningAgent(agentType)) {
157
154
  return "plan";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Pre-spawn validation for harness subagent tool calls.
3
+ */
4
+
5
+ import {
6
+ type AgentConfig,
7
+ agentAllowsMutatingTools,
8
+ } from "../../../vendor/pi-subagents/src/agents.js";
9
+ import type { HarnessPhase } from "../../lib/harness-run-context.js";
10
+ import { inferHarnessPhase } from "../../lib/harness-run-context.js";
11
+ import { classifyHarnessAgent } from "./harness-subagent-policy.js";
12
+
13
+ export interface SubagentTaskRef {
14
+ agent: string;
15
+ }
16
+
17
+ export interface PrecheckResult {
18
+ ok: boolean;
19
+ message?: string;
20
+ }
21
+
22
+ function collectAgents(params: {
23
+ agent?: string;
24
+ tasks?: SubagentTaskRef[];
25
+ chain?: SubagentTaskRef[];
26
+ aggregator?: { agent: string };
27
+ }): string[] {
28
+ const names: string[] = [];
29
+ if (params.agent) names.push(params.agent);
30
+ if (params.tasks) for (const t of params.tasks) names.push(t.agent);
31
+ if (params.chain) for (const c of params.chain) names.push(c.agent);
32
+ if (params.aggregator) names.push(params.aggregator.agent);
33
+ return names;
34
+ }
35
+
36
+ function resolveAgent(
37
+ agents: AgentConfig[],
38
+ name: string,
39
+ ): AgentConfig | undefined {
40
+ return agents.find((a) => a.name === name);
41
+ }
42
+
43
+ export function precheckHarnessSubagentSpawn(
44
+ params: {
45
+ agent?: string;
46
+ tasks?: SubagentTaskRef[];
47
+ chain?: SubagentTaskRef[];
48
+ aggregator?: { agent: string };
49
+ },
50
+ agents: AgentConfig[],
51
+ phase: HarnessPhase,
52
+ ): PrecheckResult {
53
+ const names = collectAgents(params);
54
+ const mutating = names.filter((n) => {
55
+ const cfg = resolveAgent(agents, n);
56
+ return cfg
57
+ ? agentAllowsMutatingTools(cfg)
58
+ : n.startsWith("harness/executor");
59
+ });
60
+
61
+ if (phase === "plan" && mutating.length > 0) {
62
+ return {
63
+ ok: false,
64
+ message:
65
+ `Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}). ` +
66
+ `Use read-only harness/planning/* agents until execute phase.`,
67
+ };
68
+ }
69
+
70
+ if ((params.tasks?.length ?? 0) > 1 && mutating.length > 1) {
71
+ return {
72
+ ok: false,
73
+ message:
74
+ "Parallel subagent tasks cannot include multiple mutating agents (file race risk). " +
75
+ "Run one executor at a time.",
76
+ };
77
+ }
78
+
79
+ for (const name of names) {
80
+ if (!name.startsWith("harness/")) continue;
81
+ const kind = classifyHarnessAgent(name);
82
+ if (kind === "planner" && phase !== "plan") {
83
+ // allowed — planning agents can run in plan only ideally
84
+ }
85
+ }
86
+
87
+ return { ok: true };
88
+ }
89
+
90
+ export function inferPhaseForPrecheck(
91
+ entries: unknown[],
92
+ prompt?: string,
93
+ ): HarnessPhase {
94
+ return inferHarnessPhase(entries as never, prompt);
95
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ultimate-pi harness wrapper around vendored pi-subagents.
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type { AgentConfig } from "../../../vendor/pi-subagents/src/agents.js";
10
+ import {
11
+ createSubagentsExtension,
12
+ type HarnessSubagentsOptions,
13
+ type SpawnAuthForward,
14
+ } from "../../../vendor/pi-subagents/src/subagents.js";
15
+ import { captureHarnessEvent } from "./harness-posthog.js";
16
+ import {
17
+ checkHarnessSpawnBudget,
18
+ countHarnessAgentsInRequest,
19
+ createSpawnBudgetState,
20
+ recordSpawnEnd,
21
+ recordSpawnStart,
22
+ } from "./harness-spawn-budget.js";
23
+ import {
24
+ isUsableApiKey,
25
+ resolveConcreteSubagentModel,
26
+ } from "./harness-subagent-auth.js";
27
+ import {
28
+ inferPhaseForPrecheck,
29
+ precheckHarnessSubagentSpawn,
30
+ } from "./harness-subagent-precheck.js";
31
+
32
+ const spawnBudget = createSpawnBudgetState();
33
+ let lastSessionId = "harness";
34
+
35
+ function maskApiKey(key: string | undefined): string | undefined {
36
+ if (!key) return undefined;
37
+ if (key.length <= 12) return "***";
38
+ return `${key.slice(0, 7)}…${key.slice(-4)}`;
39
+ }
40
+
41
+ // #region agent log
42
+ function agentDebugLog(
43
+ hypothesisId: string,
44
+ location: string,
45
+ message: string,
46
+ data: Record<string, unknown>,
47
+ ): void {
48
+ fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ "X-Debug-Session-Id": "e762d5",
53
+ },
54
+ body: JSON.stringify({
55
+ sessionId: "e762d5",
56
+ hypothesisId,
57
+ location,
58
+ message,
59
+ data,
60
+ timestamp: Date.now(),
61
+ }),
62
+ }).catch(() => {});
63
+ }
64
+ // #endregion
65
+
66
+ async function resolveHarnessSpawnAuth(
67
+ ctx: ExtensionContext,
68
+ agent: AgentConfig,
69
+ ): Promise<SpawnAuthForward | undefined> {
70
+ const parentModel = ctx.model
71
+ ? { provider: ctx.model.provider, id: ctx.model.id }
72
+ : undefined;
73
+ const concrete = resolveConcreteSubagentModel(ctx.cwd, parentModel, agent);
74
+ if (!concrete) {
75
+ // #region agent log
76
+ agentDebugLog(
77
+ "D",
78
+ "harness-subagents-bridge.ts:resolveHarnessSpawnAuth",
79
+ "no concrete model",
80
+ {
81
+ agent: agent.name,
82
+ agentModel: agent.model,
83
+ parentModel: parentModel
84
+ ? `${parentModel.provider}/${parentModel.id}`
85
+ : undefined,
86
+ },
87
+ );
88
+ // #endregion
89
+ return undefined;
90
+ }
91
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(
92
+ concrete.provider,
93
+ );
94
+ // #region agent log
95
+ agentDebugLog(
96
+ "F",
97
+ "harness-subagents-bridge.ts:resolveHarnessSpawnAuth",
98
+ "concrete subprocess auth",
99
+ {
100
+ agent: agent.name,
101
+ parentModel: parentModel
102
+ ? `${parentModel.provider}/${parentModel.id}`
103
+ : undefined,
104
+ concreteModel: concrete.modelRef,
105
+ routerProfile: concrete.routerProfile,
106
+ routerTier: concrete.routerTier,
107
+ apiKey: maskApiKey(apiKey),
108
+ usable: isUsableApiKey(apiKey),
109
+ },
110
+ );
111
+ // #endregion
112
+ if (!isUsableApiKey(apiKey)) return undefined;
113
+ return {
114
+ provider: concrete.provider,
115
+ modelRef: concrete.modelRef,
116
+ apiKey,
117
+ };
118
+ }
119
+
120
+ export function createHarnessSubagentsExtension(
121
+ packageRoot: string,
122
+ ): (pi: ExtensionAPI) => void {
123
+ const options: HarnessSubagentsOptions = {
124
+ packageRoot,
125
+ defaultAgentScope: "both",
126
+ defaultConfirmProjectAgents: false,
127
+ truncateDetails: true,
128
+ resolveSpawnAuth: resolveHarnessSpawnAuth,
129
+ beforeExecute: async (params, agents, ctx) => {
130
+ lastSessionId = ctx.sessionManager.getSessionId();
131
+ const { harnessCount } = countHarnessAgentsInRequest(
132
+ params as Parameters<typeof countHarnessAgentsInRequest>[0],
133
+ );
134
+ if (harnessCount > 0) {
135
+ const budget = checkHarnessSpawnBudget(spawnBudget, harnessCount);
136
+ if (!budget.ok) {
137
+ return { ok: false, message: budget.message };
138
+ }
139
+ const phase = inferPhaseForPrecheck(ctx.sessionManager.getEntries());
140
+ const pre = precheckHarnessSubagentSpawn(
141
+ params as Parameters<typeof precheckHarnessSubagentSpawn>[0],
142
+ agents,
143
+ phase,
144
+ );
145
+ if (!pre.ok) {
146
+ return { ok: false, message: pre.message };
147
+ }
148
+ }
149
+ return { ok: true };
150
+ },
151
+ onSpawnStart: (harnessCount) => {
152
+ if (harnessCount <= 0) return;
153
+ recordSpawnStart(spawnBudget, harnessCount);
154
+ captureHarnessEvent(lastSessionId, "harness_subagent_spawned", {
155
+ active_after: spawnBudget.active,
156
+ spawn_count: harnessCount,
157
+ });
158
+ },
159
+ onSpawnEnd: (harnessCount) => {
160
+ if (harnessCount <= 0) return;
161
+ recordSpawnEnd(spawnBudget, harnessCount);
162
+ },
163
+ onCompleted: ({ agents, mode, durationMs }) => {
164
+ if (agents.length === 0) return;
165
+ captureHarnessEvent(lastSessionId, "harness_subagent_completed", {
166
+ mode,
167
+ duration_ms: durationMs,
168
+ agent_count: agents.length,
169
+ });
170
+ },
171
+ };
172
+
173
+ return (pi: ExtensionAPI) => {
174
+ createSubagentsExtension(pi, options);
175
+ };
176
+ }
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import {
4
4
  canonicalPlanPath,
@@ -9,6 +9,7 @@ import {
9
9
  saveRunContextToDisk,
10
10
  validatePlanPacket,
11
11
  } from "../../../lib/harness-run-context.js";
12
+ import { writeYamlFile } from "../../../lib/harness-yaml.js";
12
13
  import { writePlanReviewMarkdown } from "./plan-review.js";
13
14
 
14
15
  export const CREATE_PLAN_SNIPPET =
@@ -17,7 +18,7 @@ export const CREATE_PLAN_SNIPPET =
17
18
  export const CREATE_PLAN_GUIDELINES = [
18
19
  "Call create_plan only after the user approves via approve_plan (Approve selection).",
19
20
  "Pass the same plan_packet you showed in approve_plan — path is resolved automatically.",
20
- "Never use write or edit for plan-packet.json; create_plan is the only allowed plan write.",
21
+ "Never use write or edit for plan-packet.yaml; create_plan is the only allowed plan write.",
21
22
  ];
22
23
 
23
24
  export interface CreatePlanDeps {
@@ -89,11 +90,7 @@ export async function executeCreatePlan(
89
90
 
90
91
  try {
91
92
  await mkdir(dirname(planPath), { recursive: true });
92
- await writeFile(
93
- planPath,
94
- `${JSON.stringify(planPacket, null, 2)}\n`,
95
- "utf-8",
96
- );
93
+ await writeYamlFile(planPath, planPacket);
97
94
  } catch (err) {
98
95
  const msg = err instanceof Error ? err.message : String(err);
99
96
  return { ok: false, error: `create_plan: write failed — ${msg}` };
@@ -245,7 +245,7 @@ export function formatPlanPacketMarkdown(
245
245
  );
246
246
  } else if (status === "approved") {
247
247
  lines.push(
248
- "Approved in the harness TUI. Waiting for `create_plan` to write `plan-packet.json`, or run `/harness-plan-commit` if that step failed.",
248
+ "Approved in the harness TUI. Waiting for `create_plan` to write `plan-packet.yaml`, or run `/harness-plan-commit` if that step failed.",
249
249
  );
250
250
  } else {
251
251
  lines.push(
@@ -7,11 +7,17 @@ export const DEFAULT_PLAN_APPROVAL_OPTIONS = [
7
7
  "Cancel",
8
8
  ] as const;
9
9
 
10
- /** Optional Darwin research artifacts from /harness-plan (not persisted in plan-packet.json). */
10
+ /** Optional Darwin research artifacts from /harness-plan (research-brief.yaml, not in plan-packet). */
11
11
  export interface PlanResearchBrief {
12
12
  decomposition?: Record<string, unknown> | null;
13
13
  hypothesis?: Record<string, unknown> | null;
14
14
  eval?: Record<string, unknown> | null;
15
+ stack?: Record<string, unknown> | null;
16
+ debate?: {
17
+ rounds?: Record<string, unknown>[];
18
+ hypothesis_validations?: Record<string, unknown>[];
19
+ } | null;
20
+ dag_validation?: Record<string, unknown> | null;
15
21
  }
16
22
 
17
23
  export interface ApprovePlanParams {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Plan Review Gate — convert integrator YAML to debate bus round JSON.
3
+ */
4
+
5
+ import type { DebateParticipant } from "../../lib/debate-orchestrator-types.js";
6
+
7
+ export interface PlanReviewRoundDraft {
8
+ schema_version: string;
9
+ round_index: number;
10
+ debate_round_focus?: string;
11
+ round_summary?: string;
12
+ validation_summary?: string;
13
+ adversary_summary?: string;
14
+ disputes?: string[];
15
+ recommended_packet_patches?: Array<{ path: string; value: unknown }>;
16
+ review_gate_ready?: boolean;
17
+ participants?: DebateParticipant[];
18
+ claims?: string[];
19
+ rebuttals?: string[];
20
+ evidence_refs?: string[];
21
+ token_usage?: {
22
+ per_agent: Record<string, number>;
23
+ round_total: number;
24
+ };
25
+ consensus_delta?: number;
26
+ severity_scores?: {
27
+ correctness: number;
28
+ security: number;
29
+ architecture: number;
30
+ test_integrity: number;
31
+ };
32
+ }
33
+
34
+ export function buildPlanReviewRoundEnvelope(
35
+ draft: PlanReviewRoundDraft,
36
+ opts: { runId: string; debateId: string },
37
+ ): {
38
+ protocol: "pi-debate-bus/v1";
39
+ kind: "round";
40
+ correlation: {
41
+ run_id: string;
42
+ debate_id: string;
43
+ round_index: number;
44
+ sender: DebateParticipant;
45
+ };
46
+ payload: {
47
+ participants: DebateParticipant[];
48
+ claims: string[];
49
+ rebuttals: string[];
50
+ evidence_refs: string[];
51
+ token_usage: { per_agent: Record<string, number>; round_total: number };
52
+ consensus_delta: number;
53
+ severity_scores?: PlanReviewRoundDraft["severity_scores"];
54
+ };
55
+ } {
56
+ const participants = (draft.participants ?? [
57
+ "PlanEvaluatorAgent",
58
+ "PlanAdversaryAgent",
59
+ "ReviewIntegratorAgent",
60
+ ]) as DebateParticipant[];
61
+
62
+ return {
63
+ protocol: "pi-debate-bus/v1",
64
+ kind: "round",
65
+ correlation: {
66
+ run_id: opts.runId,
67
+ debate_id: opts.debateId,
68
+ round_index: draft.round_index,
69
+ sender: "ReviewIntegratorAgent",
70
+ },
71
+ payload: {
72
+ participants,
73
+ claims: draft.claims ?? [draft.round_summary ?? "review round"],
74
+ rebuttals: draft.rebuttals ?? draft.disputes ?? [],
75
+ evidence_refs: draft.evidence_refs ?? [],
76
+ token_usage: draft.token_usage ?? {
77
+ per_agent: { ReviewIntegratorAgent: 0 },
78
+ round_total: 0,
79
+ },
80
+ consensus_delta: draft.consensus_delta ?? 0,
81
+ severity_scores: draft.severity_scores,
82
+ },
83
+ };
84
+ }
@@ -7,6 +7,7 @@ export const SUBAGENT_BLOCKED_TOOLS = new Set([
7
7
  "get_subagent_result",
8
8
  "steer_subagent",
9
9
  "blackboard",
10
+ "subagent",
10
11
  ]);
11
12
 
12
13
  const ASK_USER_ALLOWED_AGENT_TYPES = new Set([
@@ -243,7 +243,7 @@ export default function policyGate(pi: ExtensionAPI) {
243
243
 
244
244
  const planPhaseHint =
245
245
  state.phase === "plan"
246
- ? "\nPlan phase: scouts → decompose → hypothesis via harness/planning/*; parent builds PlanPacket, ask_user on fork, parallel plan-adversary + hypothesis-eval, approve_plan (optional research_brief), then create_plan (never write plan-packet.json directly)."
246
+ ? "\nPlan phase: scouts → decompose → hypothesis stack-researcher execution-plan-author validate-plan-dag 4-round plan debate approve_plan create_plan (YAML plan-packet.yaml). Post-execute: /harness-critic."
247
247
  : "";
248
248
 
249
249
  return {