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.
Files changed (61) hide show
  1. package/.agents/skills/harness-governor/SKILL.md +11 -0
  2. package/.agents/skills/harness-orchestration/SKILL.md +3 -1
  3. package/.agents/skills/harness-plan/SKILL.md +5 -5
  4. package/.pi/agents/harness/adversary.md +1 -1
  5. package/.pi/agents/harness/evaluator.md +1 -1
  6. package/.pi/agents/harness/executor.md +1 -1
  7. package/.pi/agents/harness/incident-recorder.md +1 -1
  8. package/.pi/agents/harness/meta-optimizer.md +1 -1
  9. package/.pi/agents/harness/planning/decompose.md +4 -33
  10. package/.pi/agents/harness/planning/execution-plan-author.md +3 -2
  11. package/.pi/agents/harness/planning/hypothesis-validator.md +3 -2
  12. package/.pi/agents/harness/planning/hypothesis.md +4 -27
  13. package/.pi/agents/harness/planning/implementation-researcher.md +3 -2
  14. package/.pi/agents/harness/planning/plan-adversary.md +2 -3
  15. package/.pi/agents/harness/planning/plan-evaluator.md +3 -2
  16. package/.pi/agents/harness/planning/review-integrator.md +2 -3
  17. package/.pi/agents/harness/planning/scout-graphify.md +3 -22
  18. package/.pi/agents/harness/planning/scout-semantic.md +3 -18
  19. package/.pi/agents/harness/planning/scout-structure.md +3 -18
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +3 -2
  21. package/.pi/agents/harness/planning/stack-researcher.md +3 -2
  22. package/.pi/agents/harness/tie-breaker.md +1 -1
  23. package/.pi/agents/harness/trace-librarian.md +1 -1
  24. package/.pi/extensions/budget-guard.ts +33 -19
  25. package/.pi/extensions/harness-debate-tools.ts +42 -3
  26. package/.pi/extensions/harness-run-context.ts +96 -2
  27. package/.pi/extensions/harness-subagent-submit.ts +195 -0
  28. package/.pi/extensions/lib/debate-bus-core.ts +42 -5
  29. package/.pi/extensions/lib/harness-subagent-policy.ts +45 -0
  30. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +82 -0
  31. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +172 -0
  32. package/.pi/extensions/lib/harness-subagents-bridge.ts +42 -0
  33. package/.pi/extensions/lib/plan-debate-gate.ts +12 -1
  34. package/.pi/extensions/lib/plan-debate-lane.ts +15 -0
  35. package/.pi/harness/agents.manifest.json +22 -22
  36. package/.pi/harness/docs/adrs/0037-subagent-submit-tools.md +31 -0
  37. package/.pi/harness/docs/adrs/0038-budget-telemetry-only.md +23 -0
  38. package/.pi/harness/docs/adrs/README.md +2 -0
  39. package/.pi/harness/specs/harness-executor-handoff.schema.json +19 -0
  40. package/.pi/harness/specs/harness-human-required.schema.json +16 -0
  41. package/.pi/harness/specs/plan-scout-findings.schema.json +19 -0
  42. package/.pi/lib/harness-agent-output.ts +45 -0
  43. package/.pi/lib/harness-budget-enforce.ts +18 -0
  44. package/.pi/lib/harness-schema-validate.ts +89 -0
  45. package/.pi/lib/harness-spawn-parse.ts +86 -0
  46. package/.pi/lib/harness-subagent-submit-path.ts +41 -0
  47. package/.pi/lib/harness-ui-state.ts +15 -2
  48. package/.pi/prompts/harness-auto.md +2 -2
  49. package/.pi/prompts/harness-plan.md +9 -7
  50. package/.pi/prompts/harness-run.md +2 -2
  51. package/.pi/scripts/harness-verify.mjs +2 -0
  52. package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
  53. package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
  54. package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
  55. package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
  56. package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
  57. package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
  58. package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
  59. package/CHANGELOG.md +10 -0
  60. package/package.json +4 -2
  61. 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 { mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { dirname } from "node:path";
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 = process.env.HARNESS_DEBATE_HARD_STOP === "true";
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
- return { name: "plan", ...budget };
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
- return {
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
- hooks.appendEntry("harness-budget-exhausted", envelope.payload);
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
+ }