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.
- package/.agents/skills/harness-context/SKILL.md +13 -6
- package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
- package/.agents/skills/harness-decisions/SKILL.md +1 -1
- package/.agents/skills/harness-eval/SKILL.md +6 -21
- package/.agents/skills/harness-governor/SKILL.md +4 -3
- package/.agents/skills/harness-orchestration/SKILL.md +41 -53
- package/.agents/skills/harness-plan/SKILL.md +23 -12
- package/.agents/skills/harness-review/SKILL.md +52 -0
- package/.agents/skills/harness-sentrux-setup/SKILL.md +16 -3
- package/.agents/skills/harness-steer/SKILL.md +14 -0
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/agents/harness/planning/decompose.md +7 -4
- package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
- package/.pi/agents/harness/planning/hypothesis.md +3 -1
- package/.pi/agents/harness/planning/plan-adversary.md +2 -0
- package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
- package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
- package/.pi/agents/harness/planning/planning-context.md +48 -0
- package/.pi/agents/harness/planning/review-integrator.md +2 -0
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
- package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +3 -10
- package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +3 -12
- package/.pi/agents/harness/running/executor.md +45 -0
- package/.pi/agents/harness/sentrux-steward.md +51 -0
- package/.pi/extensions/00-harness-project-control.ts +133 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
- package/.pi/extensions/budget-guard.ts +2 -0
- package/.pi/extensions/debate-orchestrator.ts +2 -0
- package/.pi/extensions/harness-ask-user.ts +2 -2
- package/.pi/extensions/harness-debate-tools.ts +2 -2
- package/.pi/extensions/harness-live-widget.ts +60 -3
- package/.pi/extensions/harness-plan-approval.ts +64 -58
- package/.pi/extensions/harness-run-context.ts +715 -90
- package/.pi/extensions/harness-subagent-submit.ts +46 -12
- package/.pi/extensions/harness-subagents.ts +2 -2
- package/.pi/extensions/harness-telemetry.ts +2 -0
- package/.pi/extensions/harness-web-tools.ts +2 -2
- package/.pi/extensions/lib/extension-load-guard.ts +10 -0
- package/.pi/extensions/lib/harness-artifact-gate.ts +172 -0
- package/.pi/extensions/lib/harness-posthog.ts +9 -5
- package/.pi/extensions/lib/harness-spawn-topology.ts +165 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +1 -2
- package/.pi/extensions/lib/harness-subagent-policy.ts +28 -24
- package/.pi/extensions/lib/harness-subagent-precheck.ts +36 -10
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +22 -22
- package/.pi/extensions/lib/harness-subagents-bridge.ts +7 -29
- package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
- package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
- package/.pi/extensions/lib/plan-approval/types.ts +1 -1
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
- package/.pi/extensions/lib/plan-approval-readiness.ts +192 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +12 -5
- package/.pi/extensions/lib/plan-debate-gate.ts +22 -1
- package/.pi/extensions/lib/plan-debate-lanes.ts +32 -2
- package/.pi/extensions/lib/plan-review-gate.ts +8 -0
- package/.pi/extensions/lib/posthog-client.ts +76 -0
- package/.pi/extensions/lib/spawn-policy.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +26 -19
- package/.pi/extensions/review-integrity.ts +91 -10
- package/.pi/extensions/sentrux-rules-sync.ts +2 -0
- package/.pi/extensions/test-diff-integrity.ts +1 -0
- package/.pi/extensions/trace-recorder.ts +2 -0
- package/.pi/harness/agents.manifest.json +37 -37
- package/.pi/harness/corpus/cron.example +8 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +214 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
- package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +8 -6
- package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
- package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
- package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
- package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
- package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
- package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
- package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +37 -0
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/README.md +11 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +163 -0
- package/.pi/harness/docs/practice-map.md +110 -0
- package/.pi/harness/env.harness.template +5 -3
- package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +5 -2
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-run-context.schema.json +11 -0
- package/.pi/harness/specs/harness-spawn-context.schema.json +15 -1
- package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
- package/.pi/harness/specs/plan-packet.schema.json +4 -0
- package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
- package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
- package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
- package/.pi/harness/specs/repair-brief.schema.json +45 -0
- package/.pi/harness/specs/review-outcome.schema.json +46 -0
- package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
- package/.pi/harness/specs/steer-state.schema.json +20 -0
- package/.pi/lib/harness-context-mode-policy.ts +256 -0
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-repair-brief.ts +145 -0
- package/.pi/lib/harness-run-context.ts +591 -32
- package/.pi/lib/harness-ui-state.ts +114 -21
- package/.pi/prompts/harness-auto.md +10 -10
- package/.pi/prompts/harness-critic.md +3 -30
- package/.pi/prompts/harness-eval.md +4 -37
- package/.pi/prompts/harness-plan.md +116 -54
- package/.pi/prompts/harness-review.md +150 -15
- package/.pi/prompts/harness-run.md +62 -10
- package/.pi/prompts/harness-sentrux-steward.md +55 -0
- package/.pi/prompts/harness-setup.md +5 -4
- package/.pi/prompts/harness-steer.md +30 -0
- package/.pi/scripts/README.md +1 -0
- package/.pi/scripts/graphify-kb-updater.mjs +398 -0
- package/.pi/scripts/harness-agents-manifest.mjs +1 -1
- package/.pi/scripts/harness-project-toggle.mjs +129 -0
- package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
- package/.pi/scripts/harness-verify.mjs +22 -6
- package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
- package/.pi/scripts/validate-plan-dag.mjs +3 -3
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +94 -58
- package/package.json +5 -4
- package/.pi/agents/harness/executor.md +0 -47
- package/.pi/agents/harness/planning/scout-graphify.md +0 -37
- package/.pi/agents/harness/planning/scout-semantic.md +0 -39
- package/.pi/agents/harness/planning/scout-structure.md +0 -35
- package/.pi/prompts/git-sync.md +0 -124
- /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 = "
|
|
235
|
+
profile = "light";
|
|
236
236
|
rationale.push(
|
|
237
|
-
"
|
|
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"
|
|
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:
|
|
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 (
|
|
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
|
|
46
|
+
/** Lanes for consolidated Review Gate (single round; blind verifier first). */
|
|
47
47
|
export function lanesForConsolidatedRound(): DebateLaneKind[] {
|
|
48
|
-
return [
|
|
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-
|
|
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
|
|