ultimate-pi 0.10.1 → 0.12.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.
Files changed (135) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
  2. package/.agents/skills/harness-decisions/SKILL.md +3 -3
  3. package/.agents/skills/harness-orchestration/SKILL.md +59 -25
  4. package/.agents/skills/harness-plan/SKILL.md +16 -15
  5. package/.pi/agents/harness/adversary.md +0 -1
  6. package/.pi/agents/harness/evaluator.md +0 -1
  7. package/.pi/agents/harness/executor.md +1 -2
  8. package/.pi/agents/harness/incident-recorder.md +0 -1
  9. package/.pi/agents/harness/meta-optimizer.md +0 -1
  10. package/.pi/agents/harness/planning/decompose.md +83 -0
  11. package/.pi/agents/harness/planning/execution-plan-author.md +30 -0
  12. package/.pi/agents/harness/planning/hypothesis-validator.md +23 -0
  13. package/.pi/agents/harness/planning/hypothesis.md +89 -0
  14. package/.pi/agents/harness/planning/plan-adversary.md +18 -0
  15. package/.pi/agents/harness/planning/plan-evaluator.md +18 -0
  16. package/.pi/agents/harness/planning/review-integrator.md +23 -0
  17. package/.pi/agents/harness/planning/scout-graphify.md +54 -0
  18. package/.pi/agents/harness/planning/scout-semantic.md +47 -0
  19. package/.pi/agents/harness/planning/scout-structure.md +50 -0
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +18 -0
  21. package/.pi/agents/harness/planning/stack-researcher.md +24 -0
  22. package/.pi/agents/harness/tie-breaker.md +0 -1
  23. package/.pi/agents/harness/trace-librarian.md +0 -1
  24. package/.pi/extensions/debate-orchestrator.ts +90 -53
  25. package/.pi/extensions/harness-ask-user.ts +5 -0
  26. package/.pi/extensions/harness-plan-approval.ts +137 -3
  27. package/.pi/extensions/harness-run-context.ts +146 -6
  28. package/.pi/extensions/harness-subagents.ts +10 -5
  29. package/.pi/extensions/harness-web-tools.ts +2 -0
  30. package/.pi/extensions/lib/extension-load-guard.ts +39 -0
  31. package/.pi/extensions/lib/harness-posthog.ts +6 -1
  32. package/.pi/extensions/lib/harness-spawn-budget.ts +75 -0
  33. package/.pi/extensions/lib/harness-subagent-auth.ts +123 -0
  34. package/.pi/extensions/lib/{harness-subagents/harness-subagent-policy.ts → harness-subagent-policy.ts} +34 -9
  35. package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
  36. package/.pi/extensions/lib/harness-subagents-bridge.ts +176 -0
  37. package/.pi/extensions/lib/plan-approval/create-plan.ts +9 -7
  38. package/.pi/extensions/lib/plan-approval/plan-review.ts +393 -0
  39. package/.pi/extensions/lib/plan-approval/schema.ts +16 -1
  40. package/.pi/extensions/lib/plan-approval/types.ts +16 -0
  41. package/.pi/extensions/lib/plan-approval/validate.ts +2 -0
  42. package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
  43. package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +2 -5
  44. package/.pi/extensions/policy-gate.ts +1 -1
  45. package/.pi/extensions/review-integrity.ts +48 -29
  46. package/.pi/extensions/ultimate-pi-vcc.ts +5 -0
  47. package/.pi/harness/agents.manifest.json +126 -82
  48. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -6
  49. package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +34 -0
  50. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +41 -0
  51. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -0
  52. package/.pi/harness/docs/adrs/README.md +2 -0
  53. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r1.yaml +25 -0
  54. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r4.yaml +26 -0
  55. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/sprint-audit-r4.yaml +5 -0
  56. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-packet.yaml +196 -0
  57. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-review.md +14 -0
  58. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +32 -0
  59. package/.pi/harness/evals/smoke/run-context.fixture.json +1 -1
  60. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +88 -0
  61. package/.pi/harness/specs/README.md +1 -1
  62. package/.pi/harness/specs/harness-posthog-event.schema.json +6 -1
  63. package/.pi/harness/specs/harness-spawn-context.schema.json +2 -1
  64. package/.pi/harness/specs/plan-adversary-brief.schema.json +45 -0
  65. package/.pi/harness/specs/plan-decomposition-brief.schema.json +108 -0
  66. package/.pi/harness/specs/plan-execution-plan-brief.schema.json +13 -0
  67. package/.pi/harness/specs/plan-execution-plan.schema.json +255 -0
  68. package/.pi/harness/specs/plan-hypothesis-brief.schema.json +96 -0
  69. package/.pi/harness/specs/plan-hypothesis-eval.schema.json +61 -0
  70. package/.pi/harness/specs/plan-packet.schema.json +14 -5
  71. package/.pi/harness/specs/plan-review-round-draft.schema.json +68 -0
  72. package/.pi/harness/specs/plan-sprint-audit-turn.schema.json +29 -0
  73. package/.pi/harness/specs/plan-stack-brief.schema.json +65 -0
  74. package/.pi/harness/specs/plan-validation-turn.schema.json +42 -0
  75. package/.pi/harness/specs/round-result.schema.json +16 -9
  76. package/.pi/lib/debate-orchestrator-types.ts +38 -0
  77. package/.pi/lib/harness-agent-discovery.mjs +81 -0
  78. package/.pi/lib/harness-run-context.ts +76 -38
  79. package/.pi/lib/harness-yaml.mjs +73 -0
  80. package/.pi/lib/harness-yaml.ts +90 -0
  81. package/.pi/prompts/harness-auto.md +13 -11
  82. package/.pi/prompts/harness-critic.md +2 -2
  83. package/.pi/prompts/harness-eval.md +3 -3
  84. package/.pi/prompts/harness-incident.md +2 -2
  85. package/.pi/prompts/harness-plan.md +106 -37
  86. package/.pi/prompts/harness-review.md +2 -2
  87. package/.pi/prompts/harness-router-tune.md +1 -1
  88. package/.pi/prompts/harness-run.md +2 -2
  89. package/.pi/prompts/harness-setup.md +15 -6
  90. package/.pi/prompts/harness-trace.md +2 -2
  91. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  92. package/.pi/scripts/harness-resolve-up-pkg.mjs +13 -0
  93. package/.pi/scripts/harness-verify.mjs +28 -19
  94. package/.pi/scripts/validate-plan-dag.mjs +258 -0
  95. package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
  96. package/CHANGELOG.md +24 -0
  97. package/THIRD_PARTY_NOTICES.md +8 -0
  98. package/biome.json +4 -1
  99. package/package.json +6 -4
  100. package/.pi/agents/harness/planner.md +0 -54
  101. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +0 -126
  102. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +0 -119
  103. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +0 -87
  104. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +0 -118
  105. package/.pi/extensions/lib/harness-subagents/blackboard.ts +0 -175
  106. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +0 -10
  107. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +0 -310
  108. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -59
  109. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +0 -27
  110. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +0 -558
  111. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -684
  112. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +0 -175
  113. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +0 -59
  114. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +0 -134
  115. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +0 -5
  116. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +0 -123
  117. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +0 -43
  118. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +0 -144
  119. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +0 -2494
  120. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +0 -52
  121. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +0 -182
  122. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +0 -92
  123. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +0 -115
  124. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +0 -103
  125. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +0 -177
  126. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +0 -416
  127. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +0 -210
  128. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +0 -108
  129. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +0 -187
  130. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +0 -639
  131. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +0 -324
  132. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +0 -110
  133. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +0 -71
  134. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +0 -195
  135. /package/.pi/extensions/{00-ultimate-pi-system-prompt.ts → custom-system-prompt.ts} +0 -0
