ultimate-pi 0.15.0 → 0.17.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-governor/SKILL.md +11 -0
- package/.agents/skills/harness-orchestration/SKILL.md +3 -1
- package/.agents/skills/harness-plan/SKILL.md +5 -5
- package/.pi/agents/harness/adversary.md +1 -1
- package/.pi/agents/harness/evaluator.md +1 -1
- package/.pi/agents/harness/executor.md +1 -1
- package/.pi/agents/harness/incident-recorder.md +1 -1
- package/.pi/agents/harness/meta-optimizer.md +1 -1
- package/.pi/agents/harness/planning/decompose.md +4 -33
- package/.pi/agents/harness/planning/execution-plan-author.md +3 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +3 -2
- package/.pi/agents/harness/planning/hypothesis.md +4 -27
- package/.pi/agents/harness/planning/implementation-researcher.md +3 -2
- package/.pi/agents/harness/planning/plan-adversary.md +2 -3
- package/.pi/agents/harness/planning/plan-evaluator.md +3 -2
- package/.pi/agents/harness/planning/review-integrator.md +2 -3
- package/.pi/agents/harness/planning/scout-graphify.md +3 -22
- package/.pi/agents/harness/planning/scout-semantic.md +3 -18
- package/.pi/agents/harness/planning/scout-structure.md +3 -18
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +3 -2
- package/.pi/agents/harness/planning/stack-researcher.md +3 -2
- package/.pi/agents/harness/tie-breaker.md +1 -1
- package/.pi/agents/harness/trace-librarian.md +1 -1
- package/.pi/extensions/budget-guard.ts +33 -19
- package/.pi/extensions/harness-debate-tools.ts +54 -6
- package/.pi/extensions/harness-run-context.ts +108 -2
- package/.pi/extensions/harness-subagent-submit.ts +172 -0
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +49 -6
- package/.pi/extensions/lib/harness-subagent-auth.ts +104 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +59 -0
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +82 -0
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +172 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +127 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +61 -8
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +92 -18
- package/.pi/extensions/lib/plan-debate-lane.ts +15 -0
- package/.pi/extensions/lib/plan-debate-lanes.ts +27 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +51 -0
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +22 -22
- package/.pi/harness/docs/adrs/0037-subagent-submit-tools.md +31 -0
- package/.pi/harness/docs/adrs/0038-budget-telemetry-only.md +23 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +40 -17
- package/.pi/harness/specs/harness-executor-handoff.schema.json +19 -0
- package/.pi/harness/specs/harness-human-required.schema.json +16 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/plan-scout-findings.schema.json +19 -0
- package/.pi/lib/harness-agent-output.ts +45 -0
- package/.pi/lib/harness-budget-enforce.ts +18 -0
- package/.pi/lib/harness-schema-validate.ts +89 -0
- package/.pi/lib/harness-spawn-parse.ts +86 -0
- package/.pi/lib/harness-subagent-submit-path.ts +41 -0
- package/.pi/lib/harness-ui-state.ts +15 -2
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +34 -14
- package/.pi/prompts/harness-run.md +2 -2
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +31 -0
- package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
- package/CHANGELOG.md +21 -0
- package/package.json +4 -2
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
- package/vendor/pi-subagents/src/subagents.ts +29 -3
|
@@ -8,6 +8,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import { Type } from "@sinclair/typebox";
|
|
9
9
|
import { parse as parseYaml } from "yaml";
|
|
10
10
|
import type { DebateParticipant } from "../lib/debate-orchestrator-types.js";
|
|
11
|
+
import {
|
|
12
|
+
extractLastSubmitCall,
|
|
13
|
+
type MessageLike,
|
|
14
|
+
} from "../lib/harness-agent-output.js";
|
|
11
15
|
import {
|
|
12
16
|
getLatestRunContext,
|
|
13
17
|
getRunIdFromSession,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
import { getDebateState } from "./lib/debate-bus-state.js";
|
|
23
27
|
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
24
28
|
import { captureHarnessEvent } from "./lib/harness-posthog.js";
|
|
29
|
+
import { DEBATE_AGENT_SUBMIT_TOOL } from "./lib/harness-subagent-submit-registry.js";
|
|
25
30
|
import {
|
|
26
31
|
type DebateEligibilityInput,
|
|
27
32
|
harnessPlanDebateEligibility,
|
|
@@ -40,6 +45,7 @@ import {
|
|
|
40
45
|
} from "./lib/plan-debate-id.js";
|
|
41
46
|
import {
|
|
42
47
|
applyDebateLane,
|
|
48
|
+
applyDebateLaneFromDoc,
|
|
43
49
|
type DebateLaneKind,
|
|
44
50
|
debateLaneForAgent,
|
|
45
51
|
formatApplyLaneMessage,
|
|
@@ -95,13 +101,19 @@ function telemetryRound(
|
|
|
95
101
|
|
|
96
102
|
function subagentResults(
|
|
97
103
|
details: unknown,
|
|
98
|
-
): Array<{ agent: string; finalOutput?: string }> {
|
|
104
|
+
): Array<{ agent: string; finalOutput?: string; messages?: MessageLike[] }> {
|
|
99
105
|
const d = details as {
|
|
100
|
-
results?: Array<{
|
|
106
|
+
results?: Array<{
|
|
107
|
+
agent: string;
|
|
108
|
+
finalOutput?: string;
|
|
109
|
+
messages?: MessageLike[];
|
|
110
|
+
}>;
|
|
101
111
|
};
|
|
102
112
|
return d?.results ?? [];
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
const USE_SUBMIT_TOOLS = process.env.HARNESS_SUBMIT_TOOLS !== "0";
|
|
116
|
+
|
|
105
117
|
export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
106
118
|
if (!claimExtensionLoad("harness-debate-tools", MODULE_URL)) return;
|
|
107
119
|
|
|
@@ -118,7 +130,34 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
118
130
|
let lastRound = 1;
|
|
119
131
|
for (const result of subagentResults(event.details)) {
|
|
120
132
|
const lane = debateLaneForAgent(result.agent ?? "");
|
|
121
|
-
if (!lane
|
|
133
|
+
if (!lane) continue;
|
|
134
|
+
|
|
135
|
+
const submitTool = DEBATE_AGENT_SUBMIT_TOOL[result.agent ?? ""];
|
|
136
|
+
const submitCall =
|
|
137
|
+
USE_SUBMIT_TOOLS && submitTool && result.messages
|
|
138
|
+
? extractLastSubmitCall(result.messages, submitTool)
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
if (submitCall) {
|
|
142
|
+
const out = await applyDebateLaneFromDoc({
|
|
143
|
+
runDir: rd,
|
|
144
|
+
lane,
|
|
145
|
+
doc: submitCall.document,
|
|
146
|
+
});
|
|
147
|
+
if (out.round_index) lastRound = out.round_index;
|
|
148
|
+
pi.appendEntry("harness-debate-lane-applied", {
|
|
149
|
+
agent: result.agent,
|
|
150
|
+
source: "submit_tool",
|
|
151
|
+
tool: submitCall.toolName,
|
|
152
|
+
...out,
|
|
153
|
+
});
|
|
154
|
+
applied.push(formatApplyLaneMessage(out));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!result.finalOutput?.trim()) continue;
|
|
159
|
+
if (USE_SUBMIT_TOOLS && submitTool) continue;
|
|
160
|
+
|
|
122
161
|
const out = await applyDebateLane({
|
|
123
162
|
runDir: rd,
|
|
124
163
|
lane,
|
|
@@ -153,7 +192,7 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
153
192
|
name: "harness_plan_debate_eligibility",
|
|
154
193
|
label: "Plan Debate Eligibility",
|
|
155
194
|
description:
|
|
156
|
-
"Pre-debate profile selection (full|standard|light). Call after DAG pass, before harness_debate_open. Uses risk, fork, implementation/stack briefs — not R1 hypothesis output.",
|
|
195
|
+
"Pre-debate profile selection (full|standard|light|fast). Call after DAG pass, before harness_debate_open. Uses risk, fork, implementation/stack briefs — not R1 hypothesis output.",
|
|
157
196
|
parameters: Type.Object({
|
|
158
197
|
risk_level: Type.Optional(
|
|
159
198
|
Type.String({ description: "low | med | high" }),
|
|
@@ -211,6 +250,7 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
211
250
|
const result = harnessPlanDebateEligibility(input);
|
|
212
251
|
const lines = [
|
|
213
252
|
`profile: ${result.profile}`,
|
|
253
|
+
`review_gate_mode: ${result.review_gate_strategy.mode}`,
|
|
214
254
|
`required_focuses: ${result.required_focuses.join(", ")}`,
|
|
215
255
|
`min_focus_rounds: ${result.min_focus_rounds}`,
|
|
216
256
|
`debate_global_cap: ${result.debate_global_cap}`,
|
|
@@ -234,7 +274,7 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
234
274
|
Type.String({ description: "Optional; normalized to plan-<run_id>" }),
|
|
235
275
|
),
|
|
236
276
|
debate_profile: Type.Optional(
|
|
237
|
-
Type.String({ description: "full | standard | light" }),
|
|
277
|
+
Type.String({ description: "full | standard | light | fast" }),
|
|
238
278
|
),
|
|
239
279
|
required_focuses: Type.Optional(
|
|
240
280
|
Type.Array(
|
|
@@ -258,7 +298,8 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
258
298
|
const profile =
|
|
259
299
|
p.debate_profile === "full" ||
|
|
260
300
|
p.debate_profile === "standard" ||
|
|
261
|
-
p.debate_profile === "light"
|
|
301
|
+
p.debate_profile === "light" ||
|
|
302
|
+
p.debate_profile === "fast"
|
|
262
303
|
? p.debate_profile
|
|
263
304
|
: "standard";
|
|
264
305
|
const required_focuses = (p.required_focuses ?? []).filter((f) =>
|
|
@@ -269,11 +310,14 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
269
310
|
required_focuses:
|
|
270
311
|
required_focuses.length > 0 ? required_focuses : undefined,
|
|
271
312
|
});
|
|
313
|
+
const review_gate_mode =
|
|
314
|
+
profile === "fast" ? ("consolidated" as const) : ("threaded" as const);
|
|
272
315
|
await initPlanMessenger(runDir(projectRoot, runId), {
|
|
273
316
|
runId,
|
|
274
317
|
debateId,
|
|
275
318
|
debate_profile: profile,
|
|
276
319
|
required_focuses: opened.required_focuses,
|
|
320
|
+
review_gate_mode,
|
|
277
321
|
});
|
|
278
322
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
279
323
|
captureHarnessEvent(sessionId, "harness_debate_round", {
|
|
@@ -286,11 +330,15 @@ export default function harnessDebateTools(pi: ExtensionAPI) {
|
|
|
286
330
|
const lines = [
|
|
287
331
|
`Plan debate opened: ${debateId}`,
|
|
288
332
|
`Profile: ${profile}`,
|
|
333
|
+
`Review gate mode: ${review_gate_mode}`,
|
|
289
334
|
required_focuses.length
|
|
290
335
|
? `Required focuses: ${required_focuses.join(", ")}`
|
|
291
336
|
: opened.required_focuses?.length
|
|
292
337
|
? `Required focuses: ${opened.required_focuses.join(", ")}`
|
|
293
338
|
: "Required focuses: (default all four)",
|
|
339
|
+
review_gate_mode === "consolidated"
|
|
340
|
+
? "Consolidated path: one review round (artifacts/review-round-consolidated.yaml); escalate to threaded rounds only on blockers."
|
|
341
|
+
: "Threaded path: one review round per focus (spec → wbs → schedule → quality).",
|
|
294
342
|
`Messenger: debate-messenger/ (inbox + threads/round-N/transcript.jsonl)`,
|
|
295
343
|
];
|
|
296
344
|
if (warning) lines.push(`Note: ${warning}`);
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* in before_agent_start so trace-recorder reuses it on agent_start.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
10
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { Type } from "@sinclair/typebox";
|
|
12
13
|
import {
|
|
@@ -56,6 +57,10 @@ import {
|
|
|
56
57
|
writeYamlFile,
|
|
57
58
|
} from "../lib/harness-yaml.js";
|
|
58
59
|
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
60
|
+
import {
|
|
61
|
+
evaluateHarnessSubagentToolCall,
|
|
62
|
+
isSubmitToolName,
|
|
63
|
+
} from "./lib/harness-subagent-policy.js";
|
|
59
64
|
import { isReviewRoundArtifactPath } from "./lib/plan-debate-gate.js";
|
|
60
65
|
import { isReviewRoundYamlWriteAllowed } from "./lib/plan-debate-write-guard.js";
|
|
61
66
|
|
|
@@ -714,6 +719,36 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
714
719
|
});
|
|
715
720
|
|
|
716
721
|
pi.on("tool_call", async (event, ctx) => {
|
|
722
|
+
// #region agent log
|
|
723
|
+
fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
|
|
724
|
+
method: "POST",
|
|
725
|
+
headers: {
|
|
726
|
+
"Content-Type": "application/json",
|
|
727
|
+
"X-Debug-Session-Id": "2ca12b",
|
|
728
|
+
},
|
|
729
|
+
body: JSON.stringify({
|
|
730
|
+
sessionId: "2ca12b",
|
|
731
|
+
location: "harness-run-context.ts:tool_call",
|
|
732
|
+
message: "submit policy hook",
|
|
733
|
+
data: {
|
|
734
|
+
toolName: event.toolName,
|
|
735
|
+
typeofIsSubmitToolName: typeof isSubmitToolName,
|
|
736
|
+
},
|
|
737
|
+
timestamp: Date.now(),
|
|
738
|
+
hypothesisId: "H1",
|
|
739
|
+
}),
|
|
740
|
+
}).catch(() => {});
|
|
741
|
+
// #endregion
|
|
742
|
+
if (isSubmitToolName(event.toolName)) {
|
|
743
|
+
const decision = evaluateHarnessSubagentToolCall(
|
|
744
|
+
event.toolName,
|
|
745
|
+
event.input as Record<string, unknown>,
|
|
746
|
+
"parent-orchestrator",
|
|
747
|
+
);
|
|
748
|
+
if (decision.action === "block") {
|
|
749
|
+
return { block: true, reason: decision.reason };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
717
752
|
if (event.toolName === "write") {
|
|
718
753
|
const entries = getEntries(ctx);
|
|
719
754
|
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
@@ -990,6 +1025,18 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
990
1025
|
};
|
|
991
1026
|
}
|
|
992
1027
|
const relForGate = pathArg.replace(/\\/g, "/");
|
|
1028
|
+
if (/\.json$/i.test(relForGate) && relForGate.startsWith("artifacts/")) {
|
|
1029
|
+
return {
|
|
1030
|
+
content: [
|
|
1031
|
+
{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: `Path not allowed: ${pathArg}. Plan artifacts under artifacts/ must be .yaml (use submit_* from subagents or write_harness_yaml with YAML content).`,
|
|
1034
|
+
},
|
|
1035
|
+
],
|
|
1036
|
+
details: { path: pathArg },
|
|
1037
|
+
isError: true,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
993
1040
|
if (
|
|
994
1041
|
isReviewRoundArtifactPath(relForGate) &&
|
|
995
1042
|
!isReviewRoundYamlWriteAllowed()
|
|
@@ -1030,6 +1077,65 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1030
1077
|
},
|
|
1031
1078
|
});
|
|
1032
1079
|
|
|
1080
|
+
pi.registerTool({
|
|
1081
|
+
name: "harness_artifact_ready",
|
|
1082
|
+
label: "Harness Artifact Ready",
|
|
1083
|
+
description:
|
|
1084
|
+
"Check that harness artifact paths exist under the active run (no JSON parsing).",
|
|
1085
|
+
parameters: Type.Object({
|
|
1086
|
+
paths: Type.Array(Type.String(), {
|
|
1087
|
+
minItems: 1,
|
|
1088
|
+
description:
|
|
1089
|
+
"Relative paths under the run dir, e.g. artifacts/decomposition.yaml",
|
|
1090
|
+
}),
|
|
1091
|
+
}),
|
|
1092
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1093
|
+
const entries = getEntries(ctx);
|
|
1094
|
+
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
1095
|
+
if (!runCtx?.run_id) {
|
|
1096
|
+
return {
|
|
1097
|
+
content: [{ type: "text", text: "No active harness run." }],
|
|
1098
|
+
details: {},
|
|
1099
|
+
isError: true,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const paths = (params as { paths?: string[] }).paths ?? [];
|
|
1103
|
+
const projectRoot = process.cwd();
|
|
1104
|
+
const runRoot = join(
|
|
1105
|
+
projectRoot,
|
|
1106
|
+
".pi",
|
|
1107
|
+
"harness",
|
|
1108
|
+
"runs",
|
|
1109
|
+
runCtx.run_id,
|
|
1110
|
+
);
|
|
1111
|
+
const missing: string[] = [];
|
|
1112
|
+
const present: string[] = [];
|
|
1113
|
+
for (const rel of paths) {
|
|
1114
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
1115
|
+
const abs = join(runRoot, normalized);
|
|
1116
|
+
try {
|
|
1117
|
+
await access(abs, constants.R_OK);
|
|
1118
|
+
present.push(normalized);
|
|
1119
|
+
} catch {
|
|
1120
|
+
missing.push(normalized);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const ok = missing.length === 0;
|
|
1124
|
+
return {
|
|
1125
|
+
content: [
|
|
1126
|
+
{
|
|
1127
|
+
type: "text",
|
|
1128
|
+
text: ok
|
|
1129
|
+
? `All ${present.length} artifact(s) present.`
|
|
1130
|
+
: `Missing: ${missing.join(", ")}`,
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
details: { ok, present, missing, run_id: runCtx.run_id },
|
|
1134
|
+
isError: !ok,
|
|
1135
|
+
};
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1033
1139
|
pi.registerCommand("harness-use-run", {
|
|
1034
1140
|
description: "Point this session at an existing run directory (recovery)",
|
|
1035
1141
|
handler: async (args, ctx) => {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess-only harness submit tools — validate + write artifacts under run_dir.
|
|
3
|
+
* Loaded via `pi --no-extensions -e harness-subagent-submit.ts` for harness agents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
10
|
+
import { getHarnessPackageRoot } from "./lib/harness-paths.js";
|
|
11
|
+
import { evaluateHarnessSubagentToolCall } from "./lib/harness-subagent-policy.js";
|
|
12
|
+
import { executeSubmitPipeline } from "./lib/harness-subagent-submit-pipeline.js";
|
|
13
|
+
import { SUBMIT_TOOL_SPECS } from "./lib/harness-subagent-submit-registry.js";
|
|
14
|
+
|
|
15
|
+
// @ts-expect-error pi extensions run as ESM
|
|
16
|
+
const MODULE_URL = import.meta.url;
|
|
17
|
+
|
|
18
|
+
const DocumentSchema = Type.Object(
|
|
19
|
+
{
|
|
20
|
+
document: Type.Record(Type.String(), Type.Unknown(), {
|
|
21
|
+
description:
|
|
22
|
+
"Plan artifact fields (validated via plan-*.schema.json, persisted as canonical YAML on disk)",
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
{ additionalProperties: false },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function resolveRunContext(): {
|
|
29
|
+
projectRoot: string;
|
|
30
|
+
specsDir: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
runDirEnv?: string;
|
|
33
|
+
agentId: string;
|
|
34
|
+
} {
|
|
35
|
+
const projectRoot = process.env.HARNESS_PKG_ROOT ?? process.cwd();
|
|
36
|
+
const specsDir = join(projectRoot, ".pi", "harness", "specs");
|
|
37
|
+
const runId = process.env.HARNESS_RUN_ID?.trim() ?? "";
|
|
38
|
+
const runDirEnv = process.env.HARNESS_RUN_DIR?.trim();
|
|
39
|
+
const agentId = process.env.HARNESS_AGENT_ID?.trim() ?? "";
|
|
40
|
+
return { projectRoot, specsDir, runId, runDirEnv, agentId };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSubprocessHarness(): boolean {
|
|
44
|
+
return (
|
|
45
|
+
process.env.PI_HARNESS_SUBPROCESS === "1" &&
|
|
46
|
+
Boolean(process.env.HARNESS_RUN_ID?.trim())
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
51
|
+
if (!claimExtensionLoad("harness-subagent-submit", MODULE_URL)) return;
|
|
52
|
+
// Option A: only load submit tools in subprocess (`-e` bundle), not parent discovery.
|
|
53
|
+
if (process.env.PI_HARNESS_SUBPROCESS !== "1") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const _packageRoot = getHarnessPackageRoot(MODULE_URL);
|
|
58
|
+
|
|
59
|
+
pi.on("tool_call", async (event) => {
|
|
60
|
+
if (!event.toolName.startsWith("submit_")) return undefined;
|
|
61
|
+
const subprocessOk = isSubprocessHarness();
|
|
62
|
+
if (!subprocessOk) {
|
|
63
|
+
return {
|
|
64
|
+
block: true,
|
|
65
|
+
reason:
|
|
66
|
+
"harness-subagent-submit: submit_* tools are only available in harness subagent subprocesses.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const { agentId } = resolveRunContext();
|
|
70
|
+
if (!agentId) {
|
|
71
|
+
return {
|
|
72
|
+
block: true,
|
|
73
|
+
reason:
|
|
74
|
+
"harness-subagent-submit: HARNESS_AGENT_ID is required for submit tools.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const decision = evaluateHarnessSubagentToolCall(
|
|
78
|
+
event.toolName,
|
|
79
|
+
event.input as Record<string, unknown>,
|
|
80
|
+
agentId,
|
|
81
|
+
);
|
|
82
|
+
if (decision.action === "block") {
|
|
83
|
+
return { block: true, reason: decision.reason };
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
for (const spec of SUBMIT_TOOL_SPECS) {
|
|
89
|
+
pi.registerTool({
|
|
90
|
+
name: spec.toolName,
|
|
91
|
+
label: spec.toolName.replace(/^submit_/, "Submit "),
|
|
92
|
+
description: `Terminal harness artifact submit for ${spec.agents.join(", ")}. Call once with the full schema document before ending the turn.`,
|
|
93
|
+
parameters: DocumentSchema,
|
|
94
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
95
|
+
if (!isSubprocessHarness()) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: "submit tools require PI_HARNESS_SUBPROCESS and HARNESS_RUN_ID",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
details: {},
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const { projectRoot, specsDir, runId, runDirEnv, agentId } =
|
|
108
|
+
resolveRunContext();
|
|
109
|
+
if (!spec.agents.includes(agentId)) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `${spec.toolName} is not allowed for agent ${agentId}`,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
details: { agentId, tool: spec.toolName },
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const document = (params as { document?: Record<string, unknown> })
|
|
122
|
+
.document;
|
|
123
|
+
if (!document || typeof document !== "object") {
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: "text", text: "document object is required" }],
|
|
126
|
+
details: {},
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const result = await executeSubmitPipeline({
|
|
131
|
+
projectRoot,
|
|
132
|
+
specsDir,
|
|
133
|
+
spec,
|
|
134
|
+
agentId,
|
|
135
|
+
document,
|
|
136
|
+
runId,
|
|
137
|
+
runDirEnv,
|
|
138
|
+
});
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: `Validation failed:\n${(result.validation_errors ?? []).join("\n")}`,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
isError: true,
|
|
148
|
+
details: result,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const lines = [`ok: wrote ${result.artifact_path}`];
|
|
152
|
+
if (result.lane_result?.messenger_posted) {
|
|
153
|
+
lines.push("messenger updated");
|
|
154
|
+
}
|
|
155
|
+
if (result.human_required) {
|
|
156
|
+
lines.push("human_required: parent must call ask_user");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
160
|
+
details: result as unknown,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Absolute path to the subprocess submit extension (Option A). */
|
|
168
|
+
export function harnessSubagentSubmitExtensionPath(
|
|
169
|
+
packageRoot: string,
|
|
170
|
+
): string {
|
|
171
|
+
return join(packageRoot, ".pi", "extensions", "harness-subagent-submit.ts");
|
|
172
|
+
}
|
|
@@ -127,6 +127,7 @@ function propsFromRun(
|
|
|
127
127
|
): Record<string, unknown> {
|
|
128
128
|
return {
|
|
129
129
|
harness_run_id: runId,
|
|
130
|
+
run_id: runId,
|
|
130
131
|
harness_plan_id: planId,
|
|
131
132
|
harness_phase: phase,
|
|
132
133
|
pi_session_id: distinctId,
|
|
@@ -134,6 +135,28 @@ function propsFromRun(
|
|
|
134
135
|
};
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function normalizedRunId(
|
|
139
|
+
data: Record<string, unknown>,
|
|
140
|
+
trace: TraceState | null,
|
|
141
|
+
distinctId: string,
|
|
142
|
+
): string {
|
|
143
|
+
const fromData = [
|
|
144
|
+
data.harness_run_id,
|
|
145
|
+
data.run_id,
|
|
146
|
+
data.runId,
|
|
147
|
+
data.debate_id,
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of fromData) {
|
|
150
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (typeof trace?.run_id === "string" && trace.run_id.length > 0) {
|
|
155
|
+
return trace.run_id;
|
|
156
|
+
}
|
|
157
|
+
return distinctId;
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
function mapCustomEntry(
|
|
138
161
|
customType: string,
|
|
139
162
|
data: Record<string, unknown>,
|
|
@@ -144,11 +167,9 @@ function mapCustomEntry(
|
|
|
144
167
|
event: HarnessPostHogEventName;
|
|
145
168
|
properties: Record<string, unknown>;
|
|
146
169
|
} | null {
|
|
147
|
-
const runId =
|
|
148
|
-
(typeof data.run_id === "string" && data.run_id) ||
|
|
149
|
-
trace?.run_id ||
|
|
150
|
-
distinctId;
|
|
170
|
+
const runId = normalizedRunId(data, trace, distinctId);
|
|
151
171
|
const planId =
|
|
172
|
+
(typeof data.harness_plan_id === "string" && data.harness_plan_id) ||
|
|
152
173
|
(typeof data.plan_id === "string" && data.plan_id) ||
|
|
153
174
|
policy?.planId ||
|
|
154
175
|
trace?.plan_id ||
|
|
@@ -185,6 +206,7 @@ function mapCustomEntry(
|
|
|
185
206
|
event: "harness_debate_consensus",
|
|
186
207
|
properties: {
|
|
187
208
|
...base,
|
|
209
|
+
debate_id: String(data.debate_id ?? runId),
|
|
188
210
|
consensus_id:
|
|
189
211
|
typeof data.debate_id === "string" ? data.debate_id : runId,
|
|
190
212
|
outcome: String(kind),
|
|
@@ -195,6 +217,8 @@ function mapCustomEntry(
|
|
|
195
217
|
event: "harness_debate_round",
|
|
196
218
|
properties: {
|
|
197
219
|
...base,
|
|
220
|
+
debate_id: String(data.debate_id ?? runId),
|
|
221
|
+
round_index: Number(data.round_index ?? data.round ?? 0),
|
|
198
222
|
round: Number(data.round_index ?? data.round ?? 0),
|
|
199
223
|
outcome: String(kind ?? "round"),
|
|
200
224
|
},
|
|
@@ -206,6 +230,7 @@ function mapCustomEntry(
|
|
|
206
230
|
event: "harness_debate_consensus",
|
|
207
231
|
properties: {
|
|
208
232
|
...base,
|
|
233
|
+
debate_id: String(data.debate_id ?? runId),
|
|
209
234
|
consensus_id:
|
|
210
235
|
typeof data.consensus_id === "string"
|
|
211
236
|
? data.consensus_id
|
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
PLAN_DEBATE_PARTICIPANTS,
|
|
12
12
|
POST_EXECUTE_DEBATE_PARTICIPANTS,
|
|
13
13
|
} from "../../lib/debate-orchestrator-types.js";
|
|
14
|
+
import {
|
|
15
|
+
isHarnessBudgetEnforceOn,
|
|
16
|
+
shouldEmitBlockingBudgetExhausted,
|
|
17
|
+
} from "../../lib/harness-budget-enforce.js";
|
|
14
18
|
import {
|
|
15
19
|
type DebateState,
|
|
16
20
|
getDebateState,
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
} from "./debate-bus-state.js";
|
|
22
26
|
import {
|
|
23
27
|
type DebateProfile,
|
|
28
|
+
PLAN_BUDGET_FAST,
|
|
24
29
|
PLAN_BUDGET_LIGHT,
|
|
25
30
|
PLAN_BUDGET_STANDARD,
|
|
26
31
|
} from "./plan-debate-eligibility.js";
|
|
@@ -75,7 +80,8 @@ const THRESHOLDS = {
|
|
|
75
80
|
architecture: 0.8,
|
|
76
81
|
test_integrity: 0.8,
|
|
77
82
|
};
|
|
78
|
-
const HARD_STOP_DEBATE_CAPS =
|
|
83
|
+
const HARD_STOP_DEBATE_CAPS =
|
|
84
|
+
process.env.HARNESS_DEBATE_HARD_STOP === "true" && isHarnessBudgetEnforceOn();
|
|
79
85
|
|
|
80
86
|
const PLAN_BUDGET = PLAN_BUDGET_STANDARD;
|
|
81
87
|
|
|
@@ -108,15 +114,40 @@ export function capsForDebate(
|
|
|
108
114
|
} {
|
|
109
115
|
if (isPlanDebateId(debateId)) {
|
|
110
116
|
const active = profile ?? getDebateState()?.debate_profile ?? "standard";
|
|
111
|
-
const budget =
|
|
112
|
-
|
|
117
|
+
const budget =
|
|
118
|
+
active === "light"
|
|
119
|
+
? PLAN_BUDGET_LIGHT
|
|
120
|
+
: active === "fast"
|
|
121
|
+
? PLAN_BUDGET_FAST
|
|
122
|
+
: PLAN_BUDGET;
|
|
123
|
+
const caps = { name: "plan" as const, ...budget };
|
|
124
|
+
if (!isHarnessBudgetEnforceOn()) {
|
|
125
|
+
return {
|
|
126
|
+
...caps,
|
|
127
|
+
max_rounds: caps.max_rounds,
|
|
128
|
+
max_exchanges_per_round: Math.max(caps.max_exchanges_per_round, 2),
|
|
129
|
+
round_token_cap: caps.round_token_cap * 2,
|
|
130
|
+
debate_global_cap: caps.debate_global_cap * 2,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return caps;
|
|
113
134
|
}
|
|
114
|
-
|
|
115
|
-
name: "aggressive",
|
|
135
|
+
const caps = {
|
|
136
|
+
name: "aggressive" as const,
|
|
116
137
|
min_focus_rounds: 1,
|
|
117
138
|
max_exchanges_per_round: 1,
|
|
118
139
|
...AGGRESSIVE_BUDGET,
|
|
119
140
|
};
|
|
141
|
+
if (!isHarnessBudgetEnforceOn()) {
|
|
142
|
+
return {
|
|
143
|
+
...caps,
|
|
144
|
+
max_rounds: caps.max_rounds,
|
|
145
|
+
max_exchanges_per_round: Math.max(caps.max_exchanges_per_round, 2),
|
|
146
|
+
round_token_cap: caps.round_token_cap * 2,
|
|
147
|
+
debate_global_cap: caps.debate_global_cap * 2,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return caps;
|
|
120
151
|
}
|
|
121
152
|
|
|
122
153
|
function participantAllowed(
|
|
@@ -280,7 +311,19 @@ async function emitBudgetExhausted(
|
|
|
280
311
|
},
|
|
281
312
|
};
|
|
282
313
|
hooks.appendEntry("harness-debate-envelope", envelope);
|
|
283
|
-
|
|
314
|
+
if (shouldEmitBlockingBudgetExhausted()) {
|
|
315
|
+
hooks.appendEntry("harness-budget-exhausted", envelope.payload);
|
|
316
|
+
} else {
|
|
317
|
+
const telemetryPayload = {
|
|
318
|
+
...(envelope.payload as Record<string, unknown>),
|
|
319
|
+
telemetry_only: true,
|
|
320
|
+
};
|
|
321
|
+
hooks.appendEntry("harness-debate-budget-telemetry", telemetryPayload);
|
|
322
|
+
hooks.appendEntry("harness-budget-telemetry", {
|
|
323
|
+
...telemetryPayload,
|
|
324
|
+
source: "debate-bus",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
284
327
|
await writeDebateEvent(state.debate_id, envelope);
|
|
285
328
|
}
|
|
286
329
|
|