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
|
@@ -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 {
|
|
9
|
+
import { resolveGuardedRunDir } from "../lib/harness-subagent-submit-path.js";
|
|
10
|
+
import { claimHarnessGovernanceLoad } 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,10 +21,18 @@ const MODULE_URL = import.meta.url;
|
|
|
17
21
|
|
|
18
22
|
const DocumentSchema = Type.Object(
|
|
19
23
|
{
|
|
20
|
-
document: Type.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
),
|
|
24
36
|
},
|
|
25
37
|
{ additionalProperties: false },
|
|
26
38
|
);
|
|
@@ -48,7 +60,8 @@ function isSubprocessHarness(): boolean {
|
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
51
|
-
if (!
|
|
63
|
+
if (!claimHarnessGovernanceLoad("harness-subagent-submit", MODULE_URL))
|
|
64
|
+
return;
|
|
52
65
|
// Option A: only load submit tools in subprocess (`-e` bundle), not parent discovery.
|
|
53
66
|
if (process.env.PI_HARNESS_SUBPROCESS !== "1") {
|
|
54
67
|
return;
|
|
@@ -118,21 +131,42 @@ export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
|
118
131
|
isError: true,
|
|
119
132
|
};
|
|
120
133
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
const runResolved = await resolveGuardedRunDir({
|
|
135
|
+
projectRoot,
|
|
136
|
+
runId,
|
|
137
|
+
runDirEnv,
|
|
138
|
+
});
|
|
139
|
+
if (!runResolved.ok) {
|
|
124
140
|
return {
|
|
125
|
-
content: [{ type: "text", text:
|
|
141
|
+
content: [{ type: "text", text: runResolved.error }],
|
|
126
142
|
details: {},
|
|
127
143
|
isError: true,
|
|
128
144
|
};
|
|
129
145
|
}
|
|
146
|
+
const loaded = await loadSubmitDocument({
|
|
147
|
+
projectRoot,
|
|
148
|
+
runDir: runResolved.runDir,
|
|
149
|
+
document: (params as { document?: Record<string, unknown> }).document,
|
|
150
|
+
source_path: (params as { source_path?: string }).source_path,
|
|
151
|
+
});
|
|
152
|
+
if (!loaded.ok) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `Validation failed:\n${loaded.validation_errors.join("\n")}`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
isError: true,
|
|
161
|
+
details: loaded,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
130
164
|
const result = await executeSubmitPipeline({
|
|
131
165
|
projectRoot,
|
|
132
166
|
specsDir,
|
|
133
167
|
spec,
|
|
134
168
|
agentId,
|
|
135
|
-
document,
|
|
169
|
+
document: loaded.document,
|
|
136
170
|
runId,
|
|
137
171
|
runDirEnv,
|
|
138
172
|
});
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
9
|
+
import { claimHarnessGovernanceLoad } from "./lib/extension-load-guard.js";
|
|
10
10
|
|
|
11
11
|
// @ts-expect-error pi extensions run as ESM
|
|
12
12
|
const MODULE_URL = import.meta.url;
|
|
13
13
|
|
|
14
14
|
async function loadHarnessSubagents(): Promise<(pi: ExtensionAPI) => void> {
|
|
15
|
-
if (!
|
|
15
|
+
if (!claimHarnessGovernanceLoad("harness-subagents", MODULE_URL)) {
|
|
16
16
|
return () => {};
|
|
17
17
|
}
|
|
18
18
|
const { getHarnessPackageRoot } = await import("./lib/harness-paths.js");
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
|
|
12
13
|
import {
|
|
13
14
|
captureHarnessEvent,
|
|
14
15
|
type HarnessPostHogEventName,
|
|
@@ -338,6 +339,7 @@ function mapCustomEntry(
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
export default function harnessTelemetry(pi: ExtensionAPI) {
|
|
342
|
+
if (!isHarnessProjectEnabled()) return;
|
|
341
343
|
const flushedHashes = new Set<string>();
|
|
342
344
|
let lastPolicyPhase: HarnessPhase | null = null;
|
|
343
345
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
-
import {
|
|
7
|
+
import { claimHarnessGovernanceLoad } from "./lib/extension-load-guard.js";
|
|
8
8
|
import {
|
|
9
9
|
harnessWebContextLine,
|
|
10
10
|
readTextExcerpt,
|
|
@@ -98,7 +98,7 @@ function sessionCwd(ctx: { cwd?: string }): string {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export default function harnessWebTools(pi: ExtensionAPI) {
|
|
101
|
-
if (!
|
|
101
|
+
if (!claimHarnessGovernanceLoad("harness-web-tools", MODULE_URL)) return;
|
|
102
102
|
pi.on("before_agent_start", async (event) => {
|
|
103
103
|
return {
|
|
104
104
|
systemPrompt: `${event.systemPrompt}\n\n${harnessWebContextLine()}`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isHarnessProjectEnabled } from "../../lib/harness-project-config.js";
|
|
4
5
|
|
|
5
6
|
const LOAD_GUARD_KEY = Symbol.for("ultimate-pi.extension-load-guard");
|
|
6
7
|
|
|
@@ -37,3 +38,12 @@ export function claimExtensionLoad(key: string, moduleUrl: string): boolean {
|
|
|
37
38
|
registry.add(key);
|
|
38
39
|
return true;
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Skip duplicate loads and skip all governance extensions when harness is disabled. */
|
|
43
|
+
export function claimHarnessGovernanceLoad(
|
|
44
|
+
key: string,
|
|
45
|
+
moduleUrl: string,
|
|
46
|
+
): boolean {
|
|
47
|
+
if (!isHarnessProjectEnabled()) return false;
|
|
48
|
+
return claimExtensionLoad(key, moduleUrl);
|
|
49
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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/eval-verdict.yaml": "eval-verdict.schema.json",
|
|
24
|
+
"artifacts/adversary-report.yaml": "adversary-report.schema.json",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PREREQUISITE_ORDER: Record<string, string[]> = {
|
|
28
|
+
"artifacts/hypothesis.yaml": ["artifacts/decomposition.yaml"],
|
|
29
|
+
"artifacts/implementation-research.yaml": [
|
|
30
|
+
"artifacts/decomposition.yaml",
|
|
31
|
+
"artifacts/hypothesis.yaml",
|
|
32
|
+
],
|
|
33
|
+
"artifacts/stack.yaml": [
|
|
34
|
+
"artifacts/decomposition.yaml",
|
|
35
|
+
"artifacts/hypothesis.yaml",
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
40
|
+
try {
|
|
41
|
+
await access(path, constants.R_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function artifactStatusBad(doc: Record<string, unknown>): string | null {
|
|
49
|
+
const status = String(doc.status ?? "ok").toLowerCase();
|
|
50
|
+
if (status === "partial" || status === "failed" || status === "error") {
|
|
51
|
+
return `artifact status is "${status}"`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function validateHarnessArtifactFile(
|
|
57
|
+
runRoot: string,
|
|
58
|
+
relPath: string,
|
|
59
|
+
specsDir: string,
|
|
60
|
+
): Promise<ArtifactGateResult> {
|
|
61
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
62
|
+
const abs = join(runRoot, normalized);
|
|
63
|
+
const errors: string[] = [];
|
|
64
|
+
|
|
65
|
+
if (!(await fileExists(abs))) {
|
|
66
|
+
return { ok: false, errors: [`missing file: ${normalized}`] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const st = await stat(abs);
|
|
70
|
+
if (st.size < 8) {
|
|
71
|
+
errors.push(`${normalized}: file too small (${st.size} bytes)`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let doc: Record<string, unknown> | null = null;
|
|
75
|
+
try {
|
|
76
|
+
const raw = await readFile(abs, "utf-8");
|
|
77
|
+
if (!raw.trim()) {
|
|
78
|
+
errors.push(`${normalized}: empty file`);
|
|
79
|
+
} else {
|
|
80
|
+
doc = parseYaml(raw) as Record<string, unknown>;
|
|
81
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
82
|
+
errors.push(`${normalized}: root must be a YAML object`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
errors.push(
|
|
87
|
+
`${normalized}: invalid YAML (${e instanceof Error ? e.message : String(e)})`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const schemaFile = ARTIFACT_SCHEMA[normalized];
|
|
92
|
+
if (doc && schemaFile) {
|
|
93
|
+
const validation = await validateAgainstHarnessSchema(
|
|
94
|
+
specsDir,
|
|
95
|
+
schemaFile,
|
|
96
|
+
doc,
|
|
97
|
+
);
|
|
98
|
+
if (!validation.ok) {
|
|
99
|
+
errors.push(
|
|
100
|
+
`${normalized}: schema validation failed — ${validation.errors.join("; ")}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (doc && normalized === "artifacts/planning-context.yaml") {
|
|
106
|
+
const statusErr = artifactStatusBad(doc);
|
|
107
|
+
if (statusErr) {
|
|
108
|
+
errors.push(`${normalized}: ${statusErr}`);
|
|
109
|
+
}
|
|
110
|
+
const coverage = doc.coverage as Record<string, unknown> | undefined;
|
|
111
|
+
if (coverage && typeof coverage === "object") {
|
|
112
|
+
for (const lane of ["architecture", "structure"] as const) {
|
|
113
|
+
const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
|
|
114
|
+
const laneStatus = String(laneDoc?.status ?? "").toLowerCase();
|
|
115
|
+
if (laneStatus !== "ok" && laneStatus !== "partial") {
|
|
116
|
+
errors.push(
|
|
117
|
+
`${normalized}: coverage.${lane}.status must be ok or partial (got "${laneStatus || "missing"}")`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const prereqs = PREREQUISITE_ORDER[normalized] ?? [];
|
|
125
|
+
for (const prereq of prereqs) {
|
|
126
|
+
if (!(await fileExists(join(runRoot, prereq)))) {
|
|
127
|
+
errors.push(`${normalized}: prerequisite missing (${prereq})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { ok: errors.length === 0, errors };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function validateHarnessArtifactPaths(
|
|
135
|
+
runRoot: string,
|
|
136
|
+
paths: string[],
|
|
137
|
+
specsDir: string,
|
|
138
|
+
): Promise<{
|
|
139
|
+
ok: boolean;
|
|
140
|
+
present: string[];
|
|
141
|
+
missing: string[];
|
|
142
|
+
errors: string[];
|
|
143
|
+
}> {
|
|
144
|
+
const present: string[] = [];
|
|
145
|
+
const missing: string[] = [];
|
|
146
|
+
const errors: string[] = [];
|
|
147
|
+
|
|
148
|
+
for (const rel of paths) {
|
|
149
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
150
|
+
const gate = await validateHarnessArtifactFile(
|
|
151
|
+
runRoot,
|
|
152
|
+
normalized,
|
|
153
|
+
specsDir,
|
|
154
|
+
);
|
|
155
|
+
if (gate.errors.some((e) => e.startsWith("missing file"))) {
|
|
156
|
+
missing.push(normalized);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!gate.ok) {
|
|
160
|
+
errors.push(...gate.errors);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
present.push(normalized);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ok: missing.length === 0 && errors.length === 0,
|
|
168
|
+
present,
|
|
169
|
+
missing,
|
|
170
|
+
errors,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -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,165 @@
|
|
|
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
|
+
const PLANNING_CONTEXT_AGENT = "harness/planning/planning-context";
|
|
27
|
+
|
|
28
|
+
const PARALLEL_RESEARCH_AGENTS = new Set([
|
|
29
|
+
"harness/planning/implementation-researcher",
|
|
30
|
+
"harness/planning/stack-researcher",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function countInSet(names: string[], allowed: Set<string>): number {
|
|
34
|
+
return names.filter((n) => allowed.has(n)).length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isReconnaissanceAgent(name: string): boolean {
|
|
38
|
+
return name === PLANNING_CONTEXT_AGENT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function decompositionReady(
|
|
42
|
+
projectRoot: string,
|
|
43
|
+
runId: string,
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
const path = join(
|
|
46
|
+
projectRoot,
|
|
47
|
+
".pi",
|
|
48
|
+
"harness",
|
|
49
|
+
"runs",
|
|
50
|
+
runId,
|
|
51
|
+
"artifacts",
|
|
52
|
+
"decomposition.yaml",
|
|
53
|
+
);
|
|
54
|
+
try {
|
|
55
|
+
await access(path, constants.R_OK);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function validateHarnessSpawnTopology(
|
|
63
|
+
names: string[],
|
|
64
|
+
phase: HarnessPhase,
|
|
65
|
+
opts?: {
|
|
66
|
+
parallelTaskCount?: number;
|
|
67
|
+
projectRoot?: string;
|
|
68
|
+
runId?: string | null;
|
|
69
|
+
},
|
|
70
|
+
): Promise<SpawnTopologyResult> {
|
|
71
|
+
const taskCount =
|
|
72
|
+
opts?.parallelTaskCount ?? (names.length > 1 ? names.length : 1);
|
|
73
|
+
|
|
74
|
+
if (taskCount > 1) {
|
|
75
|
+
const hasDecompose = names.includes(DECOMPOSE_AGENT);
|
|
76
|
+
const hasHypothesis = names.includes(HYPOTHESIS_AGENT);
|
|
77
|
+
if (hasDecompose && hasHypothesis) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
message:
|
|
81
|
+
"Cannot spawn decompose and hypothesis in the same parallel batch. " +
|
|
82
|
+
"Gate artifacts/decomposition.yaml, then spawn hypothesis sequentially.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const debateCount = countInSet(names, DEBATE_LANE_AGENTS);
|
|
87
|
+
const debateNames = names.filter((n) => DEBATE_LANE_AGENTS.has(n));
|
|
88
|
+
const parallelProbePair =
|
|
89
|
+
debateCount === 2 &&
|
|
90
|
+
debateNames.includes("harness/planning/plan-evaluator") &&
|
|
91
|
+
debateNames.includes("harness/planning/plan-adversary");
|
|
92
|
+
if (debateCount > 1 && !parallelProbePair) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
message: `Review Gate: spawn one debate lane agent per subagent call (got ${debateCount}: ${debateNames.join(", ")}). Exception: plan-evaluator ∥ plan-adversary for parallel_probes.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const planningContext = names.filter(
|
|
100
|
+
(n) => n === PLANNING_CONTEXT_AGENT,
|
|
101
|
+
).length;
|
|
102
|
+
const research = countInSet(names, PARALLEL_RESEARCH_AGENTS);
|
|
103
|
+
const recon = planningContext;
|
|
104
|
+
|
|
105
|
+
if (planningContext > 1) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
message: "At most one planning-context subagent per parallel batch.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const otherHarness = names.filter(
|
|
113
|
+
(n) =>
|
|
114
|
+
n.startsWith("harness/") &&
|
|
115
|
+
!isReconnaissanceAgent(n) &&
|
|
116
|
+
!PARALLEL_RESEARCH_AGENTS.has(n) &&
|
|
117
|
+
!DEBATE_LANE_AGENTS.has(n) &&
|
|
118
|
+
n !== DECOMPOSE_AGENT &&
|
|
119
|
+
n !== HYPOTHESIS_AGENT,
|
|
120
|
+
);
|
|
121
|
+
if (
|
|
122
|
+
(recon > 0 && (research > 0 || otherHarness.length > 0)) ||
|
|
123
|
+
(research > 0 && otherHarness.length > 0)
|
|
124
|
+
) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
message:
|
|
128
|
+
"Parallel batches may include only one independent group: " +
|
|
129
|
+
"research (≤2 lanes), optional single planning-context, " +
|
|
130
|
+
"or a single sequential lane agent.",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (research > 2) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
message:
|
|
137
|
+
"At most 2 research lanes (implementation-researcher, stack-researcher) per parallel batch.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (names.includes(HYPOTHESIS_AGENT) && opts?.projectRoot && opts?.runId) {
|
|
143
|
+
const ready = await decompositionReady(opts.projectRoot, opts.runId);
|
|
144
|
+
if (!ready) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
message:
|
|
148
|
+
"Cannot spawn hypothesis before artifacts/decomposition.yaml exists. " +
|
|
149
|
+
"Complete decompose and harness_artifact_ready on decomposition first.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (phase === "plan") {
|
|
155
|
+
const mutating = names.filter((n) => n.startsWith("harness/running/"));
|
|
156
|
+
if (mutating.length > 0) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
message: `Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}).`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
@@ -51,8 +51,7 @@ const ROUTINE_PLANNING_AGENT_PATHS = new Set([
|
|
|
51
51
|
"harness/planning/review-integrator",
|
|
52
52
|
"harness/planning/hypothesis-validator",
|
|
53
53
|
"harness/planning/sprint-contract-auditor",
|
|
54
|
-
"harness/planning/
|
|
55
|
-
"harness/planning/scout-semantic",
|
|
54
|
+
"harness/planning/planning-context",
|
|
56
55
|
"harness/planning/decompose",
|
|
57
56
|
"harness/planning/hypothesis",
|
|
58
57
|
"harness/planning/stack-research",
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Per-agent tool policy for harness/* subagents (defense in depth with frontmatter).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
evaluateContextModeMutation,
|
|
7
|
+
isMutatingBash,
|
|
8
|
+
} from "../../lib/harness-context-mode-policy.js";
|
|
9
|
+
import type { HarnessPhase } from "../../lib/harness-run-context.js";
|
|
5
10
|
import {
|
|
6
11
|
isSubmitToolName,
|
|
7
12
|
SUBMIT_TOOLS_BY_AGENT,
|
|
@@ -40,21 +45,6 @@ const PLANNING_BASH_DENY_PATTERNS = [
|
|
|
40
45
|
/\buv\s+tool\s+install\b.*cocoindex/i,
|
|
41
46
|
];
|
|
42
47
|
|
|
43
|
-
const BASH_MUTATION_PATTERNS = [
|
|
44
|
-
/\brm\s+-/i,
|
|
45
|
-
/\bmv\s+/i,
|
|
46
|
-
/\bcp\s+/i,
|
|
47
|
-
/\btouch\s+/i,
|
|
48
|
-
/\bmkdir\s+/i,
|
|
49
|
-
/\btee\s+/i,
|
|
50
|
-
/\bgit\s+(add|commit|push|reset|checkout|merge|rebase|cherry-pick|apply)\b/i,
|
|
51
|
-
/\bnpm\s+(install|uninstall|ci)\b/i,
|
|
52
|
-
/\bpnpm\s+(add|install|remove)\b/i,
|
|
53
|
-
/\byarn\s+(add|install|remove)\b/i,
|
|
54
|
-
/\bsed\s+-i\b/i,
|
|
55
|
-
/\bperl\s+-i\b/i,
|
|
56
|
-
];
|
|
57
|
-
|
|
58
48
|
const READ_ONLY_KINDS = new Set<HarnessAgentKind>([
|
|
59
49
|
"planner",
|
|
60
50
|
"evaluator",
|
|
@@ -76,13 +66,13 @@ export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
|
|
|
76
66
|
return "planner";
|
|
77
67
|
}
|
|
78
68
|
switch (id) {
|
|
79
|
-
case "executor":
|
|
69
|
+
case "running/executor":
|
|
80
70
|
return "executor";
|
|
81
|
-
case "evaluator":
|
|
71
|
+
case "reviewing/evaluator":
|
|
82
72
|
return "evaluator";
|
|
83
|
-
case "adversary":
|
|
73
|
+
case "reviewing/adversary":
|
|
84
74
|
return "adversary";
|
|
85
|
-
case "tie-breaker":
|
|
75
|
+
case "reviewing/tie-breaker":
|
|
86
76
|
return "tie_breaker";
|
|
87
77
|
case "meta-optimizer":
|
|
88
78
|
return "meta";
|
|
@@ -95,10 +85,6 @@ export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
|
|
|
95
85
|
}
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
function isMutatingBash(command: string): boolean {
|
|
99
|
-
return BASH_MUTATION_PATTERNS.some((pattern) => pattern.test(command));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
88
|
export function isHarnessPackageAgent(agentType: string): boolean {
|
|
103
89
|
return agentType.startsWith("harness/");
|
|
104
90
|
}
|
|
@@ -141,7 +127,7 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
141
127
|
return {
|
|
142
128
|
action: "block",
|
|
143
129
|
reason:
|
|
144
|
-
"submit_human_required is not available for harness/executor.",
|
|
130
|
+
"submit_human_required is not available for harness/running/executor.",
|
|
145
131
|
};
|
|
146
132
|
}
|
|
147
133
|
return { action: "allow" };
|
|
@@ -207,6 +193,24 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
207
193
|
}
|
|
208
194
|
}
|
|
209
195
|
|
|
196
|
+
const ctxPhase =
|
|
197
|
+
(harnessSubagentPhaseHint(agentType) as HarnessPhase | null) ?? "plan";
|
|
198
|
+
const ctxDecision = evaluateContextModeMutation(
|
|
199
|
+
toolName,
|
|
200
|
+
input ?? {},
|
|
201
|
+
ctxPhase,
|
|
202
|
+
{ aborted: false, readOnlyAgent: true },
|
|
203
|
+
);
|
|
204
|
+
if (ctxDecision.blocked) {
|
|
205
|
+
return {
|
|
206
|
+
action: "block",
|
|
207
|
+
reason: ctxDecision.reason.replace(
|
|
208
|
+
/^policy-gate:/,
|
|
209
|
+
"harness-subagent-policy:",
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
return { action: "allow" };
|
|
211
215
|
}
|
|
212
216
|
|