ultimate-pi 0.17.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 +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/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-live-widget.ts +27 -1
- package/.pi/extensions/harness-plan-approval.ts +62 -56
- package/.pi/extensions/harness-run-context.ts +541 -84
- package/.pi/extensions/harness-subagent-submit.ts +43 -10
- 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 +1 -0
- package/.pi/extensions/lib/harness-subagent-policy.ts +23 -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 +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 +241 -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/policy-gate.ts +24 -19
- 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/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 +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/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/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 +118 -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-steer.md +30 -0
- package/.pi/scripts/graphify-kb-updater.mjs +358 -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 +11 -0
- package/package.json +5 -4
- 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,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
|
);
|
|
@@ -118,21 +130,42 @@ export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
|
118
130
|
isError: true,
|
|
119
131
|
};
|
|
120
132
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
const runResolved = await resolveGuardedRunDir({
|
|
134
|
+
projectRoot,
|
|
135
|
+
runId,
|
|
136
|
+
runDirEnv,
|
|
137
|
+
});
|
|
138
|
+
if (!runResolved.ok) {
|
|
124
139
|
return {
|
|
125
|
-
content: [{ type: "text", text:
|
|
140
|
+
content: [{ type: "text", text: runResolved.error }],
|
|
126
141
|
details: {},
|
|
127
142
|
isError: true,
|
|
128
143
|
};
|
|
129
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
|
+
}
|
|
130
163
|
const result = await executeSubmitPipeline({
|
|
131
164
|
projectRoot,
|
|
132
165
|
specsDir,
|
|
133
166
|
spec,
|
|
134
167
|
agentId,
|
|
135
|
-
document,
|
|
168
|
+
document: loaded.document,
|
|
136
169
|
runId,
|
|
137
170
|
runDirEnv,
|
|
138
171
|
});
|
|
@@ -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
|
+
}
|
|
@@ -51,6 +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/planning-context",
|
|
54
55
|
"harness/planning/scout-structure",
|
|
55
56
|
"harness/planning/scout-semantic",
|
|
56
57
|
"harness/planning/decompose",
|
|
@@ -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",
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
type AgentConfig,
|
|
7
7
|
agentAllowsMutatingTools,
|
|
8
8
|
} from "../../../vendor/pi-subagents/src/agents.js";
|
|
9
|
-
import
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
type HarnessPhase,
|
|
11
|
+
inferHarnessPhase,
|
|
12
|
+
} from "../../lib/harness-run-context.js";
|
|
13
|
+
import { validateHarnessSpawnTopology } from "./harness-spawn-topology.js";
|
|
11
14
|
import { classifyHarnessAgent } from "./harness-subagent-policy.js";
|
|
12
15
|
|
|
13
16
|
export interface SubagentTaskRef {
|
|
@@ -19,6 +22,11 @@ export interface PrecheckResult {
|
|
|
19
22
|
message?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
export interface PrecheckOptions {
|
|
26
|
+
projectRoot?: string;
|
|
27
|
+
runId?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function collectAgents(params: {
|
|
23
31
|
agent?: string;
|
|
24
32
|
tasks?: SubagentTaskRef[];
|
|
@@ -40,7 +48,7 @@ function resolveAgent(
|
|
|
40
48
|
return agents.find((a) => a.name === name);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
export function precheckHarnessSubagentSpawn(
|
|
51
|
+
export async function precheckHarnessSubagentSpawn(
|
|
44
52
|
params: {
|
|
45
53
|
agent?: string;
|
|
46
54
|
tasks?: SubagentTaskRef[];
|
|
@@ -49,7 +57,8 @@ export function precheckHarnessSubagentSpawn(
|
|
|
49
57
|
},
|
|
50
58
|
agents: AgentConfig[],
|
|
51
59
|
phase: HarnessPhase,
|
|
52
|
-
|
|
60
|
+
opts?: PrecheckOptions,
|
|
61
|
+
): Promise<PrecheckResult> {
|
|
53
62
|
const names = collectAgents(params);
|
|
54
63
|
const mutating = names.filter((n) => {
|
|
55
64
|
const cfg = resolveAgent(agents, n);
|
|
@@ -67,7 +76,17 @@ export function precheckHarnessSubagentSpawn(
|
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
const parallelEvalAdversary =
|
|
80
|
+
(params.tasks?.length ?? 0) === 2 &&
|
|
81
|
+
params.tasks?.some((t) => t.agent === "harness/evaluator") &&
|
|
82
|
+
params.tasks?.some((t) => t.agent === "harness/adversary") &&
|
|
83
|
+
phase === "evaluate";
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
(params.tasks?.length ?? 0) > 1 &&
|
|
87
|
+
mutating.length > 1 &&
|
|
88
|
+
!parallelEvalAdversary
|
|
89
|
+
) {
|
|
71
90
|
return {
|
|
72
91
|
ok: false,
|
|
73
92
|
message:
|
|
@@ -76,12 +95,19 @@ export function precheckHarnessSubagentSpawn(
|
|
|
76
95
|
};
|
|
77
96
|
}
|
|
78
97
|
|
|
98
|
+
const parallelTaskCount = params.tasks?.length ?? (params.agent ? 1 : 0);
|
|
99
|
+
const topology = await validateHarnessSpawnTopology(names, phase, {
|
|
100
|
+
parallelTaskCount,
|
|
101
|
+
projectRoot: opts?.projectRoot,
|
|
102
|
+
runId: opts?.runId,
|
|
103
|
+
});
|
|
104
|
+
if (!topology.ok) {
|
|
105
|
+
return topology;
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
for (const name of names) {
|
|
80
109
|
if (!name.startsWith("harness/")) continue;
|
|
81
|
-
|
|
82
|
-
if (kind === "planner" && phase !== "plan") {
|
|
83
|
-
// allowed — planning agents can run in plan only ideally
|
|
84
|
-
}
|
|
110
|
+
classifyHarnessAgent(name);
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
return { ok: true };
|