ultimate-pi 0.18.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-decisions/SKILL.md +1 -1
- package/.agents/skills/harness-orchestration/SKILL.md +4 -4
- package/.agents/skills/harness-review/SKILL.md +7 -7
- package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
- package/.agents/skills/harness-steer/SKILL.md +1 -1
- package/.agents/skills/sentrux/SKILL.md +9 -9
- package/.pi/agents/harness/planning/decompose.md +1 -1
- package/.pi/extensions/00-harness-project-control.ts +133 -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 +33 -2
- package/.pi/extensions/harness-plan-approval.ts +2 -2
- package/.pi/extensions/harness-run-context.ts +180 -12
- package/.pi/extensions/harness-subagent-submit.ts +3 -2
- 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 +5 -15
- package/.pi/extensions/lib/harness-spawn-topology.ts +4 -27
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -2
- package/.pi/extensions/lib/harness-subagent-policy.ts +5 -5
- package/.pi/extensions/lib/harness-subagent-precheck.ts +3 -3
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +3 -21
- package/.pi/extensions/lib/plan-approval-readiness.ts +3 -52
- package/.pi/extensions/lib/spawn-policy.ts +3 -3
- package/.pi/extensions/observation-bus.ts +2 -0
- package/.pi/extensions/policy-gate.ts +2 -0
- 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 +23 -31
- package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
- package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
- package/.pi/harness/docs/adrs/README.md +1 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
- package/.pi/harness/docs/practice-map.md +2 -2
- package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
- package/.pi/lib/harness-project-config.ts +91 -0
- package/.pi/lib/harness-run-context.ts +1 -1
- package/.pi/lib/harness-ui-state.ts +27 -12
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-critic.md +1 -1
- package/.pi/prompts/harness-plan.md +3 -5
- package/.pi/prompts/harness-review.md +9 -9
- package/.pi/prompts/harness-run.md +7 -7
- package/.pi/prompts/harness-setup.md +5 -4
- package/.pi/prompts/harness-steer.md +2 -2
- package/.pi/scripts/README.md +1 -0
- package/.pi/scripts/graphify-kb-updater.mjs +48 -8
- 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/CHANGELOG.md +12 -0
- package/README.md +94 -58
- package/package.json +3 -3
- package/.pi/agents/harness/planning/scout-graphify.md +0 -39
- package/.pi/agents/harness/planning/scout-semantic.md +0 -41
- package/.pi/agents/harness/planning/scout-structure.md +0 -37
- /package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -0
- /package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -0
- /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
- /package/.pi/agents/harness/{executor.md → running/executor.md} +0 -0
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* in before_agent_start so trace-recorder reuses it on agent_start.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
mkdir,
|
|
10
|
+
readdir,
|
|
11
|
+
readFile,
|
|
12
|
+
rename,
|
|
13
|
+
stat,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
10
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
18
|
import { Type } from "@sinclair/typebox";
|
|
12
19
|
import {
|
|
@@ -65,7 +72,7 @@ import {
|
|
|
65
72
|
parseStructuredDocument,
|
|
66
73
|
writeYamlFile,
|
|
67
74
|
} from "../lib/harness-yaml.js";
|
|
68
|
-
import {
|
|
75
|
+
import { claimHarnessGovernanceLoad } from "./lib/extension-load-guard.js";
|
|
69
76
|
import {
|
|
70
77
|
evaluateHarnessSubagentToolCall,
|
|
71
78
|
isSubmitToolName,
|
|
@@ -96,6 +103,136 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
|
|
|
96
103
|
pi.events.emit("harness-run-context:updated", { run_id: ctx.run_id });
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
const PLAN_REVISION_ARTIFACT_FILES = new Set([
|
|
107
|
+
"planning-context.yaml",
|
|
108
|
+
"decomposition.yaml",
|
|
109
|
+
"hypothesis.yaml",
|
|
110
|
+
"implementation-research.yaml",
|
|
111
|
+
"stack.yaml",
|
|
112
|
+
"execution-plan-draft.yaml",
|
|
113
|
+
"plan-phase-status.yaml",
|
|
114
|
+
"plan-phase-waiver.yaml",
|
|
115
|
+
"sentrux-manifest-proposal.yaml",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const PLAN_REVISION_ARTIFACT_PREFIXES = [
|
|
119
|
+
"hypothesis-validation-r",
|
|
120
|
+
"review-round-r",
|
|
121
|
+
"plan-evaluator-r",
|
|
122
|
+
"plan-adversary-r",
|
|
123
|
+
"sprint-contract-audit-r",
|
|
124
|
+
"adversary-brief-r",
|
|
125
|
+
] as const;
|
|
126
|
+
|
|
127
|
+
async function moveIfExists(from: string, to: string): Promise<boolean> {
|
|
128
|
+
try {
|
|
129
|
+
await stat(from);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
await mkdir(dirname(to), { recursive: true });
|
|
134
|
+
await rename(from, to);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPlanRevisionArtifactFile(name: string): boolean {
|
|
139
|
+
if (PLAN_REVISION_ARTIFACT_FILES.has(name)) return true;
|
|
140
|
+
if (name === "review-round-consolidated.yaml") return true;
|
|
141
|
+
return PLAN_REVISION_ARTIFACT_PREFIXES.some((prefix) =>
|
|
142
|
+
name.startsWith(prefix),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function archivePlanRevisionArtifacts(input: {
|
|
147
|
+
projectRoot: string;
|
|
148
|
+
runId: string;
|
|
149
|
+
reason: string;
|
|
150
|
+
recordedAt?: string;
|
|
151
|
+
}): Promise<{ archiveDir: string; moved: string[] }> {
|
|
152
|
+
const recordedAt = input.recordedAt ?? nowIso();
|
|
153
|
+
const revisionId = recordedAt.replace(/[:.]/g, "-");
|
|
154
|
+
const runDir = join(input.projectRoot, ".pi", "harness", "runs", input.runId);
|
|
155
|
+
const artifactsDir = join(runDir, "artifacts");
|
|
156
|
+
const archiveDir = join(artifactsDir, "revisions", revisionId);
|
|
157
|
+
const moved: string[] = [];
|
|
158
|
+
|
|
159
|
+
async function archiveRel(rel: string): Promise<void> {
|
|
160
|
+
const ok = await moveIfExists(join(runDir, rel), join(archiveDir, rel));
|
|
161
|
+
if (ok) moved.push(rel);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await archiveRel("plan-packet.yaml");
|
|
165
|
+
await archiveRel("plan-review.md");
|
|
166
|
+
await archiveRel("research-brief.yaml");
|
|
167
|
+
await archiveRel("debate-messenger");
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const names = await readdir(artifactsDir);
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
if (!isPlanRevisionArtifactFile(name)) continue;
|
|
173
|
+
await archiveRel(join("artifacts", name));
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// No artifacts yet.
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const debateRel = join(
|
|
180
|
+
".pi",
|
|
181
|
+
"harness",
|
|
182
|
+
"debates",
|
|
183
|
+
`plan-${input.runId}.jsonl`,
|
|
184
|
+
);
|
|
185
|
+
const debateArchived = await moveIfExists(
|
|
186
|
+
join(input.projectRoot, debateRel),
|
|
187
|
+
join(archiveDir, "debates", basename(debateRel)),
|
|
188
|
+
);
|
|
189
|
+
if (debateArchived) moved.push(debateRel);
|
|
190
|
+
|
|
191
|
+
if (moved.length > 0) {
|
|
192
|
+
await mkdir(archiveDir, { recursive: true });
|
|
193
|
+
await writeFile(
|
|
194
|
+
join(archiveDir, "revision-reset.json"),
|
|
195
|
+
`${JSON.stringify(
|
|
196
|
+
{
|
|
197
|
+
schema_version: "1.0.0",
|
|
198
|
+
run_id: input.runId,
|
|
199
|
+
reason: input.reason,
|
|
200
|
+
recorded_at: recordedAt,
|
|
201
|
+
moved,
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
)}\n`,
|
|
206
|
+
"utf-8",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { archiveDir, moved };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function shouldArchiveForPlanRevise(input: {
|
|
214
|
+
command: string;
|
|
215
|
+
mode: "create" | "revise" | null;
|
|
216
|
+
runCtx: HarnessRunContext;
|
|
217
|
+
reviewOutcome: Awaited<ReturnType<typeof readReviewOutcomeFromRun>>;
|
|
218
|
+
userPrompt: string;
|
|
219
|
+
}): boolean {
|
|
220
|
+
if (input.command !== "harness-plan" && input.command !== "harness-auto") {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (input.mode !== "revise") return false;
|
|
224
|
+
const next = (input.runCtx.next_recommended_command ?? "").toLowerCase();
|
|
225
|
+
const prompt = input.userPrompt.toLowerCase();
|
|
226
|
+
return (
|
|
227
|
+
input.reviewOutcome?.remediation_class === "plan_gap" ||
|
|
228
|
+
next.includes("/harness-plan") ||
|
|
229
|
+
next.includes("revise") ||
|
|
230
|
+
prompt.includes("--mode revise") ||
|
|
231
|
+
prompt.includes("--mode=revise") ||
|
|
232
|
+
prompt.includes("mode: revise")
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
99
236
|
function syncPolicyFromRunContext(
|
|
100
237
|
pi: ExtensionAPI,
|
|
101
238
|
entries: unknown[],
|
|
@@ -236,10 +373,7 @@ async function offerCrossSessionResume(
|
|
|
236
373
|
hasUI: boolean;
|
|
237
374
|
sessionManager: { getEntries(): unknown[] };
|
|
238
375
|
ui: {
|
|
239
|
-
notify(
|
|
240
|
-
message: string,
|
|
241
|
-
type?: "info" | "warning" | "error",
|
|
242
|
-
): void;
|
|
376
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
243
377
|
};
|
|
244
378
|
},
|
|
245
379
|
): Promise<void> {
|
|
@@ -272,7 +406,7 @@ async function offerCrossSessionResume(
|
|
|
272
406
|
}
|
|
273
407
|
|
|
274
408
|
export default function harnessRunContext(pi: ExtensionAPI) {
|
|
275
|
-
if (!
|
|
409
|
+
if (!claimHarnessGovernanceLoad("harness-run-context", MODULE_URL)) return;
|
|
276
410
|
let activeCtx: HarnessRunContext | null = null;
|
|
277
411
|
|
|
278
412
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -658,12 +792,15 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
658
792
|
}
|
|
659
793
|
|
|
660
794
|
let activePlanBlock = "";
|
|
795
|
+
let planMode: "create" | "revise" | null = null;
|
|
661
796
|
if (command === "harness-plan" || command === "harness-auto") {
|
|
662
|
-
|
|
663
|
-
activeCtx.
|
|
797
|
+
planMode =
|
|
798
|
+
activeCtx.plan_id ||
|
|
799
|
+
activeCtx.plan_packet_path ||
|
|
800
|
+
activeCtx.status === "aborted"
|
|
664
801
|
? "revise"
|
|
665
802
|
: "create";
|
|
666
|
-
activePlanBlock = formatActivePlanBlock(activeCtx,
|
|
803
|
+
activePlanBlock = formatActivePlanBlock(activeCtx, planMode, planSummary);
|
|
667
804
|
} else if (command === "harness-run") {
|
|
668
805
|
activePlanBlock = formatActivePlanBlock(
|
|
669
806
|
activeCtx,
|
|
@@ -688,6 +825,37 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
688
825
|
activePlanBlock = formatActivePlanBlock(activeCtx, "read", planSummary);
|
|
689
826
|
}
|
|
690
827
|
|
|
828
|
+
if (command === "harness-plan" || command === "harness-auto") {
|
|
829
|
+
const reviewOutcome = await readReviewOutcomeFromRun(
|
|
830
|
+
activeCtx.run_id,
|
|
831
|
+
projectRoot,
|
|
832
|
+
);
|
|
833
|
+
if (
|
|
834
|
+
shouldArchiveForPlanRevise({
|
|
835
|
+
command,
|
|
836
|
+
mode: planMode,
|
|
837
|
+
runCtx: activeCtx,
|
|
838
|
+
reviewOutcome,
|
|
839
|
+
userPrompt,
|
|
840
|
+
})
|
|
841
|
+
) {
|
|
842
|
+
const reset = await archivePlanRevisionArtifacts({
|
|
843
|
+
projectRoot,
|
|
844
|
+
runId: activeCtx.run_id,
|
|
845
|
+
reason: "review_plan_gap_revise",
|
|
846
|
+
});
|
|
847
|
+
if (reset.moved.length > 0) {
|
|
848
|
+
pi.appendEntry("harness-plan-revision-reset", {
|
|
849
|
+
run_id: activeCtx.run_id,
|
|
850
|
+
archive_dir: reset.archiveDir,
|
|
851
|
+
moved: reset.moved,
|
|
852
|
+
reason: "review_plan_gap_revise",
|
|
853
|
+
recorded_at: nowIso(),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
691
859
|
persistContext(pi, activeCtx);
|
|
692
860
|
|
|
693
861
|
return {
|
|
@@ -1211,7 +1379,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
1211
1379
|
content: [
|
|
1212
1380
|
{
|
|
1213
1381
|
type: "text",
|
|
1214
|
-
text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/evaluator or harness/adversary subagents; parent gates with harness_artifact_ready only.`,
|
|
1382
|
+
text: `Path not allowed: ${pathArg}. Post-run verdicts must be written via submit_* in harness/reviewing/evaluator or harness/reviewing/adversary subagents; parent gates with harness_artifact_ready only.`,
|
|
1215
1383
|
},
|
|
1216
1384
|
],
|
|
1217
1385
|
details: { path: pathArg },
|
|
@@ -7,7 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "@sinclair/typebox";
|
|
9
9
|
import { resolveGuardedRunDir } from "../lib/harness-subagent-submit-path.js";
|
|
10
|
-
import {
|
|
10
|
+
import { claimHarnessGovernanceLoad } from "./lib/extension-load-guard.js";
|
|
11
11
|
import { getHarnessPackageRoot } from "./lib/harness-paths.js";
|
|
12
12
|
import { evaluateHarnessSubagentToolCall } from "./lib/harness-subagent-policy.js";
|
|
13
13
|
import {
|
|
@@ -60,7 +60,8 @@ function isSubprocessHarness(): boolean {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export default function harnessSubagentSubmit(pi: ExtensionAPI) {
|
|
63
|
-
if (!
|
|
63
|
+
if (!claimHarnessGovernanceLoad("harness-subagent-submit", MODULE_URL))
|
|
64
|
+
return;
|
|
64
65
|
// Option A: only load submit tools in subprocess (`-e` bundle), not parent discovery.
|
|
65
66
|
if (process.env.PI_HARNESS_SUBPROCESS !== "1") {
|
|
66
67
|
return;
|
|
@@ -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
|
+
}
|
|
@@ -20,9 +20,6 @@ const ARTIFACT_SCHEMA: Record<string, string> = {
|
|
|
20
20
|
"plan-implementation-research-brief.schema.json",
|
|
21
21
|
"artifacts/stack.yaml": "plan-stack-brief.schema.json",
|
|
22
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
23
|
"artifacts/eval-verdict.yaml": "eval-verdict.schema.json",
|
|
27
24
|
"artifacts/adversary-report.yaml": "adversary-report.schema.json",
|
|
28
25
|
};
|
|
@@ -48,10 +45,10 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
function
|
|
48
|
+
function artifactStatusBad(doc: Record<string, unknown>): string | null {
|
|
52
49
|
const status = String(doc.status ?? "ok").toLowerCase();
|
|
53
50
|
if (status === "partial" || status === "failed" || status === "error") {
|
|
54
|
-
return `
|
|
51
|
+
return `artifact status is "${status}"`;
|
|
55
52
|
}
|
|
56
53
|
return null;
|
|
57
54
|
}
|
|
@@ -105,17 +102,10 @@ export async function validateHarnessArtifactFile(
|
|
|
105
102
|
}
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
if (doc && normalized.startsWith("artifacts/scout-")) {
|
|
109
|
-
const scoutErr = scoutStatusBad(doc);
|
|
110
|
-
if (scoutErr) {
|
|
111
|
-
errors.push(`${normalized}: ${scoutErr}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
105
|
if (doc && normalized === "artifacts/planning-context.yaml") {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
errors.push(`${normalized}: ${
|
|
106
|
+
const statusErr = artifactStatusBad(doc);
|
|
107
|
+
if (statusErr) {
|
|
108
|
+
errors.push(`${normalized}: ${statusErr}`);
|
|
119
109
|
}
|
|
120
110
|
const coverage = doc.coverage as Record<string, unknown> | undefined;
|
|
121
111
|
if (coverage && typeof coverage === "object") {
|
|
@@ -23,13 +23,6 @@ const DEBATE_LANE_AGENTS = new Set([
|
|
|
23
23
|
"harness/planning/review-integrator",
|
|
24
24
|
]);
|
|
25
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
26
|
const PLANNING_CONTEXT_AGENT = "harness/planning/planning-context";
|
|
34
27
|
|
|
35
28
|
const PARALLEL_RESEARCH_AGENTS = new Set([
|
|
@@ -42,7 +35,7 @@ function countInSet(names: string[], allowed: Set<string>): number {
|
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
function isReconnaissanceAgent(name: string): boolean {
|
|
45
|
-
return
|
|
38
|
+
return name === PLANNING_CONTEXT_AGENT;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
async function decompositionReady(
|
|
@@ -103,28 +96,12 @@ export async function validateHarnessSpawnTopology(
|
|
|
103
96
|
};
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
const legacyScouts = countInSet(names, LEGACY_SCOUT_AGENTS);
|
|
107
99
|
const planningContext = names.filter(
|
|
108
100
|
(n) => n === PLANNING_CONTEXT_AGENT,
|
|
109
101
|
).length;
|
|
110
102
|
const research = countInSet(names, PARALLEL_RESEARCH_AGENTS);
|
|
111
|
-
const recon =
|
|
103
|
+
const recon = planningContext;
|
|
112
104
|
|
|
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
105
|
if (planningContext > 1) {
|
|
129
106
|
return {
|
|
130
107
|
ok: false,
|
|
@@ -149,7 +126,7 @@ export async function validateHarnessSpawnTopology(
|
|
|
149
126
|
ok: false,
|
|
150
127
|
message:
|
|
151
128
|
"Parallel batches may include only one independent group: " +
|
|
152
|
-
"research (≤2 lanes), optional
|
|
129
|
+
"research (≤2 lanes), optional single planning-context, " +
|
|
153
130
|
"or a single sequential lane agent.",
|
|
154
131
|
};
|
|
155
132
|
}
|
|
@@ -175,7 +152,7 @@ export async function validateHarnessSpawnTopology(
|
|
|
175
152
|
}
|
|
176
153
|
|
|
177
154
|
if (phase === "plan") {
|
|
178
|
-
const mutating = names.filter((n) => n.startsWith("harness/
|
|
155
|
+
const mutating = names.filter((n) => n.startsWith("harness/running/"));
|
|
179
156
|
if (mutating.length > 0) {
|
|
180
157
|
return {
|
|
181
158
|
ok: false,
|
|
@@ -52,8 +52,6 @@ const ROUTINE_PLANNING_AGENT_PATHS = new Set([
|
|
|
52
52
|
"harness/planning/hypothesis-validator",
|
|
53
53
|
"harness/planning/sprint-contract-auditor",
|
|
54
54
|
"harness/planning/planning-context",
|
|
55
|
-
"harness/planning/scout-structure",
|
|
56
|
-
"harness/planning/scout-semantic",
|
|
57
55
|
"harness/planning/decompose",
|
|
58
56
|
"harness/planning/hypothesis",
|
|
59
57
|
"harness/planning/stack-research",
|
|
@@ -66,13 +66,13 @@ export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
|
|
|
66
66
|
return "planner";
|
|
67
67
|
}
|
|
68
68
|
switch (id) {
|
|
69
|
-
case "executor":
|
|
69
|
+
case "running/executor":
|
|
70
70
|
return "executor";
|
|
71
|
-
case "evaluator":
|
|
71
|
+
case "reviewing/evaluator":
|
|
72
72
|
return "evaluator";
|
|
73
|
-
case "adversary":
|
|
73
|
+
case "reviewing/adversary":
|
|
74
74
|
return "adversary";
|
|
75
|
-
case "tie-breaker":
|
|
75
|
+
case "reviewing/tie-breaker":
|
|
76
76
|
return "tie_breaker";
|
|
77
77
|
case "meta-optimizer":
|
|
78
78
|
return "meta";
|
|
@@ -127,7 +127,7 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
127
127
|
return {
|
|
128
128
|
action: "block",
|
|
129
129
|
reason:
|
|
130
|
-
"submit_human_required is not available for harness/executor.",
|
|
130
|
+
"submit_human_required is not available for harness/running/executor.",
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
return { action: "allow" };
|
|
@@ -64,7 +64,7 @@ export async function precheckHarnessSubagentSpawn(
|
|
|
64
64
|
const cfg = resolveAgent(agents, n);
|
|
65
65
|
return cfg
|
|
66
66
|
? agentAllowsMutatingTools(cfg)
|
|
67
|
-
: n.startsWith("harness/
|
|
67
|
+
: n.startsWith("harness/running/");
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
if (phase === "plan" && mutating.length > 0) {
|
|
@@ -78,8 +78,8 @@ export async function precheckHarnessSubagentSpawn(
|
|
|
78
78
|
|
|
79
79
|
const parallelEvalAdversary =
|
|
80
80
|
(params.tasks?.length ?? 0) === 2 &&
|
|
81
|
-
params.tasks?.some((t) => t.agent === "harness/evaluator") &&
|
|
82
|
-
params.tasks?.some((t) => t.agent === "harness/adversary") &&
|
|
81
|
+
params.tasks?.some((t) => t.agent === "harness/reviewing/evaluator") &&
|
|
82
|
+
params.tasks?.some((t) => t.agent === "harness/reviewing/adversary") &&
|
|
83
83
|
phase === "evaluate";
|
|
84
84
|
|
|
85
85
|
if (
|
|
@@ -28,24 +28,6 @@ export const SUBMIT_TOOL_SPECS: readonly SubmitToolSpec[] = [
|
|
|
28
28
|
schemaFile: "plan-planning-context.schema.json",
|
|
29
29
|
artifactPath: "artifacts/planning-context.yaml",
|
|
30
30
|
},
|
|
31
|
-
{
|
|
32
|
-
toolName: "submit_scout_findings",
|
|
33
|
-
agents: [
|
|
34
|
-
"harness/planning/scout-graphify",
|
|
35
|
-
"harness/planning/scout-structure",
|
|
36
|
-
"harness/planning/scout-semantic",
|
|
37
|
-
],
|
|
38
|
-
schemaFile: "plan-scout-findings.schema.json",
|
|
39
|
-
artifactPath: (doc) => {
|
|
40
|
-
const lane =
|
|
41
|
-
typeof doc.lane === "string"
|
|
42
|
-
? doc.lane
|
|
43
|
-
: typeof doc.scout_lane === "string"
|
|
44
|
-
? doc.scout_lane
|
|
45
|
-
: "graphify";
|
|
46
|
-
return `artifacts/scout-${lane}.yaml`;
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
31
|
{
|
|
50
32
|
toolName: "submit_decomposition_brief",
|
|
51
33
|
agents: ["harness/planning/decompose", "harness/planning/plan-synthesizer"],
|
|
@@ -118,19 +100,19 @@ export const SUBMIT_TOOL_SPECS: readonly SubmitToolSpec[] = [
|
|
|
118
100
|
},
|
|
119
101
|
{
|
|
120
102
|
toolName: "submit_executor_handoff",
|
|
121
|
-
agents: ["harness/executor"],
|
|
103
|
+
agents: ["harness/running/executor"],
|
|
122
104
|
schemaFile: "harness-executor-handoff.schema.json",
|
|
123
105
|
artifactPath: "handoff/executor-summary.yaml",
|
|
124
106
|
},
|
|
125
107
|
{
|
|
126
108
|
toolName: "submit_eval_verdict",
|
|
127
|
-
agents: ["harness/evaluator"],
|
|
109
|
+
agents: ["harness/reviewing/evaluator"],
|
|
128
110
|
schemaFile: "eval-verdict.schema.json",
|
|
129
111
|
artifactPath: "artifacts/eval-verdict.yaml",
|
|
130
112
|
},
|
|
131
113
|
{
|
|
132
114
|
toolName: "submit_adversary_report",
|
|
133
|
-
agents: ["harness/adversary"],
|
|
115
|
+
agents: ["harness/reviewing/adversary"],
|
|
134
116
|
schemaFile: "adversary-report.schema.json",
|
|
135
117
|
artifactPath: "artifacts/adversary-report.yaml",
|
|
136
118
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pre-approve_plan readiness checks (
|
|
2
|
+
* Pre-approve_plan readiness checks (planning context, research, phase status).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { constants } from "node:fs";
|
|
@@ -13,12 +13,6 @@ export interface PlanApprovalReadiness {
|
|
|
13
13
|
warnings: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const LEGACY_SCOUT_ARTIFACTS = [
|
|
17
|
-
"artifacts/scout-graphify.yaml",
|
|
18
|
-
"artifacts/scout-structure.yaml",
|
|
19
|
-
"artifacts/scout-semantic.yaml",
|
|
20
|
-
] as const;
|
|
21
|
-
|
|
22
16
|
const PLANNING_CONTEXT_ARTIFACT = "artifacts/planning-context.yaml";
|
|
23
17
|
|
|
24
18
|
const PHASE35_ARTIFACTS = [
|
|
@@ -86,44 +80,6 @@ function coverageLaneStatus(
|
|
|
86
80
|
return String(laneDoc?.status ?? "").toLowerCase();
|
|
87
81
|
}
|
|
88
82
|
|
|
89
|
-
async function validateLegacyScouts(
|
|
90
|
-
runDir: string,
|
|
91
|
-
quick: boolean,
|
|
92
|
-
errors: string[],
|
|
93
|
-
warnings: string[],
|
|
94
|
-
): Promise<boolean> {
|
|
95
|
-
let anyPresent = false;
|
|
96
|
-
for (const rel of LEGACY_SCOUT_ARTIFACTS) {
|
|
97
|
-
if (rel === "artifacts/scout-semantic.yaml" && quick) continue;
|
|
98
|
-
const abs = join(runDir, rel);
|
|
99
|
-
if (!(await fileExists(abs))) {
|
|
100
|
-
const waived = await hasPhaseWaiver(runDir, `missing:${rel}`);
|
|
101
|
-
if (!waived) {
|
|
102
|
-
errors.push(`missing ${rel}`);
|
|
103
|
-
}
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
anyPresent = true;
|
|
107
|
-
const doc = await readYamlObject(abs);
|
|
108
|
-
const bad = artifactStatusBad(doc, rel);
|
|
109
|
-
if (bad) {
|
|
110
|
-
const waived = await hasPhaseWaiver(
|
|
111
|
-
runDir,
|
|
112
|
-
`scout:${rel}:${String(doc?.status ?? "")}`,
|
|
113
|
-
);
|
|
114
|
-
if (!waived) {
|
|
115
|
-
errors.push(bad);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (anyPresent) {
|
|
120
|
-
warnings.push(
|
|
121
|
-
"legacy scout YAML artifacts detected — prefer artifacts/planning-context.yaml (see ADR 0041)",
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
return anyPresent;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
83
|
async function validatePlanningContext(
|
|
128
84
|
runDir: string,
|
|
129
85
|
quick: boolean,
|
|
@@ -203,19 +159,14 @@ export async function validatePlanApprovalReadiness(
|
|
|
203
159
|
quick,
|
|
204
160
|
errors,
|
|
205
161
|
);
|
|
206
|
-
const hasLegacyScouts = hasPlanningContext
|
|
207
|
-
? false
|
|
208
|
-
: await validateLegacyScouts(runDir, quick, errors, warnings);
|
|
209
162
|
|
|
210
|
-
if (!hasPlanningContext
|
|
163
|
+
if (!hasPlanningContext) {
|
|
211
164
|
const waived = await hasPhaseWaiver(
|
|
212
165
|
runDir,
|
|
213
166
|
"missing:planning-reconnaissance",
|
|
214
167
|
);
|
|
215
168
|
if (!waived) {
|
|
216
|
-
errors.push(
|
|
217
|
-
`missing ${PLANNING_CONTEXT_ARTIFACT} (or legacy scout-graphify/structure/semantic trio)`,
|
|
218
|
-
);
|
|
169
|
+
errors.push(`missing ${PLANNING_CONTEXT_ARTIFACT}`);
|
|
219
170
|
}
|
|
220
171
|
}
|
|
221
172
|
|
|
@@ -11,9 +11,9 @@ export const SUBAGENT_BLOCKED_TOOLS = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
const ASK_USER_ALLOWED_AGENT_TYPES = new Set([
|
|
14
|
-
"harness/evaluator",
|
|
15
|
-
"harness/adversary",
|
|
16
|
-
"harness/tie-breaker",
|
|
14
|
+
"harness/reviewing/evaluator",
|
|
15
|
+
"harness/reviewing/adversary",
|
|
16
|
+
"harness/reviewing/tie-breaker",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
19
|
export interface ToolCallDecision {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { isHarnessProjectEnabled } from "../lib/harness-project-config.js";
|
|
10
11
|
import { getRunIdFromSession } from "../lib/harness-run-context.js";
|
|
11
12
|
|
|
12
13
|
type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
|
|
@@ -87,6 +88,7 @@ function getRunId(ctx: {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export default function observationBus(pi: ExtensionAPI) {
|
|
91
|
+
if (!isHarnessProjectEnabled()) return;
|
|
90
92
|
const seen = new Set<string>();
|
|
91
93
|
|
|
92
94
|
pi.on("agent_end", async (_event, ctx) => {
|