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.
- package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
- package/.agents/skills/harness-decisions/SKILL.md +1 -1
- package/.agents/skills/harness-orchestration/SKILL.md +54 -28
- package/.agents/skills/harness-plan/SKILL.md +15 -20
- package/.pi/agents/harness/adversary.md +0 -1
- package/.pi/agents/harness/evaluator.md +0 -1
- package/.pi/agents/harness/executor.md +1 -2
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/meta-optimizer.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +3 -4
- package/.pi/agents/harness/planning/execution-plan-author.md +30 -0
- package/.pi/agents/harness/planning/hypothesis-validator.md +23 -0
- package/.pi/agents/harness/planning/hypothesis.md +3 -4
- package/.pi/agents/harness/planning/plan-adversary.md +10 -42
- package/.pi/agents/harness/planning/plan-evaluator.md +18 -0
- package/.pi/agents/harness/planning/review-integrator.md +23 -0
- package/.pi/agents/harness/planning/scout-graphify.md +11 -5
- package/.pi/agents/harness/planning/scout-semantic.md +11 -6
- package/.pi/agents/harness/planning/scout-structure.md +12 -6
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +18 -0
- package/.pi/agents/harness/planning/stack-researcher.md +24 -0
- package/.pi/agents/harness/tie-breaker.md +0 -1
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/debate-orchestrator.ts +90 -53
- package/.pi/extensions/harness-plan-approval.ts +2 -2
- package/.pi/extensions/harness-run-context.ts +145 -5
- package/.pi/extensions/harness-subagents.ts +2 -2
- package/.pi/extensions/lib/harness-posthog.ts +6 -1
- package/.pi/extensions/lib/harness-spawn-budget.ts +75 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +123 -0
- package/.pi/extensions/lib/{harness-subagents/harness-subagent-policy.ts → harness-subagent-policy.ts} +3 -6
- package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +176 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +4 -7
- package/.pi/extensions/lib/plan-approval/plan-review.ts +1 -1
- package/.pi/extensions/lib/plan-approval/types.ts +7 -1
- package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
- package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +1 -0
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/extensions/review-integrity.ts +48 -29
- package/.pi/harness/agents.manifest.json +37 -25
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +4 -3
- package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r1.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r4.yaml +26 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/sprint-audit-r4.yaml +5 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +32 -0
- package/.pi/harness/evals/smoke/run-context.fixture.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +88 -0
- package/.pi/harness/specs/harness-posthog-event.schema.json +6 -1
- package/.pi/harness/specs/plan-execution-plan-brief.schema.json +13 -0
- package/.pi/harness/specs/plan-execution-plan.schema.json +255 -0
- package/.pi/harness/specs/plan-packet.schema.json +14 -5
- package/.pi/harness/specs/plan-review-round-draft.schema.json +68 -0
- package/.pi/harness/specs/plan-sprint-audit-turn.schema.json +29 -0
- package/.pi/harness/specs/plan-stack-brief.schema.json +65 -0
- package/.pi/harness/specs/plan-validation-turn.schema.json +42 -0
- package/.pi/harness/specs/round-result.schema.json +16 -9
- package/.pi/lib/debate-orchestrator-types.ts +38 -0
- package/.pi/lib/harness-agent-discovery.mjs +81 -0
- package/.pi/lib/harness-run-context.ts +64 -38
- package/.pi/lib/harness-yaml.mjs +73 -0
- package/.pi/lib/harness-yaml.ts +90 -0
- package/.pi/prompts/harness-auto.md +13 -11
- package/.pi/prompts/harness-critic.md +2 -2
- package/.pi/prompts/harness-eval.md +3 -3
- package/.pi/prompts/harness-incident.md +2 -2
- package/.pi/prompts/harness-plan.md +79 -93
- package/.pi/prompts/harness-review.md +2 -2
- package/.pi/prompts/harness-router-tune.md +1 -1
- package/.pi/prompts/harness-run.md +2 -2
- package/.pi/prompts/harness-setup.md +15 -6
- package/.pi/prompts/harness-trace.md +2 -2
- package/.pi/scripts/harness-agents-manifest.mjs +1 -1
- package/.pi/scripts/harness-verify.mjs +28 -19
- package/.pi/scripts/validate-plan-dag.mjs +258 -0
- package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
- package/CHANGELOG.md +12 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/biome.json +2 -2
- package/package.json +6 -4
- package/.pi/agents/harness/planner.md +0 -13
- package/.pi/agents/harness/planning/hypothesis-eval.md +0 -59
- package/.pi/agents/harness/planning/planner.md +0 -20
- package/.pi/extensions/lib/harness-subagents/agent-loader.ts +0 -126
- package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +0 -119
- package/.pi/extensions/lib/harness-subagents/agent-parser.ts +0 -87
- package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +0 -118
- package/.pi/extensions/lib/harness-subagents/blackboard.ts +0 -175
- package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +0 -10
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +0 -137
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -77
- package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +0 -27
- package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +0 -558
- package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -666
- package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +0 -175
- package/.pi/extensions/lib/harness-subagents/vendored/context.ts +0 -59
- package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +0 -134
- package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +0 -5
- package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +0 -123
- package/.pi/extensions/lib/harness-subagents/vendored/env.ts +0 -43
- package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +0 -144
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +0 -2460
- package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +0 -52
- package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +0 -182
- package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +0 -92
- package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +0 -115
- package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +0 -103
- package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +0 -177
- package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +0 -416
- package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +0 -210
- package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +0 -108
- package/.pi/extensions/lib/harness-subagents/vendored/types.ts +0 -187
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +0 -639
- package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +0 -324
- package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +0 -110
- package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +0 -71
- package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +0 -195
- /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
|
|
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/")
|
|
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)
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
|
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 {
|