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.
Files changed (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  4. package/.agents/skills/harness-eval/SKILL.md +6 -21
  5. package/.agents/skills/harness-governor/SKILL.md +4 -3
  6. package/.agents/skills/harness-orchestration/SKILL.md +41 -53
  7. package/.agents/skills/harness-plan/SKILL.md +23 -12
  8. package/.agents/skills/harness-review/SKILL.md +52 -0
  9. package/.agents/skills/harness-sentrux-setup/SKILL.md +16 -3
  10. package/.agents/skills/harness-steer/SKILL.md +14 -0
  11. package/.agents/skills/sentrux/SKILL.md +9 -9
  12. package/.pi/agents/harness/planning/decompose.md +7 -4
  13. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  14. package/.pi/agents/harness/planning/hypothesis.md +3 -1
  15. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  16. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  17. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  18. package/.pi/agents/harness/planning/planning-context.md +48 -0
  19. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  21. package/.pi/agents/harness/{adversary.md → reviewing/adversary.md} +3 -10
  22. package/.pi/agents/harness/{evaluator.md → reviewing/evaluator.md} +3 -12
  23. package/.pi/agents/harness/running/executor.md +45 -0
  24. package/.pi/agents/harness/sentrux-steward.md +51 -0
  25. package/.pi/extensions/00-harness-project-control.ts +133 -0
  26. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  27. package/.pi/extensions/budget-guard.ts +2 -0
  28. package/.pi/extensions/debate-orchestrator.ts +2 -0
  29. package/.pi/extensions/harness-ask-user.ts +2 -2
  30. package/.pi/extensions/harness-debate-tools.ts +2 -2
  31. package/.pi/extensions/harness-live-widget.ts +60 -3
  32. package/.pi/extensions/harness-plan-approval.ts +64 -58
  33. package/.pi/extensions/harness-run-context.ts +715 -90
  34. package/.pi/extensions/harness-subagent-submit.ts +46 -12
  35. package/.pi/extensions/harness-subagents.ts +2 -2
  36. package/.pi/extensions/harness-telemetry.ts +2 -0
  37. package/.pi/extensions/harness-web-tools.ts +2 -2
  38. package/.pi/extensions/lib/extension-load-guard.ts +10 -0
  39. package/.pi/extensions/lib/harness-artifact-gate.ts +172 -0
  40. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  41. package/.pi/extensions/lib/harness-spawn-topology.ts +165 -0
  42. package/.pi/extensions/lib/harness-subagent-auth.ts +1 -2
  43. package/.pi/extensions/lib/harness-subagent-policy.ts +28 -24
  44. package/.pi/extensions/lib/harness-subagent-precheck.ts +36 -10
  45. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  46. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +22 -22
  47. package/.pi/extensions/lib/harness-subagents-bridge.ts +7 -29
  48. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  49. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  50. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  51. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  52. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  53. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  54. package/.pi/extensions/lib/plan-approval-readiness.ts +192 -0
  55. package/.pi/extensions/lib/plan-debate-eligibility.ts +12 -5
  56. package/.pi/extensions/lib/plan-debate-gate.ts +22 -1
  57. package/.pi/extensions/lib/plan-debate-lanes.ts +32 -2
  58. package/.pi/extensions/lib/plan-review-gate.ts +8 -0
  59. package/.pi/extensions/lib/posthog-client.ts +76 -0
  60. package/.pi/extensions/lib/spawn-policy.ts +3 -3
  61. package/.pi/extensions/observation-bus.ts +2 -0
  62. package/.pi/extensions/policy-gate.ts +26 -19
  63. package/.pi/extensions/review-integrity.ts +91 -10
  64. package/.pi/extensions/sentrux-rules-sync.ts +2 -0
  65. package/.pi/extensions/test-diff-integrity.ts +1 -0
  66. package/.pi/extensions/trace-recorder.ts +2 -0
  67. package/.pi/harness/agents.manifest.json +37 -37
  68. package/.pi/harness/corpus/cron.example +8 -0
  69. package/.pi/harness/corpus/graphify-kb-updater.config.json +214 -0
  70. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  71. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  72. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  73. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  74. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +8 -6
  75. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  76. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  77. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  78. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  79. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  80. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  81. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  82. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  83. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  84. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  85. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +37 -0
  86. package/.pi/harness/docs/adrs/0045-phase-scoped-agent-directories.md +33 -0
  87. package/.pi/harness/docs/adrs/README.md +11 -0
  88. package/.pi/harness/docs/graphify-kb-updater-runbook.md +163 -0
  89. package/.pi/harness/docs/practice-map.md +110 -0
  90. package/.pi/harness/env.harness.template +5 -3
  91. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  92. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +5 -2
  93. package/.pi/harness/specs/README.md +1 -1
  94. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  95. package/.pi/harness/specs/harness-spawn-context.schema.json +15 -1
  96. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  97. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  98. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  99. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  100. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  101. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  102. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  103. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  104. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  105. package/.pi/harness/specs/steer-state.schema.json +20 -0
  106. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  107. package/.pi/lib/harness-project-config.ts +91 -0
  108. package/.pi/lib/harness-repair-brief.ts +145 -0
  109. package/.pi/lib/harness-run-context.ts +591 -32
  110. package/.pi/lib/harness-ui-state.ts +114 -21
  111. package/.pi/prompts/harness-auto.md +10 -10
  112. package/.pi/prompts/harness-critic.md +3 -30
  113. package/.pi/prompts/harness-eval.md +4 -37
  114. package/.pi/prompts/harness-plan.md +116 -54
  115. package/.pi/prompts/harness-review.md +150 -15
  116. package/.pi/prompts/harness-run.md +62 -10
  117. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  118. package/.pi/prompts/harness-setup.md +5 -4
  119. package/.pi/prompts/harness-steer.md +30 -0
  120. package/.pi/scripts/README.md +1 -0
  121. package/.pi/scripts/graphify-kb-updater.mjs +398 -0
  122. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  123. package/.pi/scripts/harness-project-toggle.mjs +129 -0
  124. package/.pi/scripts/harness-sentrux-cli.mjs +142 -0
  125. package/.pi/scripts/harness-verify.mjs +22 -6
  126. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  127. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  128. package/AGENTS.md +1 -0
  129. package/CHANGELOG.md +23 -0
  130. package/README.md +94 -58
  131. package/package.json +5 -4
  132. package/.pi/agents/harness/executor.md +0 -47
  133. package/.pi/agents/harness/planning/scout-graphify.md +0 -37
  134. package/.pi/agents/harness/planning/scout-semantic.md +0 -39
  135. package/.pi/agents/harness/planning/scout-structure.md +0 -35
  136. package/.pi/prompts/git-sync.md +0 -124
  137. /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 { claimExtensionLoad } from "./lib/extension-load-guard.js";
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 { executeSubmitPipeline } from "./lib/harness-subagent-submit-pipeline.js";
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.Record(Type.String(), Type.Unknown(), {
21
- description:
22
- "Plan artifact fields (validated via plan-*.schema.json, persisted as canonical YAML on disk)",
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 (!claimExtensionLoad("harness-subagent-submit", MODULE_URL)) return;
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 document = (params as { document?: Record<string, unknown> })
122
- .document;
123
- if (!document || typeof document !== "object") {
134
+ const runResolved = await resolveGuardedRunDir({
135
+ projectRoot,
136
+ runId,
137
+ runDirEnv,
138
+ });
139
+ if (!runResolved.ok) {
124
140
  return {
125
- content: [{ type: "text", text: "document object is required" }],
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 { 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
+ }
@@ -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
- await client.shutdown();
113
- client = null;
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/scout-structure",
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