ultimate-pi 0.11.0 → 0.13.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/ck-search/SKILL.md +11 -87
- package/.agents/skills/cocoindex-search/SKILL.md +35 -0
- package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
- package/.agents/skills/harness-decisions/SKILL.md +1 -1
- package/.agents/skills/harness-orchestration/SKILL.md +54 -28
- package/.agents/skills/harness-plan/SKILL.md +15 -20
- package/.pi/PACKAGING.md +1 -0
- package/.pi/SYSTEM.md +21 -20
- 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 +3 -4
- 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 +3 -4
- package/.pi/agents/harness/planning/plan-adversary.md +10 -42
- 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 +13 -5
- package/.pi/agents/harness/planning/scout-semantic.md +23 -11
- package/.pi/agents/harness/planning/scout-structure.md +12 -6
- 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-plan-approval.ts +2 -2
- package/.pi/extensions/harness-run-context.ts +150 -5
- package/.pi/extensions/harness-subagents.ts +17 -6
- package/.pi/extensions/lib/harness-cocoindex-refresh.ts +49 -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} +8 -7
- package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +122 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +4 -7
- package/.pi/extensions/lib/plan-approval/plan-review.ts +1 -1
- package/.pi/extensions/lib/plan-approval/types.ts +7 -1
- package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
- package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +1 -0
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/extensions/review-integrity.ts +48 -29
- package/.pi/harness/agents.manifest.json +37 -25
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +4 -3
- package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +2 -2
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -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/harness-posthog-event.schema.json +6 -1
- 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-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 +64 -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 +83 -92
- 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 +30 -17
- package/.pi/prompts/harness-trace.md +2 -2
- package/.pi/scripts/README.md +1 -0
- package/.pi/scripts/harness-agents-manifest.mjs +1 -1
- package/.pi/scripts/harness-cli-verify.sh +24 -14
- package/.pi/scripts/harness-cocoindex-bootstrap.sh +182 -0
- package/.pi/scripts/harness-verify.mjs +38 -19
- package/.pi/scripts/validate-plan-dag.mjs +258 -0
- package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
- package/.pi/skills/ast-grep/SKILL.md +2 -2
- package/.pi/skills/ccc/SKILL.md +142 -0
- package/.pi/skills/ccc/references/management.md +110 -0
- package/CHANGELOG.md +22 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/biome.json +2 -2
- package/package.json +7 -4
- package/vendor/pi-subagents/LICENSE +21 -0
- package/vendor/pi-subagents/UPSTREAM_PIN.md +11 -0
- package/vendor/pi-subagents/src/agents.ts +357 -0
- package/vendor/pi-subagents/src/subagents.ts +1463 -0
- package/.pi/agents/harness/planner.md +0 -13
- package/.pi/agents/harness/planning/hypothesis-eval.md +0 -59
- package/.pi/agents/harness/planning/planner.md +0 -20
- 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 -137
- package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -77
- 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 -666
- 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 -2460
- 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,15 @@ 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";
|
|
58
|
+
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
59
|
+
|
|
60
|
+
// @ts-expect-error pi extensions run as ESM
|
|
61
|
+
const MODULE_URL = import.meta.url;
|
|
48
62
|
|
|
49
63
|
interface SessionEntryLike {
|
|
50
64
|
type?: string;
|
|
@@ -84,6 +98,32 @@ function appendHarnessTurn(pi: ExtensionAPI, turn: HarnessTurnEntry): void {
|
|
|
84
98
|
});
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
async function coerceScopedHarnessYamlWrite(
|
|
102
|
+
event: { toolName: string; input: Record<string, unknown> },
|
|
103
|
+
runCtx: HarnessRunContext,
|
|
104
|
+
projectRoot: string,
|
|
105
|
+
): Promise<{ block: true; reason: string } | undefined> {
|
|
106
|
+
if (event.toolName !== "write") return undefined;
|
|
107
|
+
const target = extractWritePathFromToolInput(event.input);
|
|
108
|
+
if (!target.endsWith(".yaml") && !target.endsWith(".yml")) return undefined;
|
|
109
|
+
const scoped = await isPlanPhaseScopedWrite(target, runCtx, projectRoot);
|
|
110
|
+
if (!scoped) return undefined;
|
|
111
|
+
const content = event.input.content;
|
|
112
|
+
if (typeof content !== "string") return undefined;
|
|
113
|
+
try {
|
|
114
|
+
event.input.content = normalizeHarnessYamlContent(content, target);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
117
|
+
return {
|
|
118
|
+
block: true,
|
|
119
|
+
reason:
|
|
120
|
+
`harness-run-context: ${target} must be canonical YAML, not embedded JSON. ` +
|
|
121
|
+
`Use write_harness_yaml with the subagent JSON/YAML block, or paste valid YAML. (${msg})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
87
127
|
function syncPolicyFromPlan(
|
|
88
128
|
pi: ExtensionAPI,
|
|
89
129
|
entries: unknown[],
|
|
@@ -155,6 +195,7 @@ function needsClarificationFollowUp(ctx: HarnessRunContext | null): boolean {
|
|
|
155
195
|
}
|
|
156
196
|
|
|
157
197
|
export default function harnessRunContext(pi: ExtensionAPI) {
|
|
198
|
+
if (!claimExtensionLoad("harness-run-context", MODULE_URL)) return;
|
|
158
199
|
let activeCtx: HarnessRunContext | null = null;
|
|
159
200
|
|
|
160
201
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -583,7 +624,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
583
624
|
activeCtx.last_outcome = "needs_clarification";
|
|
584
625
|
activeCtx.last_completed_step = "plan";
|
|
585
626
|
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.
|
|
627
|
+
"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
628
|
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
588
629
|
else
|
|
589
630
|
pi.sendMessage({
|
|
@@ -671,6 +712,18 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
671
712
|
});
|
|
672
713
|
|
|
673
714
|
pi.on("tool_call", async (event, ctx) => {
|
|
715
|
+
if (event.toolName === "write") {
|
|
716
|
+
const entries = getEntries(ctx);
|
|
717
|
+
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
718
|
+
if (runCtx) {
|
|
719
|
+
const blocked = await coerceScopedHarnessYamlWrite(
|
|
720
|
+
event,
|
|
721
|
+
runCtx,
|
|
722
|
+
process.cwd(),
|
|
723
|
+
);
|
|
724
|
+
if (blocked) return blocked;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
674
727
|
if (activeCtx?.plan_packet_path) {
|
|
675
728
|
const entries = getEntries(ctx);
|
|
676
729
|
if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
|
|
@@ -707,11 +760,11 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
707
760
|
(event.input as { filePath?: string }).filePath ??
|
|
708
761
|
"",
|
|
709
762
|
);
|
|
710
|
-
if (target.includes("plan-packet.
|
|
763
|
+
if (target.includes("plan-packet.yaml")) {
|
|
711
764
|
return {
|
|
712
765
|
block: true,
|
|
713
766
|
reason:
|
|
714
|
-
"harness-run-context: plan-packet.
|
|
767
|
+
"harness-run-context: plan-packet.yaml is read-only in evaluate/adversary phases.",
|
|
715
768
|
};
|
|
716
769
|
}
|
|
717
770
|
return undefined;
|
|
@@ -792,7 +845,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
792
845
|
|
|
793
846
|
pi.registerCommand("harness-plan-commit", {
|
|
794
847
|
description:
|
|
795
|
-
"Write approved plan-packet.
|
|
848
|
+
"Write approved plan-packet.yaml to the active run (requires harness-plan-approval)",
|
|
796
849
|
handler: async (args, ctx) => {
|
|
797
850
|
const projectRoot = process.cwd();
|
|
798
851
|
const entries = getEntries(ctx);
|
|
@@ -867,6 +920,98 @@ export default function harnessRunContext(pi: ExtensionAPI) {
|
|
|
867
920
|
},
|
|
868
921
|
});
|
|
869
922
|
|
|
923
|
+
pi.registerTool({
|
|
924
|
+
name: "write_harness_yaml",
|
|
925
|
+
label: "Write Harness YAML",
|
|
926
|
+
description:
|
|
927
|
+
"Write a plan-phase harness artifact as canonical YAML (parses subagent JSON or YAML, never embeds JSON in .yaml files).",
|
|
928
|
+
promptSnippet:
|
|
929
|
+
"Persist plan artifacts (decomposition, hypothesis, stack, review rounds) as real YAML.",
|
|
930
|
+
promptGuidelines: [
|
|
931
|
+
"Use write_harness_yaml for all artifacts/*.yaml and research-brief.yaml updates during /harness-plan.",
|
|
932
|
+
"Pass the subagent fenced json or yaml block as content; the tool converts to YAML on disk.",
|
|
933
|
+
"Do not use write with stringified JSON for .yaml paths.",
|
|
934
|
+
"plan-packet.yaml after approval: prefer create_plan; write_harness_yaml is for drafts and side artifacts only.",
|
|
935
|
+
],
|
|
936
|
+
parameters: Type.Object({
|
|
937
|
+
path: Type.String({
|
|
938
|
+
description:
|
|
939
|
+
"Path under the active run, e.g. artifacts/decomposition.yaml or research-brief.yaml",
|
|
940
|
+
}),
|
|
941
|
+
content: Type.String({
|
|
942
|
+
description:
|
|
943
|
+
"YAML or JSON document (fenced or raw) matching the artifact schema",
|
|
944
|
+
}),
|
|
945
|
+
}),
|
|
946
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
947
|
+
const entries = getEntries(ctx);
|
|
948
|
+
const runCtx = getLatestRunContext(entries) ?? activeCtx;
|
|
949
|
+
if (!runCtx?.run_id) {
|
|
950
|
+
return {
|
|
951
|
+
content: [
|
|
952
|
+
{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: 'No active harness run. Run /harness-plan "<task>" first.',
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
details: {},
|
|
958
|
+
isError: true,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const pathArg = String((params as { path?: string }).path ?? "").trim();
|
|
962
|
+
const content = String((params as { content?: string }).content ?? "");
|
|
963
|
+
if (!pathArg || !content.trim()) {
|
|
964
|
+
return {
|
|
965
|
+
content: [
|
|
966
|
+
{
|
|
967
|
+
type: "text",
|
|
968
|
+
text: "write_harness_yaml requires path and content.",
|
|
969
|
+
},
|
|
970
|
+
],
|
|
971
|
+
details: {},
|
|
972
|
+
isError: true,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const projectRoot = process.cwd();
|
|
976
|
+
const absPath = normalizeHarnessPath(pathArg, projectRoot);
|
|
977
|
+
const scoped = await isPlanPhaseScopedWrite(absPath, runCtx, projectRoot);
|
|
978
|
+
if (!scoped) {
|
|
979
|
+
return {
|
|
980
|
+
content: [
|
|
981
|
+
{
|
|
982
|
+
type: "text",
|
|
983
|
+
text: `Path not allowed: ${pathArg}. Must be under .pi/harness/runs/${runCtx.run_id}/ (artifacts/*.yaml, research-brief.yaml, etc.).`,
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
details: { path: pathArg },
|
|
987
|
+
isError: true,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
let doc: unknown;
|
|
991
|
+
try {
|
|
992
|
+
doc = parseStructuredDocument(content, pathArg);
|
|
993
|
+
} catch (err) {
|
|
994
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
995
|
+
return {
|
|
996
|
+
content: [{ type: "text", text: msg }],
|
|
997
|
+
details: { path: pathArg },
|
|
998
|
+
isError: true,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
1002
|
+
await writeYamlFile(absPath, doc);
|
|
1003
|
+
return {
|
|
1004
|
+
content: [
|
|
1005
|
+
{
|
|
1006
|
+
type: "text",
|
|
1007
|
+
text: `Wrote ${pathArg} as canonical YAML.`,
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
details: { path: absPath },
|
|
1011
|
+
};
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
|
|
870
1015
|
pi.registerCommand("harness-use-run", {
|
|
871
1016
|
description: "Point this session at an existing run directory (recovery)",
|
|
872
1017
|
handler: async (args, ctx) => {
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* harness-subagents —
|
|
2
|
+
* harness-subagents — vendored pi-subagents with ultimate-pi discovery and policy gates.
|
|
3
|
+
*
|
|
4
|
+
* Dynamic-imports the bridge only after claimExtensionLoad so a stale global npm
|
|
5
|
+
* install (missing vendor/pi-subagents) does not crash local development in this repo.
|
|
3
6
|
*/
|
|
4
7
|
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
9
|
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
6
|
-
import { getHarnessPackageRoot } from "./lib/harness-paths.js";
|
|
7
|
-
import { createHarnessSubagentsExtension } from "./lib/harness-subagents/vendored/index.js";
|
|
8
10
|
|
|
9
11
|
// @ts-expect-error pi extensions run as ESM
|
|
10
12
|
const MODULE_URL = import.meta.url;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
async function loadHarnessSubagents(): Promise<(pi: ExtensionAPI) => void> {
|
|
15
|
+
if (!claimExtensionLoad("harness-subagents", MODULE_URL)) {
|
|
16
|
+
return () => {};
|
|
17
|
+
}
|
|
18
|
+
const { getHarnessPackageRoot } = await import("./lib/harness-paths.js");
|
|
19
|
+
const { createHarnessSubagentsExtension } = await import(
|
|
20
|
+
"./lib/harness-subagents-bridge.js"
|
|
21
|
+
);
|
|
22
|
+
return createHarnessSubagentsExtension(getHarnessPackageRoot(MODULE_URL));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default await loadHarnessSubagents();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental CocoIndex refresh before harness subagent batches (plan/execute).
|
|
3
|
+
* Agents use `ccc search` only; harness owns `ccc index`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
11
|
+
|
|
12
|
+
export function refreshHarnessCocoindexIndex(cwd: string): string | undefined {
|
|
13
|
+
if (process.env.HARNESS_COCOINDEX_REFRESH === "0") {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const settingsPath = join(cwd, ".cocoindex_code", "settings.yml");
|
|
17
|
+
if (!existsSync(settingsPath)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const timeoutMs = Number(
|
|
22
|
+
process.env.HARNESS_COCOINDEX_REFRESH_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS,
|
|
23
|
+
);
|
|
24
|
+
const result = spawnSync("ccc", ["index"], {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
timeout: Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_TIMEOUT_MS,
|
|
28
|
+
stdio: "pipe",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.error) {
|
|
32
|
+
const msg = `harness-cocoindex: ccc index failed (${result.error.message})`;
|
|
33
|
+
if (process.env.HARNESS_COCOINDEX_REFRESH_STRICT === "1") {
|
|
34
|
+
return msg;
|
|
35
|
+
}
|
|
36
|
+
return `${msg} — continuing`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
const stderr = (result.stderr ?? "").trim().slice(0, 500);
|
|
41
|
+
const msg = `harness-cocoindex: ccc index exited ${result.status ?? "?"}${stderr ? `: ${stderr}` : ""}`;
|
|
42
|
+
if (process.env.HARNESS_COCOINDEX_REFRESH_STRICT === "1") {
|
|
43
|
+
return msg;
|
|
44
|
+
}
|
|
45
|
+
return `${msg} — continuing`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -24,9 +24,13 @@ const PLANNING_BASH_DENY_PATTERNS = [
|
|
|
24
24
|
/\bgraphify\s+update\b/i,
|
|
25
25
|
/\bgraphify\s+extract\b/i,
|
|
26
26
|
/\bgraphify\s+install\b/i,
|
|
27
|
+
/\bccc\s+(index|init|reset|daemon)\b/i,
|
|
28
|
+
/\bccc\s+search\b.*--refresh/i,
|
|
27
29
|
/\bpip\s+install\b/i,
|
|
28
30
|
/\buv\s+tool\s+install\b/i,
|
|
29
31
|
/\bnpm\s+install\b/i,
|
|
32
|
+
/\bnpm\s+install\b.*cocoindex/i,
|
|
33
|
+
/\buv\s+tool\s+install\b.*cocoindex/i,
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
const BASH_MUTATION_PATTERNS = [
|
|
@@ -56,17 +60,15 @@ const READ_ONLY_KINDS = new Set<HarnessAgentKind>([
|
|
|
56
60
|
|
|
57
61
|
export function isHarnessPlanningAgent(agentType: string): boolean {
|
|
58
62
|
const id = agentType.replace(/^harness\//, "");
|
|
59
|
-
return id
|
|
63
|
+
return id.startsWith("planning/");
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export function classifyHarnessAgent(agentType: string): HarnessAgentKind {
|
|
63
67
|
const id = agentType.replace(/^harness\//, "");
|
|
64
|
-
if (id.startsWith("planning/")
|
|
68
|
+
if (id.startsWith("planning/")) {
|
|
65
69
|
return "planner";
|
|
66
70
|
}
|
|
67
71
|
switch (id) {
|
|
68
|
-
case "planner":
|
|
69
|
-
return "planner";
|
|
70
72
|
case "executor":
|
|
71
73
|
return "executor";
|
|
72
74
|
case "evaluator":
|
|
@@ -123,7 +125,7 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
123
125
|
if (MUTATING_TOOLS.has(toolName)) {
|
|
124
126
|
return {
|
|
125
127
|
action: "block",
|
|
126
|
-
reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent)
|
|
128
|
+
reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent).`,
|
|
127
129
|
};
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -143,7 +145,7 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
143
145
|
return {
|
|
144
146
|
action: "block",
|
|
145
147
|
reason:
|
|
146
|
-
"harness-subagent-policy: planning scouts may use read-only graphify/sg/
|
|
148
|
+
"harness-subagent-policy: planning scouts may use read-only graphify/sg/ccc commands only.",
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
}
|
|
@@ -151,7 +153,6 @@ export function evaluateHarnessSubagentToolCall(
|
|
|
151
153
|
return { action: "allow" };
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
/** Policy phase hint seeded into subagent system prompt appendix when extensions load policy-gate. */
|
|
155
156
|
export function harnessSubagentPhaseHint(agentType: string): string | null {
|
|
156
157
|
if (isHarnessPlanningAgent(agentType)) {
|
|
157
158
|
return "plan";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-spawn validation for harness subagent tool calls.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type AgentConfig,
|
|
7
|
+
agentAllowsMutatingTools,
|
|
8
|
+
} from "../../../vendor/pi-subagents/src/agents.js";
|
|
9
|
+
import type { HarnessPhase } from "../../lib/harness-run-context.js";
|
|
10
|
+
import { inferHarnessPhase } from "../../lib/harness-run-context.js";
|
|
11
|
+
import { classifyHarnessAgent } from "./harness-subagent-policy.js";
|
|
12
|
+
|
|
13
|
+
export interface SubagentTaskRef {
|
|
14
|
+
agent: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PrecheckResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
message?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectAgents(params: {
|
|
23
|
+
agent?: string;
|
|
24
|
+
tasks?: SubagentTaskRef[];
|
|
25
|
+
chain?: SubagentTaskRef[];
|
|
26
|
+
aggregator?: { agent: string };
|
|
27
|
+
}): string[] {
|
|
28
|
+
const names: string[] = [];
|
|
29
|
+
if (params.agent) names.push(params.agent);
|
|
30
|
+
if (params.tasks) for (const t of params.tasks) names.push(t.agent);
|
|
31
|
+
if (params.chain) for (const c of params.chain) names.push(c.agent);
|
|
32
|
+
if (params.aggregator) names.push(params.aggregator.agent);
|
|
33
|
+
return names;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveAgent(
|
|
37
|
+
agents: AgentConfig[],
|
|
38
|
+
name: string,
|
|
39
|
+
): AgentConfig | undefined {
|
|
40
|
+
return agents.find((a) => a.name === name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function precheckHarnessSubagentSpawn(
|
|
44
|
+
params: {
|
|
45
|
+
agent?: string;
|
|
46
|
+
tasks?: SubagentTaskRef[];
|
|
47
|
+
chain?: SubagentTaskRef[];
|
|
48
|
+
aggregator?: { agent: string };
|
|
49
|
+
},
|
|
50
|
+
agents: AgentConfig[],
|
|
51
|
+
phase: HarnessPhase,
|
|
52
|
+
): PrecheckResult {
|
|
53
|
+
const names = collectAgents(params);
|
|
54
|
+
const mutating = names.filter((n) => {
|
|
55
|
+
const cfg = resolveAgent(agents, n);
|
|
56
|
+
return cfg
|
|
57
|
+
? agentAllowsMutatingTools(cfg)
|
|
58
|
+
: n.startsWith("harness/executor");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (phase === "plan" && mutating.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
message:
|
|
65
|
+
`Plan phase: cannot spawn mutating subagents (${mutating.join(", ")}). ` +
|
|
66
|
+
`Use read-only harness/planning/* agents until execute phase.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ((params.tasks?.length ?? 0) > 1 && mutating.length > 1) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message:
|
|
74
|
+
"Parallel subagent tasks cannot include multiple mutating agents (file race risk). " +
|
|
75
|
+
"Run one executor at a time.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const name of names) {
|
|
80
|
+
if (!name.startsWith("harness/")) continue;
|
|
81
|
+
const kind = classifyHarnessAgent(name);
|
|
82
|
+
if (kind === "planner" && phase !== "plan") {
|
|
83
|
+
// allowed — planning agents can run in plan only ideally
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function inferPhaseForPrecheck(
|
|
91
|
+
entries: unknown[],
|
|
92
|
+
prompt?: string,
|
|
93
|
+
): HarnessPhase {
|
|
94
|
+
return inferHarnessPhase(entries as never, prompt);
|
|
95
|
+
}
|