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.
Files changed (68) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  2. package/.agents/skills/harness-orchestration/SKILL.md +4 -4
  3. package/.agents/skills/harness-review/SKILL.md +7 -7
  4. package/.agents/skills/harness-sentrux-setup/SKILL.md +4 -3
  5. package/.agents/skills/harness-steer/SKILL.md +1 -1
  6. package/.agents/skills/sentrux/SKILL.md +9 -9
  7. package/.pi/agents/harness/planning/decompose.md +1 -1
  8. package/.pi/extensions/00-harness-project-control.ts +133 -0
  9. package/.pi/extensions/budget-guard.ts +2 -0
  10. package/.pi/extensions/debate-orchestrator.ts +2 -0
  11. package/.pi/extensions/harness-ask-user.ts +2 -2
  12. package/.pi/extensions/harness-debate-tools.ts +2 -2
  13. package/.pi/extensions/harness-live-widget.ts +33 -2
  14. package/.pi/extensions/harness-plan-approval.ts +2 -2
  15. package/.pi/extensions/harness-run-context.ts +180 -12
  16. package/.pi/extensions/harness-subagent-submit.ts +3 -2
  17. package/.pi/extensions/harness-subagents.ts +2 -2
  18. package/.pi/extensions/harness-telemetry.ts +2 -0
  19. package/.pi/extensions/harness-web-tools.ts +2 -2
  20. package/.pi/extensions/lib/extension-load-guard.ts +10 -0
  21. package/.pi/extensions/lib/harness-artifact-gate.ts +5 -15
  22. package/.pi/extensions/lib/harness-spawn-topology.ts +4 -27
  23. package/.pi/extensions/lib/harness-subagent-auth.ts +0 -2
  24. package/.pi/extensions/lib/harness-subagent-policy.ts +5 -5
  25. package/.pi/extensions/lib/harness-subagent-precheck.ts +3 -3
  26. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +3 -21
  27. package/.pi/extensions/lib/plan-approval-readiness.ts +3 -52
  28. package/.pi/extensions/lib/spawn-policy.ts +3 -3
  29. package/.pi/extensions/observation-bus.ts +2 -0
  30. package/.pi/extensions/policy-gate.ts +2 -0
  31. package/.pi/extensions/review-integrity.ts +91 -10
  32. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  33. package/.pi/extensions/test-diff-integrity.ts +1 -0
  34. package/.pi/extensions/trace-recorder.ts +2 -0
  35. package/.pi/harness/agents.manifest.json +23 -31
  36. package/.pi/harness/corpus/graphify-kb-updater.config.json +55 -0
  37. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +2 -1
  38. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +3 -2
  39. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  40. package/.pi/harness/docs/adrs/README.md +1 -0
  41. package/.pi/harness/docs/graphify-kb-updater-runbook.md +11 -5
  42. package/.pi/harness/docs/practice-map.md +2 -2
  43. package/.pi/harness/specs/harness-spawn-context.schema.json +1 -1
  44. package/.pi/lib/harness-project-config.ts +91 -0
  45. package/.pi/lib/harness-run-context.ts +1 -1
  46. package/.pi/lib/harness-ui-state.ts +27 -12
  47. package/.pi/prompts/harness-auto.md +2 -2
  48. package/.pi/prompts/harness-critic.md +1 -1
  49. package/.pi/prompts/harness-plan.md +3 -5
  50. package/.pi/prompts/harness-review.md +9 -9
  51. package/.pi/prompts/harness-run.md +7 -7
  52. package/.pi/prompts/harness-setup.md +5 -4
  53. package/.pi/prompts/harness-steer.md +2 -2
  54. package/.pi/scripts/README.md +1 -0
  55. package/.pi/scripts/graphify-kb-updater.mjs +48 -8
  56. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  57. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  58. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  59. package/CHANGELOG.md +12 -0
  60. package/README.md +94 -58
  61. package/package.json +3 -3
  62. package/.pi/agents/harness/planning/scout-graphify.md +0 -39
  63. package/.pi/agents/harness/planning/scout-semantic.md +0 -41
  64. package/.pi/agents/harness/planning/scout-structure.md +0 -37
  65. /package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +0 -0
  66. /package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +0 -0
  67. /package/.pi/agents/harness/{tie-breaker.md → reviewing/tie-breaker.md} +0 -0
  68. /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 { mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { dirname, join } from "node:path";
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 { claimExtensionLoad } from "./lib/extension-load-guard.js";
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 (!claimExtensionLoad("harness-run-context", MODULE_URL)) return;
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
- const mode =
663
- activeCtx.plan_ready || activeCtx.status === "aborted"
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, mode, planSummary);
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 { claimExtensionLoad } from "./lib/extension-load-guard.js";
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 (!claimExtensionLoad("harness-subagent-submit", MODULE_URL)) return;
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 { claimExtensionLoad } from "./lib/extension-load-guard.js";
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 (!claimExtensionLoad("harness-subagents", MODULE_URL)) {
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 { claimExtensionLoad } from "./lib/extension-load-guard.js";
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 (!claimExtensionLoad("harness-web-tools", MODULE_URL)) return;
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 scoutStatusBad(doc: Record<string, unknown>): string | null {
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 `scout status is "${status}"`;
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 scoutErr = scoutStatusBad(doc);
117
- if (scoutErr) {
118
- errors.push(`${normalized}: ${scoutErr}`);
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 LEGACY_SCOUT_AGENTS.has(name) || name === PLANNING_CONTEXT_AGENT;
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 = legacyScouts + planningContext;
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 legacy scouts (≤3), optional single planning-context, " +
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/executor"));
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/executor");
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 (artifacts, scouts, phase status).
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 && !hasLegacyScouts) {
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) => {