ultimate-pi 0.11.0 → 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 +1 -1
- package/.agents/skills/harness-orchestration/SKILL.md +54 -28
- package/.agents/skills/harness-plan/SKILL.md +15 -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 +11 -5
- package/.pi/agents/harness/planning/scout-semantic.md +11 -6
- 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 +145 -5
- package/.pi/extensions/harness-subagents.ts +2 -2
- 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} +3 -6
- 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 +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 +1 -1
- 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 +79 -93
- 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-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 +12 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/biome.json +2 -2
- package/package.json +6 -4
- 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
|
@@ -14,16 +14,20 @@
|
|
|
14
14
|
* }
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { appendFile, mkdir,
|
|
17
|
+
import { appendFile, mkdir, writeFile } from "node:fs/promises";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import {
|
|
21
|
+
type DebateParticipant,
|
|
22
|
+
debatePhaseFromId,
|
|
23
|
+
isPlanDebateId,
|
|
24
|
+
PLAN_DEBATE_PARTICIPANTS,
|
|
25
|
+
POST_EXECUTE_DEBATE_PARTICIPANTS,
|
|
26
|
+
} from "../lib/debate-orchestrator-types.js";
|
|
20
27
|
import { getRunIdFromSession } from "../lib/harness-run-context.js";
|
|
21
28
|
|
|
22
|
-
type DebateParticipant =
|
|
23
|
-
| "EvaluatorAgent"
|
|
24
|
-
| "AdversaryAgent"
|
|
25
|
-
| "TieBreakerAgent";
|
|
26
29
|
type PolicyDecision = "pass" | "conditional_pass" | "block" | "human_required";
|
|
30
|
+
type DebatePhase = "plan" | "post_execute";
|
|
27
31
|
|
|
28
32
|
interface RoundPayload {
|
|
29
33
|
participants: DebateParticipant[];
|
|
@@ -46,11 +50,13 @@ interface RoundPayload {
|
|
|
46
50
|
interface DebateState {
|
|
47
51
|
run_id: string;
|
|
48
52
|
debate_id: string;
|
|
53
|
+
debate_phase: DebatePhase;
|
|
49
54
|
round_count: number;
|
|
50
55
|
budget_used: number;
|
|
51
56
|
max_rounds: number;
|
|
52
57
|
round_token_cap: number;
|
|
53
58
|
debate_global_cap: number;
|
|
59
|
+
last_review_gate_ready?: boolean;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
interface BusEnvelope<T = unknown> {
|
|
@@ -104,46 +110,39 @@ function getRunId(ctx: {
|
|
|
104
110
|
);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
const PLAN_BUDGET = {
|
|
114
|
+
max_rounds: 4,
|
|
115
|
+
round_token_cap: 2000,
|
|
116
|
+
debate_global_cap: 12000,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
const AGGRESSIVE_BUDGET = {
|
|
120
|
+
max_rounds: 6,
|
|
121
|
+
round_token_cap: 2500,
|
|
122
|
+
debate_global_cap: 35000,
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
function capsForDebate(debateId: string): {
|
|
126
|
+
name: "plan" | "aggressive";
|
|
108
127
|
max_rounds: number;
|
|
109
128
|
round_token_cap: number;
|
|
110
129
|
debate_global_cap: number;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
} {
|
|
131
|
+
if (isPlanDebateId(debateId)) {
|
|
132
|
+
return { name: "plan", ...PLAN_BUDGET };
|
|
133
|
+
}
|
|
134
|
+
return { name: "aggressive", ...AGGRESSIVE_BUDGET };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function participantAllowed(participant: string, phase: DebatePhase): boolean {
|
|
138
|
+
if (phase === "plan") {
|
|
139
|
+
return (PLAN_DEBATE_PARTICIPANTS as readonly string[]).includes(
|
|
140
|
+
participant,
|
|
119
141
|
);
|
|
120
|
-
const parsed = JSON.parse(await readFile(roundSchemaPath, "utf-8")) as {
|
|
121
|
-
properties?: {
|
|
122
|
-
budget_profile?: {
|
|
123
|
-
properties?: {
|
|
124
|
-
max_rounds?: { const?: number };
|
|
125
|
-
round_token_cap?: { const?: number };
|
|
126
|
-
debate_global_cap?: { const?: number };
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
};
|
|
131
|
-
return {
|
|
132
|
-
max_rounds: Number(
|
|
133
|
-
parsed?.properties?.budget_profile?.properties?.max_rounds?.const ?? 6,
|
|
134
|
-
),
|
|
135
|
-
round_token_cap: Number(
|
|
136
|
-
parsed?.properties?.budget_profile?.properties?.round_token_cap
|
|
137
|
-
?.const ?? 2500,
|
|
138
|
-
),
|
|
139
|
-
debate_global_cap: Number(
|
|
140
|
-
parsed?.properties?.budget_profile?.properties?.debate_global_cap
|
|
141
|
-
?.const ?? 35000,
|
|
142
|
-
),
|
|
143
|
-
};
|
|
144
|
-
} catch {
|
|
145
|
-
return { max_rounds: 6, round_token_cap: 2500, debate_global_cap: 35000 };
|
|
146
142
|
}
|
|
143
|
+
return (POST_EXECUTE_DEBATE_PARTICIPANTS as readonly string[]).includes(
|
|
144
|
+
participant,
|
|
145
|
+
);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
async function writeDebateEvent(
|
|
@@ -197,13 +196,18 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
197
196
|
let lastSeverity = defaultSeverity();
|
|
198
197
|
|
|
199
198
|
async function openDebate(runId: string, debateId: string): Promise<void> {
|
|
200
|
-
const caps =
|
|
199
|
+
const caps = capsForDebate(debateId);
|
|
200
|
+
const debate_phase = debatePhaseFromId(debateId);
|
|
201
201
|
state = {
|
|
202
202
|
run_id: runId,
|
|
203
203
|
debate_id: debateId,
|
|
204
|
+
debate_phase,
|
|
204
205
|
round_count: 0,
|
|
205
206
|
budget_used: 0,
|
|
206
|
-
|
|
207
|
+
max_rounds: caps.max_rounds,
|
|
208
|
+
round_token_cap: caps.round_token_cap,
|
|
209
|
+
debate_global_cap: caps.debate_global_cap,
|
|
210
|
+
last_review_gate_ready: false,
|
|
207
211
|
};
|
|
208
212
|
pi.appendEntry("harness-debate-state", state);
|
|
209
213
|
const envelope: BusEnvelope = {
|
|
@@ -216,7 +220,8 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
216
220
|
},
|
|
217
221
|
payload: {
|
|
218
222
|
opened_at: nowIso(),
|
|
219
|
-
|
|
223
|
+
debate_phase,
|
|
224
|
+
budget_profile: caps.name,
|
|
220
225
|
},
|
|
221
226
|
};
|
|
222
227
|
pi.appendEntry("harness-debate-envelope", envelope);
|
|
@@ -267,6 +272,15 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
267
272
|
return { ok: false, reason: "debate id mismatch" };
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
for (const p of envelope.payload.participants ?? []) {
|
|
276
|
+
if (!participantAllowed(p, state.debate_phase)) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
reason: `participant ${p} invalid for debate_phase=${state.debate_phase}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
270
284
|
const nextRound = state.round_count + 1;
|
|
271
285
|
if (nextRound > state.max_rounds) {
|
|
272
286
|
await emitBudgetExhausted("max_rounds_reached");
|
|
@@ -310,6 +324,11 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
310
324
|
};
|
|
311
325
|
}
|
|
312
326
|
|
|
327
|
+
const profileName =
|
|
328
|
+
state.debate_phase === "plan"
|
|
329
|
+
? ("plan" as const)
|
|
330
|
+
: ("aggressive" as const);
|
|
331
|
+
|
|
313
332
|
const roundRecord = {
|
|
314
333
|
schema_version: "1.0.0",
|
|
315
334
|
contract_version: "1.0.0",
|
|
@@ -322,7 +341,7 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
322
341
|
evidence_refs: envelope.payload.evidence_refs,
|
|
323
342
|
token_usage: envelope.payload.token_usage,
|
|
324
343
|
budget_profile: {
|
|
325
|
-
name:
|
|
344
|
+
name: profileName,
|
|
326
345
|
max_rounds: state.max_rounds,
|
|
327
346
|
round_token_cap: state.round_token_cap,
|
|
328
347
|
debate_global_cap: state.debate_global_cap,
|
|
@@ -354,12 +373,20 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
354
373
|
),
|
|
355
374
|
);
|
|
356
375
|
const decision = decidePolicy(lastSeverity, evidenceScore);
|
|
376
|
+
const planPhase = state.debate_phase === "plan";
|
|
377
|
+
const evaluatorPassed = planPhase
|
|
378
|
+
? Boolean(state.last_review_gate_ready)
|
|
379
|
+
: true;
|
|
380
|
+
const debateComplete = planPhase
|
|
381
|
+
? state.round_count >= state.max_rounds
|
|
382
|
+
: state.round_count > 0;
|
|
357
383
|
|
|
358
384
|
const consensus = {
|
|
359
385
|
schema_version: "1.0.0",
|
|
360
386
|
contract_version: "1.0.0",
|
|
361
387
|
run_id: state.run_id,
|
|
362
388
|
debate_id: state.debate_id,
|
|
389
|
+
debate_phase: state.debate_phase,
|
|
363
390
|
round_count: state.round_count,
|
|
364
391
|
budget_used: state.budget_used,
|
|
365
392
|
severity_scores: lastSeverity,
|
|
@@ -371,15 +398,25 @@ export default function debateOrchestrator(pi: ExtensionAPI) {
|
|
|
371
398
|
},
|
|
372
399
|
confidence_weights: WEIGHTS,
|
|
373
400
|
evidence_refs: [],
|
|
374
|
-
strict_gate_prerequisites:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
strict_gate_prerequisites: planPhase
|
|
402
|
+
? {
|
|
403
|
+
plan_gate_passed: false,
|
|
404
|
+
execution_completed: false,
|
|
405
|
+
evaluator_passed: evaluatorPassed,
|
|
406
|
+
adversarial_debate_completed: debateComplete,
|
|
407
|
+
severity_policy_ok: decision !== "block",
|
|
408
|
+
benchmark_delta_checks_passed: false,
|
|
409
|
+
rollback_artifacts_generated: false,
|
|
410
|
+
}
|
|
411
|
+
: {
|
|
412
|
+
plan_gate_passed: true,
|
|
413
|
+
execution_completed: true,
|
|
414
|
+
evaluator_passed: true,
|
|
415
|
+
adversarial_debate_completed: debateComplete,
|
|
416
|
+
severity_policy_ok: decision !== "block",
|
|
417
|
+
benchmark_delta_checks_passed: false,
|
|
418
|
+
rollback_artifacts_generated: false,
|
|
419
|
+
},
|
|
383
420
|
policy_decision: decision,
|
|
384
421
|
rationale,
|
|
385
422
|
};
|
|
@@ -236,7 +236,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
236
236
|
name: "create_plan",
|
|
237
237
|
label: "Create Plan",
|
|
238
238
|
description:
|
|
239
|
-
"Write the approved PlanPacket to plan-packet.
|
|
239
|
+
"Write the approved PlanPacket to plan-packet.yaml for this harness run. Call only after approve_plan (Approve). Do not use write/edit.",
|
|
240
240
|
promptSnippet: CREATE_PLAN_SNIPPET,
|
|
241
241
|
promptGuidelines: CREATE_PLAN_GUIDELINES,
|
|
242
242
|
parameters: CreatePlanParamsSchema,
|
|
@@ -298,7 +298,7 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
298
298
|
return new Text(
|
|
299
299
|
theme.fg(
|
|
300
300
|
"success",
|
|
301
|
-
`Wrote ${details?.plan_path ?? "plan-packet.
|
|
301
|
+
`Wrote ${details?.plan_path ?? "plan-packet.yaml"}`,
|
|
302
302
|
),
|
|
303
303
|
0,
|
|
304
304
|
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);
|
|
@@ -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,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* harness-subagents —
|
|
2
|
+
* harness-subagents — vendored pi-subagents with ultimate-pi discovery and policy gates.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { claimExtensionLoad } from "./lib/extension-load-guard.js";
|
|
6
6
|
import { getHarnessPackageRoot } from "./lib/harness-paths.js";
|
|
7
|
-
import { createHarnessSubagentsExtension } from "./lib/harness-subagents
|
|
7
|
+
import { createHarnessSubagentsExtension } from "./lib/harness-subagents-bridge.js";
|
|
8
8
|
|
|
9
9
|
// @ts-expect-error pi extensions run as ESM
|
|
10
10
|
const MODULE_URL = import.meta.url;
|
|
@@ -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
|
+
}
|