ultimate-pi 0.16.0 → 0.18.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 (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-eval/SKILL.md +6 -21
  4. package/.agents/skills/harness-governor/SKILL.md +4 -3
  5. package/.agents/skills/harness-orchestration/SKILL.md +39 -51
  6. package/.agents/skills/harness-plan/SKILL.md +23 -12
  7. package/.agents/skills/harness-review/SKILL.md +52 -0
  8. package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
  9. package/.agents/skills/harness-steer/SKILL.md +14 -0
  10. package/.pi/agents/harness/adversary.md +3 -10
  11. package/.pi/agents/harness/evaluator.md +3 -12
  12. package/.pi/agents/harness/executor.md +12 -14
  13. package/.pi/agents/harness/planning/decompose.md +7 -4
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  15. package/.pi/agents/harness/planning/hypothesis.md +4 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
  17. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  18. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  19. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  20. package/.pi/agents/harness/planning/planning-context.md +48 -0
  21. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  22. package/.pi/agents/harness/planning/scout-graphify.md +3 -1
  23. package/.pi/agents/harness/planning/scout-semantic.md +3 -1
  24. package/.pi/agents/harness/planning/scout-structure.md +3 -1
  25. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  26. package/.pi/agents/harness/sentrux-steward.md +51 -0
  27. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  28. package/.pi/extensions/harness-debate-tools.ts +12 -3
  29. package/.pi/extensions/harness-live-widget.ts +27 -1
  30. package/.pi/extensions/harness-plan-approval.ts +62 -56
  31. package/.pi/extensions/harness-run-context.ts +553 -84
  32. package/.pi/extensions/harness-subagent-submit.ts +43 -33
  33. package/.pi/extensions/harness-telemetry.ts +29 -4
  34. package/.pi/extensions/lib/debate-bus-core.ts +15 -9
  35. package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
  36. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  37. package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
  38. package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
  39. package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
  40. package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
  41. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  42. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
  43. package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
  44. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  45. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  46. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  47. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  48. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  49. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  50. package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
  51. package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
  52. package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
  53. package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
  54. package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
  55. package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
  56. package/.pi/extensions/lib/plan-messenger.ts +4 -0
  57. package/.pi/extensions/lib/plan-review-gate.ts +59 -0
  58. package/.pi/extensions/lib/posthog-client.ts +76 -0
  59. package/.pi/extensions/policy-gate.ts +24 -19
  60. package/.pi/extensions/trace-recorder.ts +1 -0
  61. package/.pi/harness/agents.manifest.json +24 -16
  62. package/.pi/harness/corpus/cron.example +8 -0
  63. package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
  64. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  65. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  66. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  67. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  68. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
  69. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  70. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  71. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  72. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  73. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  74. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  75. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  76. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  77. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  78. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  79. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
  80. package/.pi/harness/docs/adrs/README.md +10 -0
  81. package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
  82. package/.pi/harness/docs/practice-map.md +110 -0
  83. package/.pi/harness/env.harness.template +5 -3
  84. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
  85. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
  86. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
  87. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
  88. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
  89. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  90. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
  91. package/.pi/harness/specs/README.md +1 -1
  92. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  93. package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
  94. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  95. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  96. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  97. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  98. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  99. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  100. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  101. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  102. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  103. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  104. package/.pi/harness/specs/steer-state.schema.json +20 -0
  105. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  106. package/.pi/lib/harness-repair-brief.ts +145 -0
  107. package/.pi/lib/harness-run-context.ts +591 -32
  108. package/.pi/lib/harness-ui-state.ts +87 -9
  109. package/.pi/model-router.example.json +13 -4
  110. package/.pi/prompts/harness-auto.md +9 -9
  111. package/.pi/prompts/harness-critic.md +3 -30
  112. package/.pi/prompts/harness-eval.md +4 -37
  113. package/.pi/prompts/harness-plan.md +139 -57
  114. package/.pi/prompts/harness-review.md +150 -15
  115. package/.pi/prompts/harness-run.md +62 -10
  116. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  117. package/.pi/prompts/harness-setup.md +4 -4
  118. package/.pi/prompts/harness-steer.md +30 -0
  119. package/.pi/scripts/graphify-kb-updater.mjs +358 -0
  120. package/.pi/scripts/harness-generate-model-router.mjs +118 -36
  121. package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
  122. package/.pi/scripts/harness-sync-model-router.mjs +15 -2
  123. package/.pi/scripts/harness-verify.mjs +51 -6
  124. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  125. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +22 -0
  128. package/package.json +5 -4
  129. package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
  130. package/vendor/pi-model-router/extensions/commands.ts +4 -4
  131. package/vendor/pi-model-router/extensions/index.ts +21 -0
  132. package/vendor/pi-model-router/extensions/provider.ts +130 -79
  133. package/vendor/pi-model-router/extensions/routing.ts +148 -0
  134. package/vendor/pi-model-router/extensions/state.ts +3 -0
  135. package/vendor/pi-model-router/extensions/types.ts +9 -0
  136. package/vendor/pi-model-router/extensions/ui.ts +16 -2
  137. package/.pi/prompts/git-sync.md +0 -124
@@ -9,12 +9,13 @@ import { parse as parseYaml } from "yaml";
9
9
 
10
10
  export const PLAN_FOCUS_AREAS = ["spec", "wbs", "schedule", "quality"] as const;
11
11
  export type PlanDebateFocus = (typeof PLAN_FOCUS_AREAS)[number];
12
+ export type PlanDebateRoundFocus = PlanDebateFocus | "all";
12
13
 
13
14
  export interface PlanFocusCoverage {
14
15
  covered: PlanDebateFocus[];
15
16
  missing: PlanDebateFocus[];
16
17
  rounds_by_focus: Partial<Record<PlanDebateFocus, number>>;
17
- focus_by_round: Partial<Record<number, PlanDebateFocus>>;
18
+ focus_by_round: Partial<Record<number, PlanDebateRoundFocus>>;
18
19
  last_review_gate_ready: boolean;
19
20
  last_round_index: number;
20
21
  }
@@ -34,8 +35,9 @@ async function fileExists(path: string): Promise<boolean> {
34
35
 
35
36
  function focusFromDraft(
36
37
  draft: Record<string, unknown>,
37
- ): PlanDebateFocus | null {
38
+ ): PlanDebateRoundFocus | null {
38
39
  const focus = String(draft.debate_round_focus ?? "").trim();
40
+ if (focus === "all") return "all";
39
41
  if ((PLAN_FOCUS_AREAS as readonly string[]).includes(focus)) {
40
42
  return focus as PlanDebateFocus;
41
43
  }
@@ -56,14 +58,14 @@ export async function getPlanFocusCoverage(
56
58
  const artifactsDir = join(runDir, "artifacts");
57
59
  const covered = new Set<PlanDebateFocus>();
58
60
  const rounds_by_focus: Partial<Record<PlanDebateFocus, number>> = {};
59
- const focus_by_round: Partial<Record<number, PlanDebateFocus>> = {};
61
+ const focus_by_round: Partial<Record<number, PlanDebateRoundFocus>> = {};
60
62
  let last_review_gate_ready = false;
61
63
  let last_round_index = 0;
62
64
 
63
65
  let files: string[] = [];
64
66
  try {
65
67
  files = (await readdir(artifactsDir)).filter((f) =>
66
- /^review-round-r\d+\.yaml$/i.test(f),
68
+ /^review-round(?:-r\d+|-consolidated)\.yaml$/i.test(f),
67
69
  );
68
70
  } catch {
69
71
  return {
@@ -77,9 +79,12 @@ export async function getPlanFocusCoverage(
77
79
  }
78
80
 
79
81
  for (const name of files.sort()) {
80
- const m = /^review-round-r(\d+)\.yaml$/i.exec(name);
82
+ const consolidated = /^review-round-consolidated\.yaml$/i.test(name);
83
+ const m = consolidated
84
+ ? ["review-round-consolidated.yaml", "1"]
85
+ : /^review-round-r(\d+)\.yaml$/i.exec(name);
81
86
  if (!m) continue;
82
- const roundIndex = Number(m[1]);
87
+ const roundIndex = consolidated ? 1 : Number(m[1]);
83
88
  if (roundIndex > last_round_index) last_round_index = roundIndex;
84
89
  const raw = await readFile(join(artifactsDir, name), "utf-8");
85
90
  let draft: Record<string, unknown>;
@@ -90,8 +95,15 @@ export async function getPlanFocusCoverage(
90
95
  }
91
96
  const focus = focusFromDraft(draft);
92
97
  if (focus) {
93
- covered.add(focus);
94
- rounds_by_focus[focus] = roundIndex;
98
+ if (focus === "all") {
99
+ for (const requiredFocus of required) {
100
+ covered.add(requiredFocus);
101
+ rounds_by_focus[requiredFocus] = roundIndex;
102
+ }
103
+ } else {
104
+ covered.add(focus);
105
+ rounds_by_focus[focus] = roundIndex;
106
+ }
95
107
  focus_by_round[roundIndex] = focus;
96
108
  }
97
109
  if (roundIndex === last_round_index) {
@@ -138,7 +150,7 @@ export function planDebateOutcomeComplete(
138
150
  export async function readDebateRoundFocus(
139
151
  runDir: string,
140
152
  roundIndex: number,
141
- ): Promise<PlanDebateFocus | null> {
153
+ ): Promise<PlanDebateRoundFocus | null> {
142
154
  const path = join(runDir, "artifacts", `review-round-r${roundIndex}.yaml`);
143
155
  if (!(await fileExists(path))) return null;
144
156
  try {
@@ -7,18 +7,29 @@ import { access, readFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { isHarnessBudgetEnforceOn } from "../../lib/harness-budget-enforce.js";
9
9
  import { capsForDebate } from "./debate-bus-core.js";
10
+ import type { DebateEligibilityResult } from "./plan-debate-eligibility.js";
10
11
  import {
11
12
  getPlanFocusCoverage,
12
13
  type PlanDebateFocus,
13
14
  planDebateOutcomeComplete,
14
15
  } from "./plan-debate-focus.js";
15
16
  import { planDebateIdForRun } from "./plan-debate-id.js";
16
- import { laneArtifactPathsForRound } from "./plan-debate-lanes.js";
17
+ import {
18
+ laneArtifactPathsForConsolidatedRound,
19
+ laneArtifactPathsForParallelProbesRound,
20
+ laneArtifactPathsForRound,
21
+ } from "./plan-debate-lanes.js";
17
22
  import {
18
23
  getMessengerRoundState,
19
24
  loadMessengerState,
20
25
  messengerRoundDebateReady,
21
26
  } from "./plan-messenger.js";
27
+ import {
28
+ CONSOLIDATED_REVIEW_ARTIFACT,
29
+ isConsolidatedReviewStrategy,
30
+ isParallelProbesReviewStrategy,
31
+ planReviewGateStrategyFromEligibility,
32
+ } from "./plan-review-gate.js";
22
33
 
23
34
  async function fileExists(path: string): Promise<boolean> {
24
35
  try {
@@ -64,6 +75,7 @@ export interface PlanDebateGateResult {
64
75
  export async function validatePlanDebateGate(
65
76
  projectRoot: string,
66
77
  runId: string,
78
+ eligibility?: DebateEligibilityResult,
67
79
  ): Promise<PlanDebateGateResult> {
68
80
  const errors: string[] = [];
69
81
  const warnings: string[] = [];
@@ -77,6 +89,34 @@ export async function validatePlanDebateGate(
77
89
  ? messenger.required_focuses
78
90
  : (["spec", "wbs", "schedule", "quality"] as const);
79
91
  const caps = capsForDebate(debateId, debateProfile);
92
+ const reviewStrategy =
93
+ eligibility != null
94
+ ? planReviewGateStrategyFromEligibility(eligibility)
95
+ : messenger?.review_gate_mode === "consolidated"
96
+ ? {
97
+ mode: "consolidated" as const,
98
+ profile: debateProfile as DebateEligibilityResult["profile"],
99
+ required_focuses: [...requiredFocuses],
100
+ min_focus_rounds: caps.min_focus_rounds,
101
+ max_rounds: caps.max_rounds,
102
+ max_exchanges_per_round: caps.max_exchanges_per_round,
103
+ round_token_cap: caps.round_token_cap,
104
+ debate_global_cap: caps.debate_global_cap,
105
+ rationale: ["messenger review_gate_mode=consolidated"],
106
+ }
107
+ : {
108
+ mode: "threaded" as const,
109
+ profile: debateProfile as DebateEligibilityResult["profile"],
110
+ required_focuses: [...requiredFocuses],
111
+ min_focus_rounds: caps.min_focus_rounds,
112
+ max_rounds: caps.max_rounds,
113
+ max_exchanges_per_round: caps.max_exchanges_per_round,
114
+ round_token_cap: caps.round_token_cap,
115
+ debate_global_cap: caps.debate_global_cap,
116
+ rationale: [],
117
+ };
118
+ const consolidated = isConsolidatedReviewStrategy(reviewStrategy);
119
+ const parallelProbes = isParallelProbesReviewStrategy(reviewStrategy);
80
120
  const coverage = await getPlanFocusCoverage(runDir, { requiredFocuses });
81
121
  const dialogueOpts = {
82
122
  max_exchanges_per_round: caps.max_exchanges_per_round,
@@ -89,31 +129,73 @@ export async function validatePlanDebateGate(
89
129
  errors.push("last submitted review round has review_gate_ready !== true");
90
130
  }
91
131
 
92
- const roundIndices = [
93
- ...new Set(
94
- Object.values(coverage.rounds_by_focus).filter(
95
- (v): v is number => typeof v === "number",
96
- ),
97
- ),
98
- ];
99
- for (const r of roundIndices) {
100
- const focus = coverage.focus_by_round[r] ?? null;
101
- for (const rel of laneArtifactPathsForRound(r, focus)) {
132
+ if (parallelProbes) {
133
+ for (const rel of laneArtifactPathsForParallelProbesRound()) {
134
+ const abs = join(runDir, rel);
135
+ if (!(await fileExists(abs))) {
136
+ errors.push(`missing ${rel}`);
137
+ }
138
+ }
139
+ const roundState = await getMessengerRoundState(runDir, 1);
140
+ const messengerCheck = messengerRoundDebateReady(
141
+ roundState,
142
+ false,
143
+ dialogueOpts,
144
+ );
145
+ if (!messengerCheck.ok) {
146
+ for (const e of messengerCheck.errors) {
147
+ errors.push(`parallel_probes round messenger: ${e}`);
148
+ }
149
+ }
150
+ } else if (consolidated) {
151
+ const absConsolidated = join(runDir, CONSOLIDATED_REVIEW_ARTIFACT);
152
+ if (!(await fileExists(absConsolidated))) {
153
+ errors.push(`missing ${CONSOLIDATED_REVIEW_ARTIFACT}`);
154
+ }
155
+ for (const rel of laneArtifactPathsForConsolidatedRound()) {
102
156
  const abs = join(runDir, rel);
103
157
  if (!(await fileExists(abs))) {
104
158
  errors.push(`missing ${rel}`);
105
159
  }
106
160
  }
107
- const roundState = await getMessengerRoundState(runDir, r);
108
- const requireSprint = focus === "quality" || r >= 4;
161
+ const roundState = await getMessengerRoundState(runDir, 1);
109
162
  const messengerCheck = messengerRoundDebateReady(
110
163
  roundState,
111
- requireSprint,
164
+ true,
112
165
  dialogueOpts,
113
166
  );
114
167
  if (!messengerCheck.ok) {
115
168
  for (const e of messengerCheck.errors) {
116
- errors.push(`round ${r} messenger: ${e}`);
169
+ errors.push(`consolidated round messenger: ${e}`);
170
+ }
171
+ }
172
+ } else {
173
+ const roundIndices = [
174
+ ...new Set(
175
+ Object.values(coverage.rounds_by_focus).filter(
176
+ (v): v is number => typeof v === "number",
177
+ ),
178
+ ),
179
+ ];
180
+ for (const r of roundIndices) {
181
+ const focus = coverage.focus_by_round[r] ?? null;
182
+ for (const rel of laneArtifactPathsForRound(r, focus)) {
183
+ const abs = join(runDir, rel);
184
+ if (!(await fileExists(abs))) {
185
+ errors.push(`missing ${rel}`);
186
+ }
187
+ }
188
+ const roundState = await getMessengerRoundState(runDir, r);
189
+ const requireSprint = focus === "quality" || r >= 4;
190
+ const messengerCheck = messengerRoundDebateReady(
191
+ roundState,
192
+ requireSprint,
193
+ dialogueOpts,
194
+ );
195
+ if (!messengerCheck.ok) {
196
+ for (const e of messengerCheck.errors) {
197
+ errors.push(`round ${r} messenger: ${e}`);
198
+ }
117
199
  }
118
200
  }
119
201
  }
@@ -203,7 +285,9 @@ export async function validatePlanDebateGate(
203
285
  }
204
286
 
205
287
  export function isReviewRoundArtifactPath(relPath: string): boolean {
206
- return /^artifacts\/review-round-r\d+\.yaml$/i.test(
207
- relPath.replace(/\\/g, "/"),
288
+ const norm = relPath.replace(/\\/g, "/");
289
+ return (
290
+ /^artifacts\/review-round-r\d+\.yaml$/i.test(norm) ||
291
+ norm === CONSOLIDATED_REVIEW_ARTIFACT
208
292
  );
209
293
  }
@@ -2,13 +2,13 @@
2
2
  * Shared Review Gate lane list for a round (gate + round-status).
3
3
  */
4
4
 
5
- import type { PlanDebateFocus } from "./plan-debate-focus.js";
5
+ import type { PlanDebateRoundFocus } from "./plan-debate-focus.js";
6
6
  import type { DebateLaneKind } from "./plan-debate-lane.js";
7
7
 
8
8
  /** Lanes required before review-integrator for this round. */
9
9
  export function lanesForRound(
10
10
  roundIndex: number,
11
- debateRoundFocus?: PlanDebateFocus | null,
11
+ debateRoundFocus?: PlanDebateRoundFocus | null,
12
12
  ): DebateLaneKind[] {
13
13
  const lanes: DebateLaneKind[] = ["validation-turn", "adversary-brief"];
14
14
  if (roundIndex === 1) {
@@ -23,7 +23,7 @@ export function lanesForRound(
23
23
  /** Relative artifact paths for lane YAML + review-round. */
24
24
  export function laneArtifactPathsForRound(
25
25
  roundIndex: number,
26
- debateRoundFocus?: PlanDebateFocus | null,
26
+ debateRoundFocus?: PlanDebateRoundFocus | null,
27
27
  ): string[] {
28
28
  const paths = lanesForRound(roundIndex, debateRoundFocus).map((lane) => {
29
29
  switch (lane) {
@@ -42,3 +42,57 @@ export function laneArtifactPathsForRound(
42
42
  paths.push(`artifacts/review-round-r${roundIndex}.yaml`);
43
43
  return paths;
44
44
  }
45
+
46
+ /** Lanes for consolidated Review Gate (single round; blind verifier first). */
47
+ export function lanesForConsolidatedRound(): DebateLaneKind[] {
48
+ return [
49
+ "hypothesis-validation",
50
+ "validation-turn",
51
+ "adversary-brief",
52
+ "sprint-audit",
53
+ ];
54
+ }
55
+
56
+ export const PARALLEL_PROBES_REVIEW_ARTIFACT =
57
+ "artifacts/review-round-parallel-probes.yaml";
58
+
59
+ /** Parallel plan-verify: inspector ∥ adversary (round 1), then integrator. */
60
+ export function lanesForParallelProbesRound(): DebateLaneKind[] {
61
+ return ["hypothesis-validation", "validation-turn", "adversary-brief"];
62
+ }
63
+
64
+ export function laneArtifactPathsForParallelProbesRound(): string[] {
65
+ const roundIndex = 1;
66
+ return [
67
+ ...lanesForParallelProbesRound().map((lane) => {
68
+ switch (lane) {
69
+ case "validation-turn":
70
+ return `artifacts/validation-turn-r${roundIndex}.yaml`;
71
+ case "adversary-brief":
72
+ return `artifacts/adversary-brief-r${roundIndex}.yaml`;
73
+ default:
74
+ return `artifacts/${lane}-r${roundIndex}.yaml`;
75
+ }
76
+ }),
77
+ PARALLEL_PROBES_REVIEW_ARTIFACT,
78
+ ];
79
+ }
80
+
81
+ export function laneArtifactPathsForConsolidatedRound(): string[] {
82
+ const roundIndex = 1;
83
+ return [
84
+ ...lanesForConsolidatedRound().map((lane) => {
85
+ switch (lane) {
86
+ case "validation-turn":
87
+ return `artifacts/validation-turn-r${roundIndex}.yaml`;
88
+ case "adversary-brief":
89
+ return `artifacts/adversary-brief-r${roundIndex}.yaml`;
90
+ case "sprint-audit":
91
+ return `artifacts/sprint-audit-r${roundIndex}.yaml`;
92
+ default:
93
+ return `artifacts/${lane}-r${roundIndex}.yaml`;
94
+ }
95
+ }),
96
+ "artifacts/review-round-consolidated.yaml",
97
+ ];
98
+ }
@@ -7,12 +7,15 @@ import { access } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
8
  import { capsForDebate } from "./debate-bus-core.js";
9
9
  import {
10
- type PlanDebateFocus,
10
+ type PlanDebateRoundFocus,
11
11
  readDebateRoundFocus,
12
12
  } from "./plan-debate-focus.js";
13
13
  import { planDebateIdForRun } from "./plan-debate-id.js";
14
14
  import { laneArtifactPath } from "./plan-debate-lane.js";
15
- import { lanesForRound } from "./plan-debate-lanes.js";
15
+ import {
16
+ lanesForConsolidatedRound,
17
+ lanesForRound,
18
+ } from "./plan-debate-lanes.js";
16
19
  import {
17
20
  getMessengerRoundState,
18
21
  loadMessengerState,
@@ -40,26 +43,32 @@ export interface RoundStatusResult {
40
43
  dialogue: { ok: boolean; errors: string[] };
41
44
  unresolved_claim_ids: string[];
42
45
  exchange_count: number;
43
- debate_round_focus?: PlanDebateFocus | null;
46
+ debate_round_focus?: PlanDebateRoundFocus | null;
44
47
  }
45
48
 
46
49
  export async function getPlanDebateRoundStatus(
47
50
  runDir: string,
48
51
  roundIndex: number,
49
52
  runId?: string,
50
- opts?: { debate_round_focus?: PlanDebateFocus },
53
+ opts?: { debate_round_focus?: PlanDebateRoundFocus },
51
54
  ): Promise<RoundStatusResult> {
55
+ const messengerState = await loadMessengerState(runDir);
56
+ const consolidated =
57
+ messengerState?.review_gate_mode === "consolidated" && roundIndex === 1;
52
58
  const focus =
53
59
  opts?.debate_round_focus ??
60
+ (consolidated ? ("all" as PlanDebateRoundFocus) : null) ??
54
61
  (await readDebateRoundFocus(runDir, roundIndex));
55
62
  const missing: string[] = [];
56
- for (const lane of lanesForRound(roundIndex, focus)) {
63
+ const laneList = consolidated
64
+ ? lanesForConsolidatedRound()
65
+ : lanesForRound(roundIndex, focus);
66
+ for (const lane of laneList) {
57
67
  const rel = laneArtifactPath(lane, roundIndex);
58
68
  if (!(await exists(join(runDir, rel)))) {
59
69
  missing.push(rel);
60
70
  }
61
71
  }
62
- const messengerState = await loadMessengerState(runDir);
63
72
  const profile = messengerState?.debate_profile;
64
73
  const caps = capsForDebate(
65
74
  runId ? planDebateIdForRun(runId) : `plan-${runId ?? "unknown"}`,
@@ -73,7 +82,9 @@ export async function getPlanDebateRoundStatus(
73
82
  if (!dialogue.ok) {
74
83
  missing.push(...dialogue.errors.map((e) => `messenger: ${e}`));
75
84
  }
76
- const reviewRound = `artifacts/review-round-r${roundIndex}.yaml`;
85
+ const reviewRound = consolidated
86
+ ? "artifacts/review-round-consolidated.yaml"
87
+ : `artifacts/review-round-r${roundIndex}.yaml`;
77
88
  const reviewRoundOnDisk = await exists(join(runDir, reviewRound));
78
89
 
79
90
  let next_tool: string | undefined;
@@ -63,6 +63,8 @@ export interface MessengerState {
63
63
  rounds: Record<string, MessengerRoundState>;
64
64
  debate_profile?: DebateProfile;
65
65
  required_focuses?: PlanDebateFocus[];
66
+ /** consolidated = single Review Gate round; threaded = per-focus rounds */
67
+ review_gate_mode?: "consolidated" | "threaded";
66
68
  }
67
69
 
68
70
  function messengerRoot(runDir: string): string {
@@ -84,6 +86,7 @@ export async function initPlanMessenger(
84
86
  debateId: string;
85
87
  debate_profile?: DebateProfile;
86
88
  required_focuses?: PlanDebateFocus[];
89
+ review_gate_mode?: "consolidated" | "threaded";
87
90
  },
88
91
  ): Promise<string> {
89
92
  const root = messengerRoot(runDir);
@@ -97,6 +100,7 @@ export async function initPlanMessenger(
97
100
  rounds: {},
98
101
  debate_profile: opts.debate_profile,
99
102
  required_focuses: opts.required_focuses,
103
+ review_gate_mode: opts.review_gate_mode,
100
104
  };
101
105
  await writeFile(
102
106
  join(root, "state.json"),
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Consolidated vs threaded Review Gate strategy for plan-phase debate.
3
+ */
4
+
5
+ import type {
6
+ DebateEligibilityResult,
7
+ PlanReviewGateStrategy,
8
+ } from "./plan-debate-eligibility.js";
9
+ import type { PlanDebateFocus } from "./plan-debate-focus.js";
10
+
11
+ export type { PlanReviewGateStrategy };
12
+
13
+ export const CONSOLIDATED_REVIEW_ROUND = 1;
14
+ export const CONSOLIDATED_REVIEW_ARTIFACT =
15
+ "artifacts/review-round-consolidated.yaml";
16
+
17
+ export function planReviewGateStrategyFromEligibility(
18
+ eligibility: DebateEligibilityResult,
19
+ ): PlanReviewGateStrategy {
20
+ return (
21
+ eligibility.review_gate_strategy ?? {
22
+ mode: eligibility.profile === "fast" ? "consolidated" : "threaded",
23
+ profile: eligibility.profile,
24
+ required_focuses: [...eligibility.required_focuses],
25
+ min_focus_rounds: eligibility.min_focus_rounds,
26
+ max_rounds: eligibility.max_rounds,
27
+ max_exchanges_per_round: eligibility.max_exchanges_per_round,
28
+ round_token_cap: eligibility.round_token_cap,
29
+ debate_global_cap: eligibility.debate_global_cap,
30
+ rationale: [...eligibility.rationale],
31
+ }
32
+ );
33
+ }
34
+
35
+ export function isConsolidatedReviewStrategy(
36
+ strategy: PlanReviewGateStrategy,
37
+ ): boolean {
38
+ return strategy.mode === "consolidated";
39
+ }
40
+
41
+ export { PARALLEL_PROBES_REVIEW_ARTIFACT } from "./plan-debate-lanes.js";
42
+
43
+ export function isParallelProbesReviewStrategy(
44
+ strategy: PlanReviewGateStrategy,
45
+ ): boolean {
46
+ return strategy.mode === "parallel_probes";
47
+ }
48
+
49
+ /** Focus areas covered in a single consolidated review round (spec + quality gate). */
50
+ export const CONSOLIDATED_REVIEW_FOCUS_AREAS: readonly PlanDebateFocus[] = [
51
+ "spec",
52
+ "quality",
53
+ ];
54
+
55
+ export function consolidatedReviewFocusesSatisfied(
56
+ covered: readonly string[],
57
+ ): boolean {
58
+ return CONSOLIDATED_REVIEW_FOCUS_AREAS.every((f) => covered.includes(f));
59
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * PostHog client helpers — IPv4-first fetch for WSL2 / broken dual-stack DNS.
3
+ *
4
+ * Node's default fetch can ETIMEDOUT against *.posthog.com while curl succeeds.
5
+ * Use createPostHogFetch() (undici, family 4) for all posthog-node clients.
6
+ */
7
+
8
+ import { Agent, fetch as undiciFetch } from "undici";
9
+
10
+ const POSTHOG_HOST_RE = /(^https?:\/\/)?([^.]+\.)*posthog\.com(\/|$)/i;
11
+
12
+ const ipv4Agent = new Agent({ connect: { family: 4 } });
13
+
14
+ let fetchPatchInstalled = false;
15
+
16
+ export function isPostHogHostUrl(url: string): boolean {
17
+ return POSTHOG_HOST_RE.test(url);
18
+ }
19
+
20
+ export function resolvePostHogHost(): string {
21
+ return process.env.POSTHOG_HOST?.trim() || "https://us.i.posthog.com";
22
+ }
23
+
24
+ /** Fetch that prefers IPv4 — fixes WSL2 ETIMEDOUT on us.i.posthog.com. */
25
+ export function createPostHogFetch(): typeof fetch {
26
+ return ((input: Parameters<typeof fetch>[0], init?: RequestInit) =>
27
+ undiciFetch(
28
+ input as Parameters<typeof undiciFetch>[0],
29
+ {
30
+ ...init,
31
+ dispatcher: ipv4Agent,
32
+ } as Parameters<typeof undiciFetch>[1],
33
+ )) as typeof fetch;
34
+ }
35
+
36
+ export function getPostHogClientOptions(): {
37
+ host: string;
38
+ fetch: typeof fetch;
39
+ requestTimeout: number;
40
+ } {
41
+ return {
42
+ host: resolvePostHogHost(),
43
+ fetch: createPostHogFetch(),
44
+ requestTimeout: 30_000,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Patch global fetch so @posthog/pi (which uses default fetch) reaches PostHog on WSL2.
50
+ * Only PostHog hostnames are routed through the IPv4 agent.
51
+ */
52
+ export function installPostHogFetchPatch(): void {
53
+ if (fetchPatchInstalled) return;
54
+ fetchPatchInstalled = true;
55
+
56
+ const nativeFetch = globalThis.fetch.bind(globalThis);
57
+ const posthogFetch = createPostHogFetch();
58
+
59
+ globalThis.fetch = ((
60
+ input: Parameters<typeof fetch>[0],
61
+ init?: RequestInit,
62
+ ) => {
63
+ const url =
64
+ typeof input === "string"
65
+ ? input
66
+ : input instanceof URL
67
+ ? input.href
68
+ : typeof input === "object" && input !== null && "url" in input
69
+ ? String((input as { url: string }).url)
70
+ : "";
71
+ if (url && isPostHogHostUrl(url)) {
72
+ return posthogFetch(input, init);
73
+ }
74
+ return nativeFetch(input, init);
75
+ }) as typeof fetch;
76
+ }
@@ -9,6 +9,10 @@
9
9
  */
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import {
13
+ evaluateContextModeMutation,
14
+ isMutatingBash,
15
+ } from "../lib/harness-context-mode-policy.js";
12
16
  import {
13
17
  extractWritePathFromToolInput,
14
18
  getLatestRunContext,
@@ -27,6 +31,7 @@ import {
27
31
  userVisiblePromptSlice,
28
32
  validatePlanPacket,
29
33
  } from "../lib/harness-run-context.js";
34
+ import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
30
35
 
31
36
  type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
32
37
 
@@ -56,20 +61,6 @@ const PHASE_ORDER: HarnessPhase[] = [
56
61
  ];
57
62
 
58
63
  const MUTATING_TOOLS = new Set(["write", "edit"]);
59
- const BASH_MUTATION_PATTERNS = [
60
- /\bgit\s+commit\b/i,
61
- /\bgit\s+push\b/i,
62
- /\bgit\s+merge\b/i,
63
- /\bgit\s+rebase\b/i,
64
- /\brm\s+(-rf?|--recursive)\b/i,
65
- /\bmv\b/i,
66
- /\bcp\b/i,
67
- /\bmkdir\b/i,
68
- /\bchmod\b/i,
69
- /\bchown\b/i,
70
- /\bsed\s+-i\b/i,
71
- /\bperl\s+-i\b/i,
72
- ];
73
64
 
74
65
  function nowIso(): string {
75
66
  return new Date().toISOString();
@@ -94,10 +85,6 @@ function hasApprovedPlanSignal(prompt: string, entries: unknown[]): boolean {
94
85
  return hasApprovedPlanSignalFromUserPrompt(prompt);
95
86
  }
96
87
 
97
- function isMutatingBash(command: string): boolean {
98
- return BASH_MUTATION_PATTERNS.some((pattern) => pattern.test(command));
99
- }
100
-
101
88
  function getLatestPolicyStateFull(ctx: {
102
89
  sessionManager: { getEntries(): unknown[] };
103
90
  }): PolicyState {
@@ -148,10 +135,15 @@ export default function policyGate(pi: ExtensionAPI) {
148
135
 
149
136
  pi.on("session_start", async (_event, ctx) => {
150
137
  state = getLatestPolicyStateFull(ctx);
138
+ const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
139
+ if (booted) {
140
+ state = getLatestPolicyStateFull(ctx);
141
+ }
151
142
  });
152
143
 
153
144
  pi.on("before_agent_start", async (event, ctx) => {
154
145
  const userPrompt = userVisiblePromptSlice(event.prompt);
146
+ await bootstrapHarnessSubprocessFromEnv(pi, ctx);
155
147
  const entries = ctx.sessionManager.getEntries();
156
148
  state = getLatestPolicyStateFull(ctx);
157
149
  const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
@@ -243,7 +235,7 @@ export default function policyGate(pi: ExtensionAPI) {
243
235
 
244
236
  const planPhaseHint =
245
237
  state.phase === "plan"
246
- ? "\nPlan phase: scouts → decompose → hypothesis → implementation-researcher + stack-researcher → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-critic."
238
+ ? "\nPlan phase: scouts (parallel) → decompose → hypothesis (sequential) → implementation-researcher + stack-researcher (parallel) → execution-plan-author → validate-plan-dag → debate eligibility + Review Gate → approve_plan → create_plan (YAML plan-packet.yaml). Post-execute: /harness-review."
247
239
  : "";
248
240
 
249
241
  return {
@@ -296,6 +288,19 @@ export default function policyGate(pi: ExtensionAPI) {
296
288
  }
297
289
  }
298
290
 
291
+ const ctxDecision = evaluateContextModeMutation(
292
+ event.toolName,
293
+ event.input as Record<string, unknown>,
294
+ state.phase,
295
+ {
296
+ aborted: state.aborted,
297
+ budgetBypass: state.budgetBypass,
298
+ },
299
+ );
300
+ if (ctxDecision.blocked) {
301
+ return { block: true, reason: ctxDecision.reason };
302
+ }
303
+
299
304
  return undefined;
300
305
  });
301
306
 
@@ -235,6 +235,7 @@ export default function traceRecorder(pi: ExtensionAPI) {
235
235
  if (shouldEmitStarted) {
236
236
  captureHarnessEvent(sessionId, "harness_run_started", {
237
237
  harness_run_id: runId,
238
+ run_id: runId,
238
239
  harness_plan_id: activeRun.planId,
239
240
  harness_phase: activeRun.phase,
240
241
  pi_session_id: sessionId,