ultimate-pi 0.17.0 → 0.18.1

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-decisions/SKILL.md +1 -1
  4. package/.agents/skills/harness-eval/SKILL.md +6 -21
  5. package/.agents/skills/harness-governor/SKILL.md +4 -3
  6. package/.agents/skills/harness-orchestration/SKILL.md +41 -53
  7. package/.agents/skills/harness-plan/SKILL.md +23 -12
  8. package/.agents/skills/harness-review/SKILL.md +52 -0
  9. package/.agents/skills/harness-sentrux-setup/SKILL.md +16 -3
  10. package/.agents/skills/harness-steer/SKILL.md +14 -0
  11. package/.agents/skills/sentrux/SKILL.md +9 -9
  12. package/.pi/agents/harness/planning/decompose.md +7 -4
  13. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  14. package/.pi/agents/harness/planning/hypothesis.md +3 -1
  15. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  16. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  17. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  18. package/.pi/agents/harness/planning/planning-context.md +48 -0
  19. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  21. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +3 -10
  22. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +3 -12
  23. package/.pi/agents/harness/running/executor.md +45 -0
  24. package/.pi/agents/harness/sentrux-steward.md +51 -0
  25. package/.pi/extensions/00-harness-project-control.ts +133 -0
  26. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  27. package/.pi/extensions/budget-guard.ts +2 -0
  28. package/.pi/extensions/debate-orchestrator.ts +2 -0
  29. package/.pi/extensions/harness-ask-user.ts +2 -2
  30. package/.pi/extensions/harness-debate-tools.ts +2 -2
  31. package/.pi/extensions/harness-live-widget.ts +60 -3
  32. package/.pi/extensions/harness-plan-approval.ts +64 -58
  33. package/.pi/extensions/harness-run-context.ts +715 -90
  34. package/.pi/extensions/harness-subagent-submit.ts +46 -12
  35. package/.pi/extensions/harness-subagents.ts +2 -2
  36. package/.pi/extensions/harness-telemetry.ts +2 -0
  37. package/.pi/extensions/harness-web-tools.ts +2 -2
  38. package/.pi/extensions/lib/extension-load-guard.ts +10 -0
  39. package/.pi/extensions/lib/harness-artifact-gate.ts +172 -0
  40. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  41. package/.pi/extensions/lib/harness-spawn-topology.ts +165 -0
  42. package/.pi/extensions/lib/harness-subagent-auth.ts +1 -2
  43. package/.pi/extensions/lib/harness-subagent-policy.ts +28 -24
  44. package/.pi/extensions/lib/harness-subagent-precheck.ts +36 -10
  45. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  46. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +22 -22
  47. package/.pi/extensions/lib/harness-subagents-bridge.ts +7 -29
  48. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  49. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  50. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  51. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  52. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  53. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  54. package/.pi/extensions/lib/plan-approval-readiness.ts +192 -0
  55. package/.pi/extensions/lib/plan-debate-eligibility.ts +12 -5
  56. package/.pi/extensions/lib/plan-debate-gate.ts +22 -1
  57. package/.pi/extensions/lib/plan-debate-lanes.ts +32 -2
  58. package/.pi/extensions/lib/plan-review-gate.ts +8 -0
  59. package/.pi/extensions/lib/posthog-client.ts +76 -0
  60. package/.pi/extensions/lib/spawn-policy.ts +3 -3
  61. package/.pi/extensions/observation-bus.ts +2 -0
  62. package/.pi/extensions/policy-gate.ts +26 -19
  63. package/.pi/extensions/review-integrity.ts +91 -10
  64. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  65. package/.pi/extensions/test-diff-integrity.ts +1 -0
  66. package/.pi/extensions/trace-recorder.ts +2 -0
  67. package/.pi/harness/agents.manifest.json +37 -37
  68. package/.pi/harness/corpus/cron.example +8 -0
  69. package/.pi/harness/corpus/graphify-kb-updater.config.json +214 -0
  70. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  71. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  72. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  73. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  74. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +8 -6
  75. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  76. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  77. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  78. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  79. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  80. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  81. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  82. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  83. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  84. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  85. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +37 -0
  86. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  87. package/.pi/harness/docs/adrs/README.md +11 -0
  88. package/.pi/harness/docs/graphify-kb-updater-runbook.md +163 -0
  89. package/.pi/harness/docs/practice-map.md +110 -0
  90. package/.pi/harness/env.harness.template +5 -3
  91. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  92. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +5 -2
  93. package/.pi/harness/specs/README.md +1 -1
  94. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  95. package/.pi/harness/specs/harness-spawn-context.schema.json +15 -1
  96. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  97. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  98. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  99. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  100. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  101. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  102. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  103. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  104. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  105. package/.pi/harness/specs/steer-state.schema.json +20 -0
  106. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  107. package/.pi/lib/harness-project-config.ts +91 -0
  108. package/.pi/lib/harness-repair-brief.ts +145 -0
  109. package/.pi/lib/harness-run-context.ts +591 -32
  110. package/.pi/lib/harness-ui-state.ts +114 -21
  111. package/.pi/prompts/harness-auto.md +10 -10
  112. package/.pi/prompts/harness-critic.md +3 -30
  113. package/.pi/prompts/harness-eval.md +4 -37
  114. package/.pi/prompts/harness-plan.md +116 -54
  115. package/.pi/prompts/harness-review.md +150 -15
  116. package/.pi/prompts/harness-run.md +62 -10
  117. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  118. package/.pi/prompts/harness-setup.md +5 -4
  119. package/.pi/prompts/harness-steer.md +30 -0
  120. package/.pi/scripts/README.md +1 -0
  121. package/.pi/scripts/graphify-kb-updater.mjs +398 -0
  122. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  123. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  124. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  125. package/.pi/scripts/harness-verify.mjs +22 -6
  126. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  127. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  128. package/AGENTS.md +1 -0
  129. package/CHANGELOG.md +23 -0
  130. package/README.md +94 -58
  131. package/package.json +5 -4
  132. package/.pi/agents/harness/executor.md +0 -47
  133. package/.pi/agents/harness/planning/scout-graphify.md +0 -37
  134. package/.pi/agents/harness/planning/scout-semantic.md +0 -39
  135. package/.pi/agents/harness/planning/scout-structure.md +0 -35
  136. package/.pi/prompts/git-sync.md +0 -124
  137. /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Pre-approve_plan readiness checks (planning context, research, phase status).
