ultimate-pi 0.15.0 → 0.16.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 +42 -3
- package/.pi/extensions/harness-run-context.ts +96 -2
- package/.pi/extensions/harness-subagent-submit.ts +195 -0
- package/.pi/extensions/lib/debate-bus-core.ts +42 -5
- package/.pi/extensions/lib/harness-subagent-policy.ts +45 -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 +42 -0
- package/.pi/extensions/lib/plan-debate-gate.ts +12 -1
- package/.pi/extensions/lib/plan-debate-lane.ts +15 -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/specs/harness-executor-handoff.schema.json +19 -0
- package/.pi/harness/specs/harness-human-required.schema.json +16 -0
- 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/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +9 -7
- package/.pi/prompts/harness-run.md +2 -2
- package/.pi/scripts/harness-verify.mjs +2 -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 +10 -0
- package/package.json +4 -2
- package/vendor/pi-subagents/src/subagents.ts +29 -3
|
@@ -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;
|
|
@@ -1030,6 +1065,65 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1030
1065
|
},
|
|
1031
1066
|
});
|
|
1032
1067
|
|
|
1068
|
+
pi.registerTool({
|
|
1069
|
+
name: "harness_artifact_ready",
|
|
1070
|
+
label: "Harness Artifact Ready",
|
|
1071
|
+
description:
|
|
1072
|
+
"Check that harness artifact paths exist under the active run (no JSON parsing).",
|
|
1073
|
+
parameters: Type.Object({
|
|
1074
|
+
paths: Type.Array(Type.String(), {
|
|
1075
|
+
minItems: 1,
|
|
1076
|
+
description:
|
|
1077
|
+
"Relative paths under the run dir, e.g. artifacts/decomposition.yaml",
|
|
1078
|
+
}),
|
|
1079
|
+
}),
|
|
1080
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1081
|
+
const entries = getEntries(ctx);
|
|
1082
|
+
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
1083
|
+
if (!runCtx?.run_id) {
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: "text", text: "No active harness run." }],
|
|
1086
|
+
details: {},
|
|
1087
|
+
isError: true,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
const paths = (params as { paths?: string[] }).paths ?? [];
|
|
1091
|
+
const projectRoot = process.cwd();
|
|
1092
|
+
const runRoot = join(
|
|
1093
|
+
projectRoot,
|
|
1094
|
+
".pi",
|
|
1095
|
+
"harness",
|
|
1096
|
+
"runs",
|
|
1097
|
+
runCtx.run_id,
|
|
1098
|
+
);
|
|
1099
|
+
const missing: string[] = [];
|
|
1100
|
+
const present: string[] = [];
|
|
1101
|
+
for (const rel of paths) {
|
|
1102
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
1103
|
+
const abs = join(runRoot, normalized);
|
|
1104
|
+
try {
|
|
1105
|
+
await access(abs, constants.R_OK);
|
|
1106
|
+
present.push(normalized);
|
|
1107
|
+
} catch {
|
|
1108
|
+
missing.push(normalized);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const ok = missing.length === 0;
|
|
1112
|
+
return {
|
|
1113
|
+
content: [
|
|
1114
|
+
{
|
|
1115
|
+
type: "text",
|
|
1116
|
+
text: ok
|
|
1117
|
+
? `All ${present.length} artifact(s) present.`
|
|
1118
|
+
: `Missing: ${missing.join(", ")}`,
|
|
1119
|
+
},
|
|
1120
|
+
],
|
|
1121
|
+
details: { ok, present, missing, run_id: runCtx.run_id },
|
|
1122
|
+
isError: !ok,
|
|
1123
|
+
};
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1033
1127
|
pi.registerCommand("harness-use-run", {
|
|
1034
1128
|
description: "Point this session at an existing run directory (recovery)",
|
|
1035
1129
|
handler: async (args, ctx) => {
|
|
@@ -0,0 +1,195 @@
|
|
|
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: "Full artifact document matching the harness JSON schema",
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
{ additionalProperties: false },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function resolveRunContext(): {
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
specsDir: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
runDirEnv?: string;
|
|
32
|
+
agentId: string;
|
|
33
|
+
} {
|
|
34
|
+
const projectRoot = process.env.HARNESS_PKG_ROOT ?? process.cwd();
|
|
35
|
+
const specsDir = join(projectRoot, ".pi", "harness", "specs");
|
|
36
|
+
const runId = process.env.HARNESS_RUN_ID?.trim() ?? "";
|
|
37
|
+
const runDirEnv = process.env.HARNESS_RUN_DIR?.trim();
|
|
38
|
+
const agentId = process.env.HARNESS_AGENT_ID?.trim() ?? "";
|
|
39
|
+
return { projectRoot, specsDir, runId, runDirEnv, agentId };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isSubprocessHarness(): boolean {
|
|
43
|
+
return (
|
|
44
|
+
process.env.PI_HARNESS_SUBPROCESS === "1" &&
|
|
45
|
+
Boolean(process.env.HARNESS_RUN_ID?.trim())
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
50
|
+
if (!claimExtensionLoad("harness-subagent-submit", MODULE_URL)) return;
|
|
51
|
+
// Option A: only load submit tools in subprocess (`-e` bundle), not parent discovery.
|
|
52
|
+
if (process.env.PI_HARNESS_SUBPROCESS !== "1") {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const _packageRoot = getHarnessPackageRoot(MODULE_URL);
|
|
57
|
+
|
|
58
|
+
pi.on("tool_call", async (event) => {
|
|
59
|
+
if (!event.toolName.startsWith("submit_")) return undefined;
|
|
60
|
+
const subprocessOk = isSubprocessHarness();
|
|
61
|
+
// #region agent log
|
|
62
|
+
fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"X-Debug-Session-Id": "2ca12b",
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
sessionId: "2ca12b",
|
|
70
|
+
hypothesisId: "H2",
|
|
71
|
+
location: "harness-subagent-submit.ts:tool_call",
|
|
72
|
+
message: "submit tool_call gate",
|
|
73
|
+
data: {
|
|
74
|
+
toolName: event.toolName,
|
|
75
|
+
PI_HARNESS_SUBPROCESS: process.env.PI_HARNESS_SUBPROCESS,
|
|
76
|
+
HARNESS_RUN_ID: process.env.HARNESS_RUN_ID ?? null,
|
|
77
|
+
HARNESS_RUN_DIR: process.env.HARNESS_RUN_DIR ?? null,
|
|
78
|
+
HARNESS_AGENT_ID: process.env.HARNESS_AGENT_ID ?? null,
|
|
79
|
+
subprocessOk,
|
|
80
|
+
},
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
}),
|
|
83
|
+
}).catch(() => {});
|
|
84
|
+
// #endregion
|
|
85
|
+
if (!subprocessOk) {
|
|
86
|
+
return {
|
|
87
|
+
block: true,
|
|
88
|
+
reason:
|
|
89
|
+
"harness-subagent-submit: submit_* tools are only available in harness subagent subprocesses.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const { agentId } = resolveRunContext();
|
|
93
|
+
if (!agentId) {
|
|
94
|
+
return {
|
|
95
|
+
block: true,
|
|
96
|
+
reason:
|
|
97
|
+
"harness-subagent-submit: HARNESS_AGENT_ID is required for submit tools.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const decision = evaluateHarnessSubagentToolCall(
|
|
101
|
+
event.toolName,
|
|
102
|
+
event.input as Record<string, unknown>,
|
|
103
|
+
agentId,
|
|
104
|
+
);
|
|
105
|
+
if (decision.action === "block") {
|
|
106
|
+
return { block: true, reason: decision.reason };
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const spec of SUBMIT_TOOL_SPECS) {
|
|
112
|
+
pi.registerTool({
|
|
113
|
+
name: spec.toolName,
|
|
114
|
+
label: spec.toolName.replace(/^submit_/, "Submit "),
|
|
115
|
+
description: `Terminal harness artifact submit for ${spec.agents.join(", ")}. Call once with the full schema document before ending the turn.`,
|
|
116
|
+
parameters: DocumentSchema,
|
|
117
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
118
|
+
if (!isSubprocessHarness()) {
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: "submit tools require PI_HARNESS_SUBPROCESS and HARNESS_RUN_ID",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
details: {},
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const { projectRoot, specsDir, runId, runDirEnv, agentId } =
|
|
131
|
+
resolveRunContext();
|
|
132
|
+
if (!spec.agents.includes(agentId)) {
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: `${spec.toolName} is not allowed for agent ${agentId}`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
details: { agentId, tool: spec.toolName },
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const document = (params as { document?: Record<string, unknown> })
|
|
145
|
+
.document;
|
|
146
|
+
if (!document || typeof document !== "object") {
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: "document object is required" }],
|
|
149
|
+
details: {},
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const result = await executeSubmitPipeline({
|
|
154
|
+
projectRoot,
|
|
155
|
+
specsDir,
|
|
156
|
+
spec,
|
|
157
|
+
agentId,
|
|
158
|
+
document,
|
|
159
|
+
runId,
|
|
160
|
+
runDirEnv,
|
|
161
|
+
});
|
|
162
|
+
if (!result.ok) {
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: `Validation failed:\n${(result.validation_errors ?? []).join("\n")}`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
isError: true,
|
|
171
|
+
details: result,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const lines = [`ok: wrote ${result.artifact_path}`];
|
|
175
|
+
if (result.lane_result?.messenger_posted) {
|
|
176
|
+
lines.push("messenger updated");
|
|
177
|
+
}
|
|
178
|
+
if (result.human_required) {
|
|
179
|
+
lines.push("human_required: parent must call ask_user");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
183
|
+
details: result as unknown,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Absolute path to the subprocess submit extension (Option A). */
|
|
191
|
+
export function harnessSubagentSubmitExtensionPath(
|
|
192
|
+
packageRoot: string,
|
|
193
|
+
): string {
|
|
194
|
+
return join(packageRoot, ".pi", "extensions", "harness-subagent-submit.ts");
|
|
195
|
+
}
|
|
@@ -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,
|
|
@@ -75,7 +79,8 @@ const THRESHOLDS = {
|
|
|
75
79
|
architecture: 0.8,
|
|
76
80
|
test_integrity: 0.8,
|
|
77
81
|
};
|
|
78
|
-
const HARD_STOP_DEBATE_CAPS =
|
|
82
|
+
const HARD_STOP_DEBATE_CAPS =
|
|
83
|
+
process.env.HARNESS_DEBATE_HARD_STOP === "true" && isHarnessBudgetEnforceOn();
|
|
79
84
|
|
|
80
85
|
const PLAN_BUDGET = PLAN_BUDGET_STANDARD;
|
|
81
86
|
|
|
@@ -109,14 +114,34 @@ export function capsForDebate(
|
|
|
109
114
|
if (isPlanDebateId(debateId)) {
|
|
110
115
|
const active = profile ?? getDebateState()?.debate_profile ?? "standard";
|
|
111
116
|
const budget = active === "light" ? PLAN_BUDGET_LIGHT : PLAN_BUDGET;
|
|
112
|
-
|
|
117
|
+
const caps = { name: "plan" as const, ...budget };
|
|
118
|
+
if (!isHarnessBudgetEnforceOn()) {
|
|
119
|
+
return {
|
|
120
|
+
...caps,
|
|
121
|
+
max_rounds: 999,
|
|
122
|
+
max_exchanges_per_round: 99,
|
|
123
|
+
round_token_cap: caps.round_token_cap * 100,
|
|
124
|
+
debate_global_cap: caps.debate_global_cap * 100,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return caps;
|
|
113
128
|
}
|
|
114
|
-
|
|
115
|
-
name: "aggressive",
|
|
129
|
+
const caps = {
|
|
130
|
+
name: "aggressive" as const,
|
|
116
131
|
min_focus_rounds: 1,
|
|
117
132
|
max_exchanges_per_round: 1,
|
|
118
133
|
...AGGRESSIVE_BUDGET,
|
|
119
134
|
};
|
|
135
|
+
if (!isHarnessBudgetEnforceOn()) {
|
|
136
|
+
return {
|
|
137
|
+
...caps,
|
|
138
|
+
max_rounds: 999,
|
|
139
|
+
max_exchanges_per_round: 99,
|
|
140
|
+
round_token_cap: caps.round_token_cap * 100,
|
|
141
|
+
debate_global_cap: caps.debate_global_cap * 100,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return caps;
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
function participantAllowed(
|
|
@@ -280,7 +305,19 @@ async function emitBudgetExhausted(
|
|
|
280
305
|
},
|
|
281
306
|
};
|
|
282
307
|
hooks.appendEntry("harness-debate-envelope", envelope);
|
|
283
|
-
|
|
308
|
+
if (shouldEmitBlockingBudgetExhausted()) {
|
|
309
|
+
hooks.appendEntry("harness-budget-exhausted", envelope.payload);
|
|
310
|
+
} else {
|
|
311
|
+
const telemetryPayload = {
|
|
312
|
+
...(envelope.payload as Record<string, unknown>),
|
|
313
|
+
telemetry_only: true,
|
|
314
|
+
};
|
|
315
|
+
hooks.appendEntry("harness-debate-budget-telemetry", telemetryPayload);
|
|
316
|
+
hooks.appendEntry("harness-budget-telemetry", {
|
|
317
|
+
...telemetryPayload,
|
|
318
|
+
source: "debate-bus",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
284
321
|
await writeDebateEvent(state.debate_id, envelope);
|
|
285
322
|
}
|
|
286
323
|
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Per-agent tool policy for harness/* subagents (defense in depth with frontmatter).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
isSubmitToolName,
|
|
7
|
+
SUBMIT_TOOLS_BY_AGENT,
|
|
8
|
+
} from "./harness-subagent-submit-registry.js";
|
|
5
9
|
import {
|
|
6
10
|
evaluateSubagentToolCall,
|
|
7
11
|
type ToolCallDecision,
|
|
@@ -107,6 +111,45 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
if (!isHarnessPackageAgent(agentType)) {
|
|
114
|
+
if (
|
|
115
|
+
isSubmitToolName(toolName) &&
|
|
116
|
+
process.env.PI_HARNESS_SUBPROCESS !== "1"
|
|
117
|
+
) {
|
|
118
|
+
return {
|
|
119
|
+
action: "block",
|
|
120
|
+
reason:
|
|
121
|
+
"harness-subagent-policy: submit_* tools are subprocess-only; parent orchestrator must use harness_artifact_ready and write_harness_yaml for merges.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { action: "allow" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isSubmitToolName(toolName)) {
|
|
128
|
+
if (process.env.PI_HARNESS_SUBPROCESS !== "1") {
|
|
129
|
+
return {
|
|
130
|
+
action: "block",
|
|
131
|
+
reason:
|
|
132
|
+
"harness-subagent-policy: submit_* tools are not available in the parent harness session.",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (toolName === "submit_human_required") {
|
|
136
|
+
const kind = classifyHarnessAgent(agentType);
|
|
137
|
+
if (kind === "executor") {
|
|
138
|
+
return {
|
|
139
|
+
action: "block",
|
|
140
|
+
reason:
|
|
141
|
+
"submit_human_required is not available for harness/executor.",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { action: "allow" };
|
|
145
|
+
}
|
|
146
|
+
const allowed = SUBMIT_TOOLS_BY_AGENT[agentType];
|
|
147
|
+
if (!allowed?.has(toolName)) {
|
|
148
|
+
return {
|
|
149
|
+
action: "block",
|
|
150
|
+
reason: `harness-subagent-policy: ${toolName} is not allowed for ${agentType}.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
110
153
|
return { action: "allow" };
|
|
111
154
|
}
|
|
112
155
|
|
|
@@ -153,6 +196,8 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
153
196
|
return { action: "allow" };
|
|
154
197
|
}
|
|
155
198
|
|
|
199
|
+
export { isSubmitToolName } from "./harness-subagent-submit-registry.js";
|
|
200
|
+
|
|
156
201
|
export function harnessSubagentPhaseHint(agentType: string): string | null {
|
|
157
202
|
if (isHarnessPlanningAgent(agentType)) {
|
|
158
203
|
return "plan";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared write pipeline for harness subagent submit tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { validateAgainstHarnessSchema } from "../../lib/harness-schema-validate.js";
|
|
8
|
+
import { resolveGuardedRunDir } from "../../lib/harness-subagent-submit-path.js";
|
|
9
|
+
import { writeYamlFile } from "../../lib/harness-yaml.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveArtifactRelPath,
|
|
12
|
+
type SubmitToolSpec,
|
|
13
|
+
} from "./harness-subagent-submit-registry.js";
|
|
14
|
+
import {
|
|
15
|
+
type ApplyDebateLaneResult,
|
|
16
|
+
applyDebateLaneFromDoc,
|
|
17
|
+
} from "./plan-debate-lane.js";
|
|
18
|
+
|
|
19
|
+
export interface SubmitPipelineResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
artifact_path?: string;
|
|
22
|
+
validation_errors?: string[];
|
|
23
|
+
lane_result?: ApplyDebateLaneResult;
|
|
24
|
+
human_required?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function executeSubmitPipeline(opts: {
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
specsDir: string;
|
|
30
|
+
spec: SubmitToolSpec;
|
|
31
|
+
agentId: string;
|
|
32
|
+
document: Record<string, unknown>;
|
|
33
|
+
runId: string;
|
|
34
|
+
runDirEnv?: string;
|
|
35
|
+
}): Promise<SubmitPipelineResult> {
|
|
36
|
+
const runResolved = await resolveGuardedRunDir({
|
|
37
|
+
projectRoot: opts.projectRoot,
|
|
38
|
+
runId: opts.runId,
|
|
39
|
+
runDirEnv: opts.runDirEnv,
|
|
40
|
+
});
|
|
41
|
+
if (!runResolved.ok) {
|
|
42
|
+
return { ok: false, validation_errors: [runResolved.error] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const validation = await validateAgainstHarnessSchema(
|
|
46
|
+
opts.specsDir,
|
|
47
|
+
opts.spec.schemaFile,
|
|
48
|
+
opts.document,
|
|
49
|
+
);
|
|
50
|
+
if (!validation.ok) {
|
|
51
|
+
return { ok: false, validation_errors: validation.errors };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const relPath = resolveArtifactRelPath(opts.spec, opts.document);
|
|
55
|
+
const absPath = join(runResolved.runDir, relPath);
|
|
56
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
57
|
+
await writeYamlFile(absPath, opts.document);
|
|
58
|
+
|
|
59
|
+
let laneResult: ApplyDebateLaneResult | undefined;
|
|
60
|
+
if (opts.spec.debateLane) {
|
|
61
|
+
laneResult = await applyDebateLaneFromDoc({
|
|
62
|
+
runDir: runResolved.runDir,
|
|
63
|
+
lane: opts.spec.debateLane,
|
|
64
|
+
doc: opts.document,
|
|
65
|
+
});
|
|
66
|
+
if (!laneResult.ok) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
artifact_path: relPath,
|
|
70
|
+
validation_errors: laneResult.errors,
|
|
71
|
+
lane_result: laneResult,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
artifact_path: relPath,
|
|
79
|
+
lane_result: laneResult,
|
|
80
|
+
human_required: opts.spec.humanRequired === true,
|
|
81
|
+
};
|
|
82
|
+
}
|