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.
- package/.agents/skills/harness-context/SKILL.md +13 -6
- package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
- 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 +39 -51
- 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 +13 -1
- package/.agents/skills/harness-steer/SKILL.md +14 -0
- package/.pi/agents/harness/adversary.md +3 -10
- package/.pi/agents/harness/evaluator.md +3 -12
- package/.pi/agents/harness/executor.md +12 -14
- 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 +4 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -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/scout-graphify.md +3 -1
- package/.pi/agents/harness/planning/scout-semantic.md +3 -1
- package/.pi/agents/harness/planning/scout-structure.md +3 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
- package/.pi/agents/harness/sentrux-steward.md +51 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
- package/.pi/extensions/harness-debate-tools.ts +12 -3
- package/.pi/extensions/harness-live-widget.ts +27 -1
- package/.pi/extensions/harness-plan-approval.ts +62 -56
- package/.pi/extensions/harness-run-context.ts +553 -84
- package/.pi/extensions/harness-subagent-submit.ts +43 -33
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +15 -9
- package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
- package/.pi/extensions/lib/harness-posthog.ts +9 -5
- package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
- package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
- package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
- 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 +241 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
- package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +59 -0
- package/.pi/extensions/lib/posthog-client.ts +76 -0
- package/.pi/extensions/policy-gate.ts +24 -19
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +24 -16
- package/.pi/harness/corpus/cron.example +8 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -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 +7 -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 +36 -0
- package/.pi/harness/docs/adrs/README.md +10 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
- package/.pi/harness/docs/practice-map.md +110 -0
- package/.pi/harness/env.harness.template +5 -3
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
- 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 +14 -0
- 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/plan-review-round-draft.schema.json +1 -1
- 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-repair-brief.ts +145 -0
- package/.pi/lib/harness-run-context.ts +591 -32
- package/.pi/lib/harness-ui-state.ts +87 -9
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +9 -9
- package/.pi/prompts/harness-critic.md +3 -30
- package/.pi/prompts/harness-eval.md +4 -37
- package/.pi/prompts/harness-plan.md +139 -57
- 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 +4 -4
- package/.pi/prompts/harness-steer.md +30 -0
- package/.pi/scripts/graphify-kb-updater.mjs +358 -0
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +51 -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 +22 -0
- package/package.json +5 -4
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
- package/.pi/prompts/git-sync.md +0 -124
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { resolveGuardedRunDir } from "../lib/harness-subagent-submit-path.js";
|
|
9
10
|
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
10
11
|
import { getHarnessPackageRoot } from "./lib/harness-paths.js";
|
|
11
12
|
import { evaluateHarnessSubagentToolCall } from "./lib/harness-subagent-policy.js";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
executeSubmitPipeline,
|
|
15
|
+
loadSubmitDocument,
|
|
16
|
+
} from "./lib/harness-subagent-submit-pipeline.js";
|
|
13
17
|
import { SUBMIT_TOOL_SPECS } from "./lib/harness-subagent-submit-registry.js";
|
|
14
18
|
|
|
15
19
|
// @ts-expect-error pi extensions run as ESM
|
|
@@ -17,9 +21,18 @@ const MODULE_URL = import.meta.url;
|
|
|
17
21
|
|
|
18
22
|
const DocumentSchema = Type.Object(
|
|
19
23
|
{
|
|
20
|
-
document: Type.
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
document: Type.Optional(
|
|
25
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
26
|
+
description:
|
|
27
|
+
"Artifact fields (deprecated when source_path is set; ADR 0043)",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
source_path: Type.Optional(
|
|
31
|
+
Type.String({
|
|
32
|
+
description:
|
|
33
|
+
"Relative path under run dir, e.g. artifacts/.draft/decomposition.yaml",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
23
36
|
},
|
|
24
37
|
{ additionalProperties: false },
|
|
25
38
|
);
|
|
@@ -58,30 +71,6 @@ export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
|
58
71
|
pi.on("tool_call", async (event) => {
|
|
59
72
|
if (!event.toolName.startsWith("submit_")) return undefined;
|
|
60
73
|
const subprocessOk = isSubprocessHarness();
|
|
61
|
-
// #region agent log
|
|
62
|
-
fetch("http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0", {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: {
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
"X-Debug-Session-Id": "2ca12b",
|
|
67
|
-
},
|
|
68
|
-
body: JSON.stringify({
|
|
69
|
-
sessionId: "2ca12b",
|
|
70
|
-
hypothesisId: "H2",
|
|
71
|
-
location: "harness-subagent-submit.ts:tool_call",
|
|
72
|
-
message: "submit tool_call gate",
|
|
73
|
-
data: {
|
|
74
|
-
toolName: event.toolName,
|
|
75
|
-
PI_HARNESS_SUBPROCESS: process.env.PI_HARNESS_SUBPROCESS,
|
|
76
|
-
HARNESS_RUN_ID: process.env.HARNESS_RUN_ID ?? null,
|
|
77
|
-
HARNESS_RUN_DIR: process.env.HARNESS_RUN_DIR ?? null,
|
|
78
|
-
HARNESS_AGENT_ID: process.env.HARNESS_AGENT_ID ?? null,
|
|
79
|
-
subprocessOk,
|
|
80
|
-
},
|
|
81
|
-
timestamp: Date.now(),
|
|
82
|
-
}),
|
|
83
|
-
}).catch(() => {});
|
|
84
|
-
// #endregion
|
|
85
74
|
if (!subprocessOk) {
|
|
86
75
|
return {
|
|
87
76
|
block: true,
|
|
@@ -141,21 +130,42 @@ export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
|
141
130
|
isError: true,
|
|
142
131
|
};
|
|
143
132
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
const runResolved = await resolveGuardedRunDir({
|
|
134
|
+
projectRoot,
|
|
135
|
+
runId,
|
|
136
|
+
runDirEnv,
|
|
137
|
+
});
|
|
138
|
+
if (!runResolved.ok) {
|
|
147
139
|
return {
|
|
148
|
-
content: [{ type: "text", text:
|
|
140
|
+
content: [{ type: "text", text: runResolved.error }],
|
|
149
141
|
details: {},
|
|
150
142
|
isError: true,
|
|
151
143
|
};
|
|
152
144
|
}
|
|
145
|
+
const loaded = await loadSubmitDocument({
|
|
146
|
+
projectRoot,
|
|
147
|
+
runDir: runResolved.runDir,
|
|
148
|
+
document: (params as { document?: Record<string, unknown> }).document,
|
|
149
|
+
source_path: (params as { source_path?: string }).source_path,
|
|
150
|
+
});
|
|
151
|
+
if (!loaded.ok) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Validation failed:\n${loaded.validation_errors.join("\n")}`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
isError: true,
|
|
160
|
+
details: loaded,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
153
163
|
const result = await executeSubmitPipeline({
|
|
154
164
|
projectRoot,
|
|
155
165
|
specsDir,
|
|
156
166
|
spec,
|
|
157
167
|
agentId,
|
|
158
|
-
document,
|
|
168
|
+
document: loaded.document,
|
|
159
169
|
runId,
|
|
160
170
|
runDirEnv,
|
|
161
171
|
});
|
|
@@ -127,6 +127,7 @@ function propsFromRun(
|
|
|
127
127
|
): Record<string, unknown> {
|
|
128
128
|
return {
|
|
129
129
|
harness_run_id: runId,
|
|
130
|
+
run_id: runId,
|
|
130
131
|
harness_plan_id: planId,
|
|
131
132
|
harness_phase: phase,
|
|
132
133
|
pi_session_id: distinctId,
|
|
@@ -134,6 +135,28 @@ function propsFromRun(
|
|
|
134
135
|
};
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function normalizedRunId(
|
|
139
|
+
data: Record<string, unknown>,
|
|
140
|
+
trace: TraceState | null,
|
|
141
|
+
distinctId: string,
|
|
142
|
+
): string {
|
|
143
|
+
const fromData = [
|
|
144
|
+
data.harness_run_id,
|
|
145
|
+
data.run_id,
|
|
146
|
+
data.runId,
|
|
147
|
+
data.debate_id,
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of fromData) {
|
|
150
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (typeof trace?.run_id === "string" && trace.run_id.length > 0) {
|
|
155
|
+
return trace.run_id;
|
|
156
|
+
}
|
|
157
|
+
return distinctId;
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
function mapCustomEntry(
|
|
138
161
|
customType: string,
|
|
139
162
|
data: Record<string, unknown>,
|
|
@@ -144,11 +167,9 @@ function mapCustomEntry(
|
|
|
144
167
|
event: HarnessPostHogEventName;
|
|
145
168
|
properties: Record<string, unknown>;
|
|
146
169
|
} | null {
|
|
147
|
-
const runId =
|
|
148
|
-
(typeof data.run_id === "string" && data.run_id) ||
|
|
149
|
-
trace?.run_id ||
|
|
150
|
-
distinctId;
|
|
170
|
+
const runId = normalizedRunId(data, trace, distinctId);
|
|
151
171
|
const planId =
|
|
172
|
+
(typeof data.harness_plan_id === "string" && data.harness_plan_id) ||
|
|
152
173
|
(typeof data.plan_id === "string" && data.plan_id) ||
|
|
153
174
|
policy?.planId ||
|
|
154
175
|
trace?.plan_id ||
|
|
@@ -185,6 +206,7 @@ function mapCustomEntry(
|
|
|
185
206
|
event: "harness_debate_consensus",
|
|
186
207
|
properties: {
|
|
187
208
|
...base,
|
|
209
|
+
debate_id: String(data.debate_id ?? runId),
|
|
188
210
|
consensus_id:
|
|
189
211
|
typeof data.debate_id === "string" ? data.debate_id : runId,
|
|
190
212
|
outcome: String(kind),
|
|
@@ -195,6 +217,8 @@ function mapCustomEntry(
|
|
|
195
217
|
event: "harness_debate_round",
|
|
196
218
|
properties: {
|
|
197
219
|
...base,
|
|
220
|
+
debate_id: String(data.debate_id ?? runId),
|
|
221
|
+
round_index: Number(data.round_index ?? data.round ?? 0),
|
|
198
222
|
round: Number(data.round_index ?? data.round ?? 0),
|
|
199
223
|
outcome: String(kind ?? "round"),
|
|
200
224
|
},
|
|
@@ -206,6 +230,7 @@ function mapCustomEntry(
|
|
|
206
230
|
event: "harness_debate_consensus",
|
|
207
231
|
properties: {
|
|
208
232
|
...base,
|
|
233
|
+
debate_id: String(data.debate_id ?? runId),
|
|
209
234
|
consensus_id:
|
|
210
235
|
typeof data.consensus_id === "string"
|
|
211
236
|
? data.consensus_id
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./debate-bus-state.js";
|
|
26
26
|
import {
|
|
27
27
|
type DebateProfile,
|
|
28
|
+
PLAN_BUDGET_FAST,
|
|
28
29
|
PLAN_BUDGET_LIGHT,
|
|
29
30
|
PLAN_BUDGET_STANDARD,
|
|
30
31
|
} from "./plan-debate-eligibility.js";
|
|
@@ -113,15 +114,20 @@ export function capsForDebate(
|
|
|
113
114
|
} {
|
|
114
115
|
if (isPlanDebateId(debateId)) {
|
|
115
116
|
const active = profile ?? getDebateState()?.debate_profile ?? "standard";
|
|
116
|
-
const budget =
|
|
117
|
+
const budget =
|
|
118
|
+
active === "light"
|
|
119
|
+
? PLAN_BUDGET_LIGHT
|
|
120
|
+
: active === "fast"
|
|
121
|
+
? PLAN_BUDGET_FAST
|
|
122
|
+
: PLAN_BUDGET;
|
|
117
123
|
const caps = { name: "plan" as const, ...budget };
|
|
118
124
|
if (!isHarnessBudgetEnforceOn()) {
|
|
119
125
|
return {
|
|
120
126
|
...caps,
|
|
121
|
-
max_rounds:
|
|
122
|
-
max_exchanges_per_round:
|
|
123
|
-
round_token_cap: caps.round_token_cap *
|
|
124
|
-
debate_global_cap: caps.debate_global_cap *
|
|
127
|
+
max_rounds: caps.max_rounds,
|
|
128
|
+
max_exchanges_per_round: Math.max(caps.max_exchanges_per_round, 2),
|
|
129
|
+
round_token_cap: caps.round_token_cap * 2,
|
|
130
|
+
debate_global_cap: caps.debate_global_cap * 2,
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
return caps;
|
|
@@ -135,10 +141,10 @@ export function capsForDebate(
|
|
|
135
141
|
if (!isHarnessBudgetEnforceOn()) {
|
|
136
142
|
return {
|
|
137
143
|
...caps,
|
|
138
|
-
max_rounds:
|
|
139
|
-
max_exchanges_per_round:
|
|
140
|
-
round_token_cap: caps.round_token_cap *
|
|
141
|
-
debate_global_cap: caps.debate_global_cap *
|
|
144
|
+
max_rounds: caps.max_rounds,
|
|
145
|
+
max_exchanges_per_round: Math.max(caps.max_exchanges_per_round, 2),
|
|
146
|
+
round_token_cap: caps.round_token_cap * 2,
|
|
147
|
+
debate_global_cap: caps.debate_global_cap * 2,
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
return caps;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-aware gates for harness_artifact_ready (existence + minimal validity).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { validateAgainstHarnessSchema } from "../../lib/harness-schema-validate.js";
|
|
10
|
+
|
|
11
|
+
export interface ArtifactGateResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ARTIFACT_SCHEMA: Record<string, string> = {
|
|
17
|
+
"artifacts/decomposition.yaml": "plan-decomposition-brief.schema.json",
|
|
18
|
+
"artifacts/hypothesis.yaml": "plan-hypothesis-brief.schema.json",
|
|
19
|
+
"artifacts/implementation-research.yaml":
|
|
20
|
+
"plan-implementation-research-brief.schema.json",
|
|
21
|
+
"artifacts/stack.yaml": "plan-stack-brief.schema.json",
|
|
22
|
+
"artifacts/planning-context.yaml": "plan-planning-context.schema.json",
|
|
23
|
+
"artifacts/scout-graphify.yaml": "plan-scout-findings.schema.json",
|
|
24
|
+
"artifacts/scout-structure.yaml": "plan-scout-findings.schema.json",
|
|
25
|
+
"artifacts/scout-semantic.yaml": "plan-scout-findings.schema.json",
|
|
26
|
+
"artifacts/eval-verdict.yaml": "eval-verdict.schema.json",
|
|
27
|
+
"artifacts/adversary-report.yaml": "adversary-report.schema.json",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const PREREQUISITE_ORDER: Record<string, string[]> = {
|
|
31
|
+
"artifacts/hypothesis.yaml": ["artifacts/decomposition.yaml"],
|
|
32
|
+
"artifacts/implementation-research.yaml": [
|
|
33
|
+
"artifacts/decomposition.yaml",
|
|
34
|
+
"artifacts/hypothesis.yaml",
|
|
35
|
+
],
|
|
36
|
+
"artifacts/stack.yaml": [
|
|
37
|
+
"artifacts/decomposition.yaml",
|
|
38
|
+
"artifacts/hypothesis.yaml",
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
await access(path, constants.R_OK);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scoutStatusBad(doc: Record<string, unknown>): string | null {
|
|
52
|
+
const status = String(doc.status ?? "ok").toLowerCase();
|
|
53
|
+
if (status === "partial" || status === "failed" || status === "error") {
|
|
54
|
+
return `scout status is "${status}"`;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function validateHarnessArtifactFile(
|
|
60
|
+
runRoot: string,
|
|
61
|
+
relPath: string,
|
|
62
|
+
specsDir: string,
|
|
63
|
+
): Promise<ArtifactGateResult> {
|
|
64
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
65
|
+
const abs = join(runRoot, normalized);
|
|
66
|
+
const errors: string[] = [];
|
|
67
|
+
|
|
68
|
+
if (!(await fileExists(abs))) {
|
|
69
|
+
return { ok: false, errors: [`missing file: ${normalized}`] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const st = await stat(abs);
|
|
73
|
+
if (st.size < 8) {
|
|
74
|
+
errors.push(`${normalized}: file too small (${st.size} bytes)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let doc: Record<string, unknown> | null = null;
|
|
78
|
+
try {
|
|
79
|
+
const raw = await readFile(abs, "utf-8");
|
|
80
|
+
if (!raw.trim()) {
|
|
81
|
+
errors.push(`${normalized}: empty file`);
|
|
82
|
+
} else {
|
|
83
|
+
doc = parseYaml(raw) as Record<string, unknown>;
|
|
84
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
85
|
+
errors.push(`${normalized}: root must be a YAML object`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
errors.push(
|
|
90
|
+
`${normalized}: invalid YAML (${e instanceof Error ? e.message : String(e)})`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const schemaFile = ARTIFACT_SCHEMA[normalized];
|
|
95
|
+
if (doc && schemaFile) {
|
|
96
|
+
const validation = await validateAgainstHarnessSchema(
|
|
97
|
+
specsDir,
|
|
98
|
+
schemaFile,
|
|
99
|
+
doc,
|
|
100
|
+
);
|
|
101
|
+
if (!validation.ok) {
|
|
102
|
+
errors.push(
|
|
103
|
+
`${normalized}: schema validation failed — ${validation.errors.join("; ")}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (doc && normalized.startsWith("artifacts/scout-")) {
|
|
109
|
+
const scoutErr = scoutStatusBad(doc);
|
|
110
|
+
if (scoutErr) {
|
|
111
|
+
errors.push(`${normalized}: ${scoutErr}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (doc && normalized === "artifacts/planning-context.yaml") {
|
|
116
|
+
const scoutErr = scoutStatusBad(doc);
|
|
117
|
+
if (scoutErr) {
|
|
118
|
+
errors.push(`${normalized}: ${scoutErr}`);
|
|
119
|
+
}
|
|
120
|
+
const coverage = doc.coverage as Record<string, unknown> | undefined;
|
|
121
|
+
if (coverage && typeof coverage === "object") {
|
|
122
|
+
for (const lane of ["architecture", "structure"] as const) {
|
|
123
|
+
const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
|
|
124
|
+
const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
|
|
125
|
+
if (laneStatus !== "ok" && laneStatus !== "partial") {
|
|
126
|
+
errors.push(
|
|
127
|
+
`${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const prereqs = PREREQUISITE_ORDER[normalized] ?? [];
|
|
135
|
+
for (const prereq of prereqs) {
|
|
136
|
+
if (!(await fileExists(join(runRoot, prereq)))) {
|
|
137
|
+
errors.push(`${normalized}: prerequisite missing (${prereq})`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { ok: errors.length === 0, errors };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function validateHarnessArtifactPaths(
|
|
145
|
+
runRoot: string,
|
|
146
|
+
paths: string[],
|
|
147
|
+
specsDir: string,
|
|
148
|
+
): Promise<{
|
|
149
|
+
ok: boolean;
|
|
150
|
+
present: string[];
|
|
151
|
+
missing: string[];
|
|
152
|
+
errors: string[];
|
|
153
|
+
}> {
|
|
154
|
+
const present: string[] = [];
|
|
155
|
+
const missing: string[] = [];
|
|
156
|
+
const errors: string[] = [];
|
|
157
|
+
|
|
158
|
+
for (const rel of paths) {
|
|
159
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
160
|
+
const gate = await validateHarnessArtifactFile(
|
|
161
|
+
runRoot,
|
|
162
|
+
normalized,
|
|
163
|
+
specsDir,
|
|
164
|
+
);
|
|
165
|
+
if (gate.errors.some((e) => e.startsWith("missing file"))) {
|
|
166
|
+
missing.push(normalized);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!gate.ok) {
|
|
170
|
+
errors.push(...gate.errors);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
present.push(normalized);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ok: missing.length === 0 && errors.length === 0,
|
|
178
|
+
present,
|
|
179
|
+
missing,
|
|
180
|
+
errors,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { PostHog } from "posthog-node";
|
|
9
|
+
import { getPostHogClientOptions } from "./posthog-client.js";
|
|
9
10
|
|
|
10
11
|
export type HarnessPostHogEventName =
|
|
11
12
|
| "harness_run_started"
|
|
@@ -48,9 +49,7 @@ function getClient(): PostHog | null {
|
|
|
48
49
|
if (client) return client;
|
|
49
50
|
const apiKey = process.env.POSTHOG_API_KEY?.trim();
|
|
50
51
|
if (!apiKey) return null;
|
|
51
|
-
client = new PostHog(apiKey,
|
|
52
|
-
host: process.env.POSTHOG_HOST?.trim() || "https://us.i.posthog.com",
|
|
53
|
-
});
|
|
52
|
+
client = new PostHog(apiKey, getPostHogClientOptions());
|
|
54
53
|
return client;
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -109,6 +108,11 @@ export function captureHarnessEvent(
|
|
|
109
108
|
|
|
110
109
|
export async function shutdownHarnessPostHog(): Promise<void> {
|
|
111
110
|
if (!client) return;
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
try {
|
|
112
|
+
await client.shutdown();
|
|
113
|
+
} catch {
|
|
114
|
+
// Best-effort telemetry — avoid noisy flush errors when offline / WSL DNS broken.
|
|
115
|
+
} finally {
|
|
116
|
+
client = null;
|
|
117
|
+
}
|
|
114
118
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness subagent spawn topology rules (no vendor imports — testable in isolation).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { HarnessPhase } from "../../lib/harness-run-context.js";
|
|
9
|
+
|
|
10
|
+
export interface SpawnTopologyResult {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
message?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DECOMPOSE_AGENT = "harness/planning/decompose";
|
|
16
|
+
const HYPOTHESIS_AGENT = "harness/planning/hypothesis";
|
|
17
|
+
|
|
18
|
+
const DEBATE_LANE_AGENTS = new Set([
|
|
19
|
+
"harness/planning/hypothesis-validator",
|
|
20
|
+
"harness/planning/plan-evaluator",
|
|
21
|
+
"harness/planning/plan-adversary",
|
|
22
|
+
"harness/planning/sprint-contract-auditor",
|
|
23
|
+
"harness/planning/review-integrator",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** @deprecated Legacy tool-tied scouts — prefer parent tools + planning-context.yaml (ADR 0041). */
|
|
27
|
+
const LEGACY_SCOUT_AGENTS = new Set([
|
|
28
|
+
"harness/planning/scout-graphify",
|
|
29
|
+
"harness/planning/scout-structure",
|
|
30
|
+
"harness/planning/scout-semantic",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const PLANNING_CONTEXT_AGENT = "harness/planning/planning-context";
|
|
34
|
+
|
|
35
|
+
const PARALLEL_RESEARCH_AGENTS = new Set([
|
|
36
|
+
"harness/planning/implementation-researcher",
|
|
37
|
+
"harness/planning/stack-researcher",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function countInSet(names: string[], allowed: Set<string>): number {
|
|
41
|
+
return names.filter((n) => allowed.has(n)).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isReconnaissanceAgent(name: string): boolean {
|
|
45
|
+
return LEGACY_SCOUT_AGENTS.has(name) || name === PLANNING_CONTEXT_AGENT;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function decompositionReady(
|
|
49
|
+
projectRoot: string,
|
|
50
|
+
runId: string,
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const path = join(
|
|
53
|
+
projectRoot,
|
|
54
|
+
".pi",
|
|
55
|
+
"harness",
|
|
56
|
+
"runs",
|
|
57
|
+
runId,
|
|
58
|
+
"artifacts",
|
|
59
|
+
"decomposition.yaml",
|
|
60
|
+
);
|
|
61
|
+
try {
|
|
62
|
+
await access(path, constants.R_OK);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function validateHarnessSpawnTopology(
|
|
70
|
+
names: string[],
|
|
71
|
+
phase: HarnessPhase,
|
|
72
|
+
opts?: {
|
|
73
|
+
parallelTaskCount?: number;
|
|
74
|
+
projectRoot?: string;
|
|
75
|
+
runId?: string | null;
|
|
76
|
+
},
|
|
77
|
+
): Promise<SpawnTopologyResult> {
|
|
78
|
+
const taskCount =
|
|
79
|
+
opts?.parallelTaskCount ?? (names.length > 1 ? names.length : 1);
|
|
80
|
+
|
|
81
|
+
if (taskCount > 1) {
|
|
82
|
+
const hasDecompose = names.includes(DECOMPOSE_AGENT);
|
|
83
|
+
const hasHypothesis = names.includes(HYPOTHESIS_AGENT);
|
|
84
|
+
if (hasDecompose && hasHypothesis) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
message:
|
|
88
|
+
"Cannot spawn decompose and hypothesis in the same parallel batch. " +
|
|
89
|
+
"Gate artifacts/decomposition.yaml, then spawn hypothesis sequentially.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const debateCount = countInSet(names, DEBATE_LANE_AGENTS);
|
|
94
|
+
const debateNames = names.filter((n) => DEBATE_LANE_AGENTS.has(n));
|
|
95
|
+
const parallelProbePair =
|
|
96
|
+
debateCount === 2 &&
|
|
97
|
+
debateNames.includes("harness/planning/plan-evaluator") &&
|
|
98
|
+
debateNames.includes("harness/planning/plan-adversary");
|
|
99
|
+
if (debateCount > 1 && !parallelProbePair) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
message: `Review Gate: spawn one debate lane agent per subagent call (got ${debateCount}: ${debateNames.join(", ")}). Exception: plan-evaluator ∥ plan-adversary for parallel_probes.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const legacyScouts = countInSet(names, LEGACY_SCOUT_AGENTS);
|
|
107
|
+
const planningContext = names.filter(
|
|
108
|
+
(n) => n === PLANNING_CONTEXT_AGENT,
|
|
109
|
+
).length;
|
|
110
|
+
const research = countInSet(names, PARALLEL_RESEARCH_AGENTS);
|
|
111
|
+
const recon = legacyScouts + planningContext;
|
|
112
|
+
|
|
113
|
+
if (legacyScouts > 0 && planningContext > 0) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
message:
|
|
117
|
+
"Do not mix legacy scout-* subagents with planning-context in one batch. " +
|
|
118
|
+
"Prefer parent tool use + planning-context.yaml, or a single planning-context subagent.",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (legacyScouts > 3) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message:
|
|
125
|
+
"At most 3 legacy planning scouts per parallel batch (deprecated — use planning-context).",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (planningContext > 1) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
message: "At most one planning-context subagent per parallel batch.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const otherHarness = names.filter(
|
|
136
|
+
(n) =>
|
|
137
|
+
n.startsWith("harness/") &&
|
|
138
|
+
!isReconnaissanceAgent(n) &&
|
|
139
|
+
!PARALLEL_RESEARCH_AGENTS.has(n) &&
|
|
140
|
+
!DEBATE_LANE_AGENTS.has(n) &&
|
|
141
|
+
n !== DECOMPOSE_AGENT &&
|
|
142
|
+
n !== HYPOTHESIS_AGENT,
|
|
143
|
+
);
|
|
144
|
+
if (
|
|
145
|
+
(recon > 0 && (research > 0 || otherHarness.length > 0)) ||
|
|
146
|
+
(research > 0 && otherHarness.length > 0)
|
|
147
|
+
) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
message:
|
|
151
|
+
"Parallel batches may include only one independent group: " +
|
|
152
|
+
"research (≤2 lanes), optional legacy scouts (≤3), optional single planning-context, " +
|
|
153
|
+
"or a single sequential lane agent.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (research > 2) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
message:
|
|
160
|
+
"At most 2 research lanes (implementation-researcher, stack-researcher) per parallel batch.",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (names.includes(HYPOTHESIS_AGENT) && opts?.projectRoot && opts?.runId) {
|
|
166
|
+
const ready = await decompositionReady(opts.projectRoot, opts.runId);
|
|
167
|
+
if (!ready) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
message:
|
|
171
|
+
"Cannot spawn hypothesis before artifacts/decomposition.yaml exists. " +
|
|
172
|
+
"Complete decompose and harness_artifact_ready on decomposition first.",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (phase === "plan") {
|
|
178
|
+
const mutating = names.filter((n) => n.startsWith("harness/executor"));
|
|
179
|
+
if (mutating.length > 0) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
message: `Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}).`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|