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.
Files changed (90) 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 +54 -6
  26. package/.pi/extensions/harness-run-context.ts +108 -2
  27. package/.pi/extensions/harness-subagent-submit.ts +172 -0
  28. package/.pi/extensions/harness-telemetry.ts +29 -4
  29. package/.pi/extensions/lib/debate-bus-core.ts +49 -6
  30. package/.pi/extensions/lib/harness-subagent-auth.ts +104 -19
  31. package/.pi/extensions/lib/harness-subagent-policy.ts +59 -0
  32. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +82 -0
  33. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +172 -0
  34. package/.pi/extensions/lib/harness-subagents-bridge.ts +127 -0
  35. package/.pi/extensions/lib/plan-debate-eligibility.ts +61 -8
  36. package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
  37. package/.pi/extensions/lib/plan-debate-gate.ts +92 -18
  38. package/.pi/extensions/lib/plan-debate-lane.ts +15 -0
  39. package/.pi/extensions/lib/plan-debate-lanes.ts +27 -3
  40. package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
  41. package/.pi/extensions/lib/plan-messenger.ts +4 -0
  42. package/.pi/extensions/lib/plan-review-gate.ts +51 -0
  43. package/.pi/extensions/trace-recorder.ts +1 -0
  44. package/.pi/harness/agents.manifest.json +22 -22
  45. package/.pi/harness/docs/adrs/0037-subagent-submit-tools.md +31 -0
  46. package/.pi/harness/docs/adrs/0038-budget-telemetry-only.md +23 -0
  47. package/.pi/harness/docs/adrs/README.md +2 -0
  48. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
  49. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
  50. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
  51. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
  52. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
  53. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +40 -17
  54. package/.pi/harness/specs/harness-executor-handoff.schema.json +19 -0
  55. package/.pi/harness/specs/harness-human-required.schema.json +16 -0
  56. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  57. package/.pi/harness/specs/plan-scout-findings.schema.json +19 -0
  58. package/.pi/lib/harness-agent-output.ts +45 -0
  59. package/.pi/lib/harness-budget-enforce.ts +18 -0
  60. package/.pi/lib/harness-schema-validate.ts +89 -0
  61. package/.pi/lib/harness-spawn-parse.ts +86 -0
  62. package/.pi/lib/harness-subagent-submit-path.ts +41 -0
  63. package/.pi/lib/harness-ui-state.ts +15 -2
  64. package/.pi/model-router.example.json +13 -4
  65. package/.pi/prompts/harness-auto.md +2 -2
  66. package/.pi/prompts/harness-plan.md +34 -14
  67. package/.pi/prompts/harness-run.md +2 -2
  68. package/.pi/prompts/harness-setup.md +4 -4
  69. package/.pi/scripts/harness-generate-model-router.mjs +118 -36
  70. package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
  71. package/.pi/scripts/harness-sync-model-router.mjs +15 -2
  72. package/.pi/scripts/harness-verify.mjs +31 -0
  73. package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
  75. package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
  76. package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
  77. package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
  78. package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
  79. package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
  80. package/CHANGELOG.md +21 -0
  81. package/package.json +4 -2
  82. package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
  83. package/vendor/pi-model-router/extensions/commands.ts +4 -4
  84. package/vendor/pi-model-router/extensions/index.ts +21 -0
  85. package/vendor/pi-model-router/extensions/provider.ts +130 -79
  86. package/vendor/pi-model-router/extensions/routing.ts +148 -0
  87. package/vendor/pi-model-router/extensions/state.ts +3 -0
  88. package/vendor/pi-model-router/extensions/types.ts +9 -0
  89. package/vendor/pi-model-router/extensions/ui.ts +16 -2
  90. 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<{ agent: string; finalOutput?: string }>;
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 || !result.finalOutput?.trim()) continue;
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 { 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;
@@ -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 = process.env.HARNESS_DEBATE_HARD_STOP === "true";
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 = active === "light" ? PLAN_BUDGET_LIGHT : PLAN_BUDGET;
112
- return { name: "plan", ...budget };
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
- return {
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
- hooks.appendEntry("harness-budget-exhausted", envelope.payload);
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