@@ -5,13 +5,16 @@
5
5
  * in before_agent_start so trace-recorder reuses it on agent_start.
6
6
  */
7
7
 
8
- import { readFile, writeFile } from "node:fs/promises";
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { dirname } from "node:path";
9
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { Type } from "@sinclair/typebox";
10
12
  import {
11
13
  canonicalPlanPath,
12
14
  createFreshRunContext,
13
15
  driftGateActive,
14
16
  extractCompletionStatuses,
17
+ extractWritePathFromToolInput,
15
18
  formatActivePlanBlock,
16
19
  formatPlanContextBlock,
17
20
  getLatestHarnessTurn,
@@ -27,10 +30,12 @@ import {
27
30
  isHarnessBootstrapPrompt,
28
31
  isNewTaskPlanBlocked,
29
32
  isPlanApprovalAskUser,
33
+ isPlanPhaseScopedWrite,
30
34
  isStaleActiveRunPointer,
31
35
  loadProjectActiveRun,
32
36
  loadRunContextFromDisk,
33
37
  nextStepAfterOutcome,
38
+ normalizeHarnessPath,
34
39
  nowIso,
35
40
  type PlanPacketSummary,
36
41
  parseHarnessSlashInput,
@@ -45,6 +50,11 @@ import {
45
50
  validatePlanOverridePath,
46
51
  validatePlanPacket,
47
52
  } from "../lib/harness-run-context.js";
53
+ import {
54
+ normalizeHarnessYamlContent,
55
+ parseStructuredDocument,
56
+ writeYamlFile,
57
+ } from "../lib/harness-yaml.js";
48
58
 
49
59
  interface SessionEntryLike {
50
60
  type?: string;
@@ -84,6 +94,32 @@ function appendHarnessTurn(pi: ExtensionAPI, turn: HarnessTurnEntry): void {
84
94
  });
85
95
  }
86
96
 
97
+ async function coerceScopedHarnessYamlWrite(
98
+ event: { toolName: string; input: Record<string, unknown> },
99
+ runCtx: HarnessRunContext,
100
+ projectRoot: string,
101
+ ): Promise<{ block: true; reason: string } | undefined> {
102
+ if (event.toolName !== "write") return undefined;
103
+ const target = extractWritePathFromToolInput(event.input);
104
+ if (!target.endsWith(".yaml") && !target.endsWith(".yml")) return undefined;
105
+ const scoped = await isPlanPhaseScopedWrite(target, runCtx, projectRoot);
106
+ if (!scoped) return undefined;
107
+ const content = event.input.content;
108
+ if (typeof content !== "string") return undefined;
109
+ try {
110
+ event.input.content = normalizeHarnessYamlContent(content, target);
111
+ } catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ return {
114
+ block: true,
115
+ reason:
116
+ `harness-run-context: ${target} must be canonical YAML, not embedded JSON. ` +
117
+ `Use write_harness_yaml with the subagent JSON/YAML block, or paste valid YAML. (${msg})`,
118
+ };
119
+ }
120
+ return undefined;
121
+ }
122
+
87
123
  function syncPolicyFromPlan(
88
124
  pi: ExtensionAPI,
89
125
  entries: unknown[],
@@ -583,7 +619,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
583
619
  activeCtx.last_outcome = "needs_clarification";
584
620
  activeCtx.last_completed_step = "plan";
585
621
  const msg =
586
- "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.json.";
622
+ "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.yaml.";
587
623
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
588
624
  else
589
625
  pi.sendMessage({
@@ -671,6 +707,18 @@ export default function harnessRunContext(pi: ExtensionAPI) {
671
707
  });
672
708
 
673
709
  pi.on("tool_call", async (event, ctx) => {
710
+ if (event.toolName === "write") {
711
+ const entries = getEntries(ctx);
712
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
713
+ if (runCtx) {
714
+ const blocked = await coerceScopedHarnessYamlWrite(
715
+ event,
716
+ runCtx,
717
+ process.cwd(),
718
+ );
719
+ if (blocked) return blocked;
720
+ }
721
+ }
674
722
  if (activeCtx?.plan_packet_path) {
675
723
  const entries = getEntries(ctx);
676
724
  if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
@@ -707,11 +755,11 @@ export default function harnessRunContext(pi: ExtensionAPI) {
707
755
  (event.input as { filePath?: string }).filePath ??
708
756
  "",
709
757
  );
710
- if (target.includes("plan-packet.json")) {
758
+ if (target.includes("plan-packet.yaml")) {
711
759
  return {
712
760
  block: true,
713
761
  reason:
714
- "harness-run-context: plan-packet.json is read-only in evaluate/adversary phases.",
762
+ "harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
715
763
  };
716
764
  }
717
765
  return undefined;
@@ -792,7 +840,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
792
840
 
793
841
  pi.registerCommand("harness-plan-commit", {
794
842
  description:
795
- "Write approved plan-packet.json to the active run (requires harness-plan-approval)",
843
+ "Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
796
844
  handler: async (args, ctx) => {
797
845
  const projectRoot = process.cwd();
798
846
  const entries = getEntries(ctx);
@@ -816,7 +864,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
816
864
  })
817
865
  ) {
818
866
  const msg =
819
- "Plan commit blocked: no user approval recorded. Approve via ask_user in the planner subagent first.";
867
+ "Plan commit blocked: no user approval recorded. Approve via approve_plan in this session first.";
820
868
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
821
869
  return;
822
870
  }
@@ -867,6 +915,98 @@ export default function harnessRunContext(pi: ExtensionAPI) {
867
915
  },
868
916
  });
869
917
 
918
+ pi.registerTool({
919
+ name: "write_harness_yaml",
920
+ label: "Write Harness YAML",
921
+ description:
922
+ "Write a plan-phase harness artifact as canonical YAML (parses subagent JSON or YAML, never embeds JSON in .yaml files).",
923
+ promptSnippet:
924
+ "Persist plan artifacts (decomposition, hypothesis, stack, review rounds) as real YAML.",
925
+ promptGuidelines: [
926
+ "Use write_harness_yaml for all artifacts/*.yaml and research-brief.yaml updates during /harness-plan.",
927
+ "Pass the subagent fenced json or yaml block as content; the tool converts to YAML on disk.",
928
+ "Do not use write with stringified JSON for .yaml paths.",
929
+ "plan-packet.yaml after approval: prefer create_plan; write_harness_yaml is for drafts and side artifacts only.",
930
+ ],
931
+ parameters: Type.Object({
932
+ path: Type.String({
933
+ description:
934
+ "Path under the active run, e.g. artifacts/decomposition.yaml or research-brief.yaml",
935
+ }),
936
+ content: Type.String({
937
+ description:
938
+ "YAML or JSON document (fenced or raw) matching the artifact schema",
939
+ }),
940
+ }),
941
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
942
+ const entries = getEntries(ctx);
943
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
944
+ if (!runCtx?.run_id) {
945
+ return {
946
+ content: [
947
+ {
948
+ type: "text",
949
+ text: 'No active harness run. Run /harness-plan "<task>" first.',
950
+ },
951
+ ],
952
+ details: {},
953
+ isError: true,
954
+ };
955
+ }
956
+ const pathArg = String((params as { path?: string }).path ?? "").trim();
957
+ const content = String((params as { content?: string }).content ?? "");
958
+ if (!pathArg || !content.trim()) {
959
+ return {
960
+ content: [
961
+ {
962
+ type: "text",
963
+ text: "write_harness_yaml requires path and content.",
964
+ },
965
+ ],
966
+ details: {},
967
+ isError: true,
968
+ };
969
+ }
970
+ const projectRoot = process.cwd();
971
+ const absPath = normalizeHarnessPath(pathArg, projectRoot);
972
+ const scoped = await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot);
973
+ if (!scoped) {
974
+ return {
975
+ content: [
976
+ {
977
+ type: "text",
978
+ text: `Path not allowed: ${pathArg}. Must be under .pi/harness/runs/${runCtx.run_id}/ (artifacts/*.yaml, research-brief.yaml, etc.).`,
979
+ },
980
+ ],
981
+ details: { path: pathArg },
982
+ isError: true,
983
+ };
984
+ }
985
+ let doc: unknown;
986
+ try {
987
+ doc = parseStructuredDocument(content, pathArg);
988
+ } catch (err) {
989
+ const msg = err instanceof Error ? err.message : String(err);
990
+ return {
991
+ content: [{ type: "text", text: msg }],
992
+ details: { path: pathArg },
993
+ isError: true,
994
+ };
995
+ }
996
+ await mkdir(dirname(absPath), { recursive: true });
997
+ await writeYamlFile(absPath, doc);
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: `Wrote ${pathArg} as canonical YAML.`,
1003
+ },
1004
+ ],
1005
+ details: { path: absPath },
1006
+ };
1007
+ },
1008
+ });
1009
+
870
1010
  pi.registerCommand("harness-use-run", {
871
1011
  description: "Point this session at an existing run directory (recovery)",
872
1012
  handler: async (args, ctx) => {
@@ -1,9 +1,14 @@
1
1
  /**
2
- * harness-subagents — package-resolved agents, blackboard, observation-bus handoffs.
2
+ * harness-subagents — vendored pi-subagents with ultimate-pi discovery and policy gates.
3
3
  */
4
+
5
+ import { claimExtensionLoad } from "./lib/extension-load-guard.js";
4
6
  import { getHarnessPackageRoot } from "./lib/harness-paths.js";
5
- import { createHarnessSubagentsExtension } from "./lib/harness-subagents/vendored/index.js";
7
+ import { createHarnessSubagentsExtension } from "./lib/harness-subagents-bridge.js";
8
+
9
+ // @ts-expect-error pi extensions run as ESM
10
+ const MODULE_URL = import.meta.url;
6
11
 
7
- export default createHarnessSubagentsExtension(
8
- getHarnessPackageRoot(import.meta.url),
9
- );
12
+ export default claimExtensionLoad("harness-subagents", MODULE_URL)
13
+ ? createHarnessSubagentsExtension(getHarnessPackageRoot(MODULE_URL))
14
+ : () => {};
@@ -4,6 +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
8
  import {
8
9
  harnessWebContextLine,
9
10
  readTextExcerpt,
@@ -97,6 +98,7 @@ function sessionCwd(ctx: { cwd?: string }): string {
97
98
  }
98
99
 
99
100
  export default function harnessWebTools(pi: ExtensionAPI) {
101
+ if (!claimExtensionLoad("harness-web-tools", MODULE_URL)) return;
100
102
  pi.on("before_agent_start", async (event) => {
101
103
  return {
102
104
  systemPrompt: `${event.systemPrompt}\n\n${harnessWebContextLine()}`,
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const LOAD_GUARD_KEY = Symbol.for("ultimate-pi.extension-load-guard");
6
+
7
+ type LoadGuardRegistry = Set<string>;
8
+
9
+ function getRegistry(): LoadGuardRegistry {
10
+ const state = globalThis as typeof globalThis & {
11
+ [LOAD_GUARD_KEY]?: LoadGuardRegistry;
12
+ };
13
+ if (!state[LOAD_GUARD_KEY]) {
14
+ state[LOAD_GUARD_KEY] = new Set<string>();
15
+ }
16
+ return state[LOAD_GUARD_KEY];
17
+ }
18
+
19
+ function isSourceRepo(): boolean {
20
+ try {
21
+ const pkg = JSON.parse(
22
+ readFileSync(join(process.cwd(), "package.json"), "utf8"),
23
+ ) as { name?: string };
24
+ return pkg.name === "ultimate-pi";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export function claimExtensionLoad(key: string, moduleUrl: string): boolean {
31
+ const registry = getRegistry();
32
+ const modulePath = fileURLToPath(moduleUrl);
33
+ if (modulePath.includes("/node_modules/ultimate-pi/") && isSourceRepo()) {
34
+ return false;
35
+ }
36
+ if (registry.has(key)) return false;
37
+ registry.add(key);
38
+ return true;
39
+ }
@@ -22,7 +22,12 @@ export type HarnessPostHogEventName =
22
22
  | "harness_drift_report"
23
23
  | "harness_eval_verdict"
24
24
  | "harness_sentrux_signal"
25
- | "harness_observation";
25
+ | "harness_observation"
26
+ | "harness_subagent_spawned"
27
+ | "harness_subagent_completed"
28
+ | "harness_subagent_result_wait"
29
+ | "harness_subagent_setup"
30
+ | "harness_blackboard_op";
26
31
 
27
32
  const SCHEMA_VERSION = "1.0.0";
28
33
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Harness subagent spawn caps (subprocess model).
3
+ */
4
+
5
+ export const HARNESS_MAX_ACTIVE_SUBAGENTS = 8;
6
+ export const HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION = 12;
7
+
8
+ export function isHarnessAgentType(type: string): boolean {
9
+ return type.startsWith("harness/");
10
+ }
11
+
12
+ export interface SpawnBudgetState {
13
+ active: number;
14
+ totalHarnessSpawns: number;
15
+ }
16
+
17
+ export function createSpawnBudgetState(): SpawnBudgetState {
18
+ return { active: 0, totalHarnessSpawns: 0 };
19
+ }
20
+
21
+ export function countHarnessAgentsInRequest(params: {
22
+ agent?: string;
23
+ tasks?: { agent: string }[];
24
+ chain?: { agent: string }[];
25
+ aggregator?: { agent: string };
26
+ }): { harnessCount: number; agents: string[] } {
27
+ const agents: string[] = [];
28
+ if (params.agent) agents.push(params.agent);
29
+ if (params.tasks) for (const t of params.tasks) agents.push(t.agent);
30
+ if (params.chain) for (const c of params.chain) agents.push(c.agent);
31
+ if (params.aggregator) agents.push(params.aggregator.agent);
32
+ const harness = agents.filter(isHarnessAgentType);
33
+ return { harnessCount: harness.length, agents: harness };
34
+ }
35
+
36
+ export function checkHarnessSpawnBudget(
37
+ state: SpawnBudgetState,
38
+ incomingHarnessTasks: number,
39
+ ): { ok: boolean; message?: string } {
40
+ if (state.active + incomingHarnessTasks > HARNESS_MAX_ACTIVE_SUBAGENTS) {
41
+ return {
42
+ ok: false,
43
+ message:
44
+ `Harness subagent limit reached (${state.active} active + ${incomingHarnessTasks} requested > ${HARNESS_MAX_ACTIVE_SUBAGENTS}). ` +
45
+ `Wait for in-flight subagent calls to finish before spawning more.`,
46
+ };
47
+ }
48
+ if (
49
+ state.totalHarnessSpawns + incomingHarnessTasks >
50
+ HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION
51
+ ) {
52
+ return {
53
+ ok: false,
54
+ message:
55
+ `Harness subagent spawn cap reached (${state.totalHarnessSpawns + incomingHarnessTasks}/${HARNESS_MAX_SUBAGENT_SPAWNS_PER_SESSION} this session). ` +
56
+ `Finish the current harness phase or start a new session.`,
57
+ };
58
+ }
59
+ return { ok: true };
60
+ }
61
+
62
+ export function recordSpawnStart(
63
+ state: SpawnBudgetState,
64
+ harnessCount: number,
65
+ ): void {
66
+ state.active += harnessCount;
67
+ state.totalHarnessSpawns += harnessCount;
68
+ }
69
+
70
+ export function recordSpawnEnd(
71
+ state: SpawnBudgetState,
72
+ harnessCount: number,
73
+ ): void {
74
+ state.active = Math.max(0, state.active - harnessCount);
75
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Resolve concrete LLM credentials for harness subagent subprocesses.
3
+ *
4
+ * Parent sessions often use `router/auto` (pi-model-router). Subagents run with
5
+ * `--no-extensions`, so they cannot use the logical router provider — they need
6
+ * a real provider/model plus that provider's API key.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { AgentConfig } from "../../../vendor/pi-subagents/src/agents.js";
12
+
13
+ const ROUTER_SENTINEL_KEY = "pi-model-router";
14
+ const SENTINEL_API_KEYS = new Set([ROUTER_SENTINEL_KEY, "<authenticated>"]);
15
+
16
+ type RouterTier = "high" | "medium" | "low";
17
+
18
+ interface ModelRouterJson {
19
+ defaultProfile?: string;
20
+ profiles?: Record<string, Partial<Record<RouterTier, { model?: string }>>>;
21
+ }
22
+
23
+ export function isUsableApiKey(key: string | undefined): key is string {
24
+ return Boolean(key && !SENTINEL_API_KEYS.has(key));
25
+ }
26
+
27
+ export function parseModelRef(
28
+ ref: string,
29
+ ): { provider: string; modelId: string } | null {
30
+ const slash = ref.indexOf("/");
31
+ if (slash <= 0) return null;
32
+ const provider = ref.slice(0, slash).trim();
33
+ const modelId = ref.slice(slash + 1).trim();
34
+ if (!provider || !modelId) return null;
35
+ return { provider, modelId };
36
+ }
37
+
38
+ export function thinkingToRouterTier(thinking?: string): RouterTier {
39
+ if (thinking === "high" || thinking === "xhigh") return "high";
40
+ if (thinking === "off" || thinking === "minimal" || thinking === "low") {
41
+ return "low";
42
+ }
43
+ return "medium";
44
+ }
45
+
46
+ /** Map router profile tier → concrete `provider/model` from `.pi/model-router.json`. */
47
+ export function resolveRouterConcreteModelRef(
48
+ cwd: string,
49
+ profileId: string,
50
+ tier: RouterTier,
51
+ ): string | undefined {
52
+ const path = join(cwd, ".pi", "model-router.json");
53
+ if (!existsSync(path)) return undefined;
54
+ let raw: ModelRouterJson;
55
+ try {
56
+ raw = JSON.parse(readFileSync(path, "utf8")) as ModelRouterJson;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ const profiles = raw.profiles;
61
+ if (!profiles) return undefined;
62
+ const profile =
63
+ profiles[profileId] ??
64
+ profiles[raw.defaultProfile ?? "auto"] ??
65
+ profiles.auto;
66
+ const model = profile?.[tier]?.model;
67
+ return typeof model === "string" && model.includes("/") ? model : undefined;
68
+ }
69
+
70
+ export interface ConcreteSubagentModel {
71
+ modelRef: string;
72
+ provider: string;
73
+ modelId: string;
74
+ routerProfile?: string;
75
+ routerTier?: RouterTier;
76
+ }
77
+
78
+ /**
79
+ * Pick the subprocess model ref before resolving API keys.
80
+ * Never returns `router/*` — always a concrete provider.
81
+ */
82
+ export function resolveConcreteSubagentModel(
83
+ cwd: string,
84
+ parentModel: { provider: string; id: string } | undefined,
85
+ agent: AgentConfig,
86
+ ): ConcreteSubagentModel | undefined {
87
+ if (agent.model && !agent.model.startsWith("router/")) {
88
+ const parsed = parseModelRef(agent.model);
89
+ if (parsed) {
90
+ return { modelRef: agent.model, ...parsed };
91
+ }
92
+ }
93
+
94
+ const parentIsRouter = parentModel?.provider === "router";
95
+ const agentIsRouter = Boolean(agent.model?.startsWith("router/"));
96
+
97
+ if (!parentIsRouter && !agentIsRouter) {
98
+ if (parentModel && parentModel.provider !== "router") {
99
+ return {
100
+ modelRef: `${parentModel.provider}/${parentModel.id}`,
101
+ provider: parentModel.provider,
102
+ modelId: parentModel.id,
103
+ };
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ const profileId =
109
+ agentIsRouter && agent.model
110
+ ? agent.model.slice("router/".length)
111
+ : (parentModel?.id ?? "auto");
112
+ const tier = thinkingToRouterTier(agent.thinking);
113
+ const concrete = resolveRouterConcreteModelRef(cwd, profileId, tier);
114
+ if (!concrete) return undefined;
115
+ const parsed = parseModelRef(concrete);
116
+ if (!parsed || parsed.provider === "router") return undefined;
117
+ return {
118
+ modelRef: concrete,
119
+ ...parsed,
120
+ routerProfile: profileId,
121
+ routerTier: tier,
122
+ };
123
+ }
@@ -20,6 +20,15 @@ export type HarnessAgentKind =
20
20
 
21
21
  const MUTATING_TOOLS = new Set(["write", "edit"]);
22
22
 
23
+ const PLANNING_BASH_DENY_PATTERNS = [
24
+ /\bgraphify\s+update\b/i,
25
+ /\bgraphify\s+extract\b/i,
26
+ /\bgraphify\s+install\b/i,
27
+ /\bpip\s+install\b/i,
28
+ /\buv\s+tool\s+install\b/i,
29
+ /\bnpm\s+install\b/i,
30
+ ];
31
+
23
32
  const BASH_MUTATION_PATTERNS = [
24
33
  /\brm\s+-/i,
25
34
  /\bmv\s+/i,
@@ -45,11 +54,17 @@ const READ_ONLY_KINDS = new Set<HarnessAgentKind>([
45
54
  "meta",
46
55
  ]);
47
56
 
57
+ export function isHarnessPlanningAgent(agentType: string): boolean {
58
+ const id = agentType.replace(/^harness\//, "");
59
+ return id.startsWith("planning/");
60
+ }
61
+
48
62
  export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
49
63
  const id = agentType.replace(/^harness\//, "");
64
+ if (id.startsWith("planning/")) {
65
+ return "planner";
66
+ }
50
67
  switch (id) {
51
- case "planner":
52
- return "planner";
53
68
  case "executor":
54
69
  return "executor";
55
70
  case "evaluator":
@@ -96,20 +111,17 @@ export function evaluateHarnessSubagentToolCall(
96
111
  return { action: "allow" };
97
112
  }
98
113
 
99
- if (toolName === "create_plan") {
100
- if (kind === "planner") {
101
- return { action: "allow" };
102
- }
114
+ if (toolName === "create_plan" || toolName === "approve_plan") {
103
115
  return {
104
116
  action: "block",
105
- reason: `harness-subagent-policy: create_plan is only for harness/planner.`,
117
+ reason: `harness-subagent-policy: ${toolName} is parent-orchestrator only (not available in subagents).`,
106
118
  };
107
119
  }
108
120
 
109
121
  if (MUTATING_TOOLS.has(toolName)) {
110
122
  return {
111
123
  action: "block",
112
- reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent). Use create_plan after approve_plan instead of write/edit.`,
124
+ reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent).`,
113
125
  };
114
126
  }
115
127
 
@@ -121,13 +133,26 @@ export function evaluateHarnessSubagentToolCall(
121
133
  reason: `harness-subagent-policy: mutating bash blocked for harness/${kind}.`,
122
134
  };
123
135
  }
136
+ if (
137
+ command &&
138
+ isHarnessPlanningAgent(agentType) &&
139
+ PLANNING_BASH_DENY_PATTERNS.some((p) => p.test(command))
140
+ ) {
141
+ return {
142
+ action: "block",
143
+ reason:
144
+ "harness-subagent-policy: planning scouts may use read-only graphify/sg/ck commands only.",
145
+ };
146
+ }
124
147
  }
125
148
 
126
149
  return { action: "allow" };
127
150
  }
128
151
 
129
- /** Policy phase hint seeded into subagent system prompt appendix when extensions load policy-gate. */
130
152
  export function harnessSubagentPhaseHint(agentType: string): string | null {
153
+ if (isHarnessPlanningAgent(agentType)) {
154
+ return "plan";
155
+ }
131
156
  const kind = classifyHarnessAgent(agentType);
132
157
  switch (kind) {
133
158
  case "planner":