3
+ */
4
+
5
+ import { constants } from "node:fs";
6
+ import { access, readFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { parse as parseYaml } from "yaml";
9
+
10
+ export interface PlanApprovalReadiness {
11
+ ok: boolean;
12
+ errors: string[];
13
+ warnings: string[];
14
+ }
15
+
16
+ const PLANNING_CONTEXT_ARTIFACT = "artifacts/planning-context.yaml";
17
+
18
+ const PHASE35_ARTIFACTS = [
19
+ "artifacts/implementation-research.yaml",
20
+ "artifacts/stack.yaml",
21
+ ] as const;
22
+
23
+ async function fileExists(path: string): Promise<boolean> {
24
+ try {
25
+ await access(path, constants.R_OK);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function readYamlObject(
33
+ path: string,
34
+ ): Promise<Record<string, unknown> | null> {
35
+ try {
36
+ const raw = await readFile(path, "utf-8");
37
+ const doc = parseYaml(raw) as unknown;
38
+ return doc && typeof doc === "object" && !Array.isArray(doc)
39
+ ? (doc as Record<string, unknown>)
40
+ : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ async function hasPhaseWaiver(
47
+ runDir: string,
48
+ reason: string,
49
+ ): Promise<boolean> {
50
+ const path = join(runDir, "artifacts", "plan-phase-waiver.yaml");
51
+ const doc = await readYamlObject(path);
52
+ if (!doc) return false;
53
+ const waived = doc.waived as unknown;
54
+ if (!Array.isArray(waived)) return false;
55
+ return waived.some((w) => {
56
+ if (!w || typeof w !== "object") return false;
57
+ const entry = w as Record<string, unknown>;
58
+ return String(entry.reason ?? "") === reason;
59
+ });
60
+ }
61
+
62
+ function artifactStatusBad(
63
+ doc: Record<string, unknown> | null,
64
+ label: string,
65
+ ): string | null {
66
+ const status = String(doc?.status ?? "ok").toLowerCase();
67
+ if (status === "partial" || status === "failed" || status === "error") {
68
+ return `${label}: status "${status}" without waiver`;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function coverageLaneStatus(
74
+ doc: Record<string, unknown> | null,
75
+ lane: string,
76
+ ): string {
77
+ const coverage = doc?.coverage as Record<string, unknown> | undefined;
78
+ if (!coverage || typeof coverage !== "object") return "";
79
+ const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
80
+ return String(laneDoc?.status ?? "").toLowerCase();
81
+ }
82
+
83
+ async function validatePlanningContext(
84
+ runDir: string,
85
+ quick: boolean,
86
+ errors: string[],
87
+ ): Promise<boolean> {
88
+ const rel = PLANNING_CONTEXT_ARTIFACT;
89
+ const abs = join(runDir, rel);
90
+ if (!(await fileExists(abs))) {
91
+ return false;
92
+ }
93
+ const doc = await readYamlObject(abs);
94
+ const bad = artifactStatusBad(doc, rel);
95
+ if (bad) {
96
+ const waived = await hasPhaseWaiver(
97
+ runDir,
98
+ `planning-context:${String(doc?.status ?? "")}`,
99
+ );
100
+ if (!waived) {
101
+ errors.push(bad);
102
+ }
103
+ }
104
+ const arch = coverageLaneStatus(doc, "architecture");
105
+ const structure = coverageLaneStatus(doc, "structure");
106
+ if (arch !== "ok" && arch !== "partial") {
107
+ errors.push(
108
+ `${rel}: coverage.architecture.status must be ok or partial (got "${arch || "missing"}")`,
109
+ );
110
+ }
111
+ if (structure !== "ok" && structure !== "partial") {
112
+ errors.push(
113
+ `${rel}: coverage.structure.status must be ok or partial (got "${structure || "missing"}")`,
114
+ );
115
+ }
116
+ if (!quick) {
117
+ const semantic = coverageLaneStatus(doc, "semantic");
118
+ if (
119
+ semantic &&
120
+ semantic !== "ok" &&
121
+ semantic !== "partial" &&
122
+ semantic !== "skipped"
123
+ ) {
124
+ errors.push(
125
+ `${rel}: coverage.semantic.status must be ok, partial, or skipped (got "${semantic}")`,
126
+ );
127
+ }
128
+ }
129
+ return true;
130
+ }
131
+
132
+ export async function validatePlanApprovalReadiness(
133
+ projectRoot: string,
134
+ runId: string,
135
+ opts?: { risk_level?: string; quick?: boolean },
136
+ ): Promise<PlanApprovalReadiness> {
137
+ const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
138
+ const errors: string[] = [];
139
+ const warnings: string[] = [];
140
+ const risk = String(opts?.risk_level ?? "med").toLowerCase();
141
+ const quick = opts?.quick === true;
142
+
143
+ const statusPath = join(runDir, "artifacts", "plan-phase-status.yaml");
144
+ const statusDoc = await readYamlObject(statusPath);
145
+ if (statusDoc) {
146
+ const planStatus = String(statusDoc.plan_status ?? "").toLowerCase();
147
+ if (planStatus === "partial" || planStatus === "needs_clarification") {
148
+ const waived = await hasPhaseWaiver(runDir, `plan_status:${planStatus}`);
149
+ if (!waived) {
150
+ errors.push(
151
+ `plan phase status is "${planStatus}" — resolve gaps, set plan_status ready, or write artifacts/plan-phase-waiver.yaml`,
152
+ );
153
+ }
154
+ }
155
+ }
156
+
157
+ const hasPlanningContext = await validatePlanningContext(
158
+ runDir,
159
+ quick,
160
+ errors,
161
+ );
162
+
163
+ if (!hasPlanningContext) {
164
+ const waived = await hasPhaseWaiver(
165
+ runDir,
166
+ "missing:planning-reconnaissance",
167
+ );
168
+ if (!waived) {
169
+ errors.push(`missing ${PLANNING_CONTEXT_ARTIFACT}`);
170
+ }
171
+ }
172
+
173
+ for (const rel of PHASE35_ARTIFACTS) {
174
+ const abs = join(runDir, rel);
175
+ if (!(await fileExists(abs))) {
176
+ if (risk === "high" || risk === "med") {
177
+ errors.push(`missing ${rel} (Phase 3.5 required for risk ${risk})`);
178
+ } else {
179
+ warnings.push(`missing ${rel} (recommended for risk ${risk})`);
180
+ }
181
+ }
182
+ }
183
+
184
+ if (!(await fileExists(join(runDir, "artifacts/decomposition.yaml")))) {
185
+ errors.push("missing artifacts/decomposition.yaml");
186
+ }
187
+ if (!(await fileExists(join(runDir, "artifacts/hypothesis.yaml")))) {
188
+ errors.push("missing artifacts/hypothesis.yaml");
189
+ }
190
+
191
+ return { ok: errors.length === 0, errors, warnings };
192
+ }
@@ -126,7 +126,7 @@ export const PLAN_BUDGET_FAST = {
126
126
  } as const;
127
127
 
128
128
  export interface PlanReviewGateStrategy {
129
- mode: "consolidated" | "threaded";
129
+ mode: "consolidated" | "threaded" | "parallel_probes";
130
130
  profile: DebateProfile;
131
131
  required_focuses: PlanDebateFocus[];
132
132
  min_focus_rounds: number;
@@ -232,9 +232,9 @@ export function harnessPlanDebateEligibility(
232
232
  confidenceAllowsLight(impl) &&
233
233
  stackHasClearPrimary(stack)
234
234
  ) {
235
- profile = "fast";
235
+ profile = "light";
236
236
  rationale.push(
237
- "fast: low risk, clear stack, high-confidence implementation approach",
237
+ "light: low risk, clear stack, high-confidence implementation (threaded spec+quality)",
238
238
  );
239
239
  } else if (risk === "med") {
240
240
  profile = "standard";
@@ -242,7 +242,9 @@ export function harnessPlanDebateEligibility(
242
242
  }
243
243
 
244
244
  const required_focuses: PlanDebateFocus[] =
245
- profile === "fast" ? [...LIGHT_FOCUS] : [...PLAN_FOCUS_AREAS];
245
+ profile === "fast" || profile === "light"
246
+ ? [...LIGHT_FOCUS]
247
+ : [...PLAN_FOCUS_AREAS];
246
248
 
247
249
  const caps = capsForProfile(profile);
248
250
 
@@ -253,7 +255,12 @@ export function harnessPlanDebateEligibility(
253
255
  human_required,
254
256
  rationale,
255
257
  review_gate_strategy: {
256
- mode: profile === "fast" ? "consolidated" : "threaded",
258
+ mode:
259
+ profile === "fast"
260
+ ? "consolidated"
261
+ : profile === "standard"
262
+ ? "parallel_probes"
263
+ : "threaded",
257
264
  profile,
258
265
  required_focuses: [...required_focuses],
259
266
  min_focus_rounds: caps.min_focus_rounds,
@@ -16,6 +16,7 @@ import {
16
16
  import { planDebateIdForRun } from "./plan-debate-id.js";
17
17
  import {
18
18
  laneArtifactPathsForConsolidatedRound,
19
+ laneArtifactPathsForParallelProbesRound,
19
20
  laneArtifactPathsForRound,
20
21
  } from "./plan-debate-lanes.js";
21
22
  import {
@@ -26,6 +27,7 @@ import {
26
27
  import {
27
28
  CONSOLIDATED_REVIEW_ARTIFACT,
28
29
  isConsolidatedReviewStrategy,
30
+ isParallelProbesReviewStrategy,
29
31
  planReviewGateStrategyFromEligibility,
30
32
  } from "./plan-review-gate.js";
31
33
 
@@ -114,6 +116,7 @@ export async function validatePlanDebateGate(
114
116
  rationale: [],
115
117
  };
116
118
  const consolidated = isConsolidatedReviewStrategy(reviewStrategy);
119
+ const parallelProbes = isParallelProbesReviewStrategy(reviewStrategy);
117
120
  const coverage = await getPlanFocusCoverage(runDir, { requiredFocuses });
118
121
  const dialogueOpts = {
119
122
  max_exchanges_per_round: caps.max_exchanges_per_round,
@@ -126,7 +129,25 @@ export async function validatePlanDebateGate(
126
129
  errors.push("last submitted review round has review_gate_ready !== true");
127
130
  }
128
131
 
129
- if (consolidated) {
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) {
130
151
  const absConsolidated = join(runDir, CONSOLIDATED_REVIEW_ARTIFACT);
131
152
  if (!(await fileExists(absConsolidated))) {
132
153
  errors.push(`missing ${CONSOLIDATED_REVIEW_ARTIFACT}`);
@@ -43,9 +43,39 @@ export function laneArtifactPathsForRound(
43
43
  return paths;
44
44
  }
45
45
 
46
- /** Lanes for consolidated Review Gate (single round, parallel-friendly). */
46
+ /** Lanes for consolidated Review Gate (single round; blind verifier first). */
47
47
  export function lanesForConsolidatedRound(): DebateLaneKind[] {
48
- return ["validation-turn", "adversary-brief", "sprint-audit"];
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
+ ];
49
79
  }
50
80
 
51
81
  export function laneArtifactPathsForConsolidatedRound(): string[] {
@@ -38,6 +38,14 @@ export function isConsolidatedReviewStrategy(
38
38
  return strategy.mode === "consolidated";
39
39
  }
40
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
+
41
49
  /** Focus areas covered in a single consolidated review round (spec + quality gate). */
42
50
  export const CONSOLIDATED_REVIEW_FOCUS_AREAS: readonly PlanDebateFocus[] = [
43
51
  "spec",
@@ -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
+ }
@@ -11,9 +11,9 @@ export const SUBAGENT_BLOCKED_TOOLS = new Set([
11
11
  ]);
12
12
 
13
13
  const ASK_USER_ALLOWED_AGENT_TYPES = new Set([
14
- "harness/evaluator",
15
- "harness/adversary",
16
- "harness/tie-breaker",
14
+ "harness/reviewing/evaluator",
15
+ "harness/reviewing/adversary",
16
+ "harness/reviewing/tie-breaker",
17
17
  ]);
18
18
 
19
19
  export interface ToolCallDecision {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { randomUUID } from "node:crypto";
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
10
11
  import { getRunIdFromSession } from "../lib/harness-run-context.js";
11
12
 
12
13
  type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
@@ -87,6 +88,7 @@ function getRunId(ctx: {
87
88
  }
88
89
 
89
90
  export default function observationBus(pi: ExtensionAPI) {
91
+ if (!isHarnessProjectEnabled()) return;
90
92
  const seen = new Set<string>();
91
93
 
92
94
  pi.on("agent_end", async (_event, ctx) => {
@@ -9,6 +9,11 @@
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";
16
+ import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
12
17
  import {
13
18
  extractWritePathFromToolInput,
14
19
  getLatestRunContext,
@@ -27,6 +32,7 @@ import {
27
32
  userVisiblePromptSlice,
28
33
  validatePlanPacket,
29
34
  } from "../lib/harness-run-context.js";
35
+ import { bootstrapHarnessSubprocessFromEnv } from "./lib/harness-subprocess-bootstrap.js";
30
36
 
31
37
  type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
32
38
 
@@ -56,20 +62,6 @@ const PHASE_ORDER: HarnessPhase[] = [
56
62
  ];
57
63
 
58
64
  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
65
 
74
66
  function nowIso(): string {
75
67
  return new Date().toISOString();
@@ -94,10 +86,6 @@ function hasApprovedPlanSignal(prompt: string, entries: unknown[]): boolean {
94
86
  return hasApprovedPlanSignalFromUserPrompt(prompt);
95
87
  }
96
88
 
97
- function isMutatingBash(command: string): boolean {
98
- return BASH_MUTATION_PATTERNS.some((pattern) => pattern.test(command));
99
- }
100
-
101
89
  function getLatestPolicyStateFull(ctx: {
102
90
  sessionManager: { getEntries(): unknown[] };
103
91
  }): PolicyState {
@@ -139,6 +127,7 @@ function getLatestPolicyStateFull(ctx: {
139
127
  }
140
128
 
141
129
  export default function policyGate(pi: ExtensionAPI) {
130
+ if (!isHarnessProjectEnabled()) return;
142
131
  let state = defaultState();
143
132
 
144
133
  const appendPolicyState = (next: PolicyState): void => {
@@ -148,10 +137,15 @@ export default function policyGate(pi: ExtensionAPI) {
148
137
 
149
138
  pi.on("session_start", async (_event, ctx) => {
150
139
  state = getLatestPolicyStateFull(ctx);
140
+ const booted = await bootstrapHarnessSubprocessFromEnv(pi, ctx);
141
+ if (booted) {
142
+ state = getLatestPolicyStateFull(ctx);
143
+ }
151
144
  });
152
145
 
153
146
  pi.on("before_agent_start", async (event, ctx) => {
154
147
  const userPrompt = userVisiblePromptSlice(event.prompt);
148
+ await bootstrapHarnessSubprocessFromEnv(pi, ctx);
155
149
  const entries = ctx.sessionManager.getEntries();
156
150
  state = getLatestPolicyStateFull(ctx);
157
151
  const bootstrapPrompt = isHarnessBootstrapPrompt(userPrompt);
@@ -243,7 +237,7 @@ export default function policyGate(pi: ExtensionAPI) {
243
237
 
244
238
  const planPhaseHint =
245
239
  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."
240
+ ? "\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
241
  : "";
248
242
 
249
243
  return {
@@ -296,6 +290,19 @@ export default function policyGate(pi: ExtensionAPI) {
296
290
  }
297
291
  }
298
292
 
293
+ const ctxDecision = evaluateContextModeMutation(
294
+ event.toolName,
295
+ event.input as Record<string, unknown>,
296
+ state.phase,
297
+ {
298
+ aborted: state.aborted,
299
+ budgetBypass: state.budgetBypass,
300
+ },
301
+ );
302
+ if (ctxDecision.blocked) {
303
+ return { block: true, reason: ctxDecision.reason };
304
+ }
305
+
299
306
  return undefined;
300
307
  });
301
308