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.
- package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
- package/.agents/skills/harness-decisions/SKILL.md +3 -3
- package/.agents/skills/harness-orchestration/SKILL.md +59 -25
- package/.agents/skills/harness-plan/SKILL.md +16 -15
- package/.pi/agents/harness/adversary.md +0 -1
- package/.pi/agents/harness/evaluator.md +0 -1
- package/.pi/agents/harness/executor.md +1 -2
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/meta-optimizer.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +83 -0
- package/.pi/agents/harness/planning/execution-plan-author.md +30 -0
- package/.pi/agents/harness/planning/hypothesis-validator.md +23 -0
- package/.pi/agents/harness/planning/hypothesis.md +89 -0
- package/.pi/agents/harness/planning/plan-adversary.md +18 -0
- package/.pi/agents/harness/planning/plan-evaluator.md +18 -0
- package/.pi/agents/harness/planning/review-integrator.md +23 -0
- package/.pi/agents/harness/planning/scout-graphify.md +54 -0
- package/.pi/agents/harness/planning/scout-semantic.md +47 -0
- package/.pi/agents/harness/planning/scout-structure.md +50 -0
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +18 -0
- package/.pi/agents/harness/planning/stack-researcher.md +24 -0
- package/.pi/agents/harness/tie-breaker.md +0 -1
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/debate-orchestrator.ts +90 -53
- package/.pi/extensions/harness-ask-user.ts +5 -0
- package/.pi/extensions/harness-plan-approval.ts +137 -3
- package/.pi/extensions/harness-run-context.ts +146 -6
- package/.pi/extensions/harness-subagents.ts +10 -5
- package/.pi/extensions/harness-web-tools.ts +2 -0
- package/.pi/extensions/lib/extension-load-guard.ts +39 -0
- package/.pi/extensions/lib/harness-posthog.ts +6 -1
- package/.pi/extensions/lib/harness-spawn-budget.ts +75 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +123 -0
- package/.pi/extensions/lib/{harness-subagents/harness-subagent-policy.ts → harness-subagent-policy.ts} +34 -9
- package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +176 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +9 -7
- package/.pi/extensions/lib/plan-approval/plan-review.ts +393 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +16 -1
- package/.pi/extensions/lib/plan-approval/types.ts +16 -0
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -0
- package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
- package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +2 -5
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/extensions/review-integrity.ts +48 -29
- package/.pi/extensions/ultimate-pi-vcc.ts +5 -0
- package/.pi/harness/agents.manifest.json +126 -82
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -6
- package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +34 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +41 -0
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r1.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r4.yaml +26 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/sprint-audit-r4.yaml +5 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +32 -0
- package/.pi/harness/evals/smoke/run-context.fixture.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +88 -0
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-posthog-event.schema.json +6 -1
- package/.pi/harness/specs/harness-spawn-context.schema.json +2 -1
- package/.pi/harness/specs/plan-adversary-brief.schema.json +45 -0
- package/.pi/harness/specs/plan-decomposition-brief.schema.json +108 -0
- package/.pi/harness/specs/plan-execution-plan-brief.schema.json +13 -0
- package/.pi/harness/specs/plan-execution-plan.schema.json +255 -0
- package/.pi/harness/specs/plan-hypothesis-brief.schema.json +96 -0
- package/.pi/harness/specs/plan-hypothesis-eval.schema.json +61 -0
- package/.pi/harness/specs/plan-packet.schema.json +14 -5
- package/.pi/harness/specs/plan-review-round-draft.schema.json +68 -0
- package/.pi/harness/specs/plan-sprint-audit-turn.schema.json +29 -0
- package/.pi/harness/specs/plan-stack-brief.schema.json +65 -0
- package/.pi/harness/specs/plan-validation-turn.schema.json +42 -0
- package/.pi/harness/specs/round-result.schema.json +16 -9
- package/.pi/lib/debate-orchestrator-types.ts +38 -0
- package/.pi/lib/harness-agent-discovery.mjs +81 -0
- package/.pi/lib/harness-run-context.ts +76 -38
- package/.pi/lib/harness-yaml.mjs +73 -0
- package/.pi/lib/harness-yaml.ts +90 -0
- package/.pi/prompts/harness-auto.md +13 -11
- package/.pi/prompts/harness-critic.md +2 -2
- package/.pi/prompts/harness-eval.md +3 -3
- package/.pi/prompts/harness-incident.md +2 -2
- package/.pi/prompts/harness-plan.md +106 -37
- package/.pi/prompts/harness-review.md +2 -2
- package/.pi/prompts/harness-router-tune.md +1 -1
- package/.pi/prompts/harness-run.md +2 -2
- package/.pi/prompts/harness-setup.md +15 -6
- package/.pi/prompts/harness-trace.md +2 -2
- package/.pi/scripts/harness-agents-manifest.mjs +1 -1
- package/.pi/scripts/harness-resolve-up-pkg.mjs +13 -0
- package/.pi/scripts/harness-verify.mjs +28 -19
- package/.pi/scripts/validate-plan-dag.mjs +258 -0
- package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
- package/CHANGELOG.md +24 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/biome.json +4 -1
- package/package.json +6 -4
- package/.pi/agents/harness/planner.md +0 -54
- package/.pi/extensions/lib/harness-subagents/agent-loader.ts +0 -126
- package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +0 -119
- package/.pi/extensions/lib/harness-subagents/agent-parser.ts +0 -87
- package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +0 -118
- package/.pi/extensions/lib/harness-subagents/blackboard.ts +0 -175
- package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +0 -10
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +0 -310
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -59
- package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +0 -27
- package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +0 -558
- package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -684
- package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +0 -175
- package/.pi/extensions/lib/harness-subagents/vendored/context.ts +0 -59
- package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +0 -134
- package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +0 -5
- package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +0 -123
- package/.pi/extensions/lib/harness-subagents/vendored/env.ts +0 -43
- package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +0 -144
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +0 -2494
- package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +0 -52
- package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +0 -182
- package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +0 -92
- package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +0 -115
- package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +0 -103
- package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +0 -177
- package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +0 -416
- package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +0 -210
- package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +0 -108
- package/.pi/extensions/lib/harness-subagents/vendored/types.ts +0 -187
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +0 -639
- package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +0 -324
- package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +0 -110
- package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +0 -71
- package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +0 -195
- /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.
|
|
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.
|
|
758
|
+
if (target.includes("plan-packet.yaml")) {
|
|
711
759
|
return {
|
|
712
760
|
block: true,
|
|
713
761
|
reason:
|
|
714
|
-
"harness-run-context: plan-packet.
|
|
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.
|
|
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
|
|
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 —
|
|
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
|
|
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
|
|
8
|
-
getHarnessPackageRoot(
|
|
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:
|
|
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)
|
|
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":
|