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.
Files changed (122) hide show
  1. package/.agents/skills/harness-debate-plan/SKILL.md +44 -0
  2. package/.agents/skills/harness-decisions/SKILL.md +1 -1
  3. package/.agents/skills/harness-orchestration/SKILL.md +54 -28
  4. package/.agents/skills/harness-plan/SKILL.md +15 -20
  5. package/.pi/agents/harness/adversary.md +0 -1
  6. package/.pi/agents/harness/evaluator.md +0 -1
  7. package/.pi/agents/harness/executor.md +1 -2
  8. package/.pi/agents/harness/incident-recorder.md +0 -1
  9. package/.pi/agents/harness/meta-optimizer.md +0 -1
  10. package/.pi/agents/harness/planning/decompose.md +3 -4
  11. package/.pi/agents/harness/planning/execution-plan-author.md +30 -0
  12. package/.pi/agents/harness/planning/hypothesis-validator.md +23 -0
  13. package/.pi/agents/harness/planning/hypothesis.md +3 -4
  14. package/.pi/agents/harness/planning/plan-adversary.md +10 -42
  15. package/.pi/agents/harness/planning/plan-evaluator.md +18 -0
  16. package/.pi/agents/harness/planning/review-integrator.md +23 -0
  17. package/.pi/agents/harness/planning/scout-graphify.md +11 -5
  18. package/.pi/agents/harness/planning/scout-semantic.md +11 -6
  19. package/.pi/agents/harness/planning/scout-structure.md +12 -6
  20. package/.pi/agents/harness/planning/sprint-contract-auditor.md +18 -0
  21. package/.pi/agents/harness/planning/stack-researcher.md +24 -0
  22. package/.pi/agents/harness/tie-breaker.md +0 -1
  23. package/.pi/agents/harness/trace-librarian.md +0 -1
  24. package/.pi/extensions/debate-orchestrator.ts +90 -53
  25. package/.pi/extensions/harness-plan-approval.ts +2 -2
  26. package/.pi/extensions/harness-run-context.ts +145 -5
  27. package/.pi/extensions/harness-subagents.ts +2 -2
  28. package/.pi/extensions/lib/harness-posthog.ts +6 -1
  29. package/.pi/extensions/lib/harness-spawn-budget.ts +75 -0
  30. package/.pi/extensions/lib/harness-subagent-auth.ts +123 -0
  31. package/.pi/extensions/lib/{harness-subagents/harness-subagent-policy.ts → harness-subagent-policy.ts} +3 -6
  32. package/.pi/extensions/lib/harness-subagent-precheck.ts +95 -0
  33. package/.pi/extensions/lib/harness-subagents-bridge.ts +176 -0
  34. package/.pi/extensions/lib/plan-approval/create-plan.ts +4 -7
  35. package/.pi/extensions/lib/plan-approval/plan-review.ts +1 -1
  36. package/.pi/extensions/lib/plan-approval/types.ts +7 -1
  37. package/.pi/extensions/lib/plan-debate-envelope.ts +84 -0
  38. package/.pi/extensions/lib/{harness-subagents/spawn-policy.ts → spawn-policy.ts} +1 -0
  39. package/.pi/extensions/policy-gate.ts +1 -1
  40. package/.pi/extensions/review-integrity.ts +48 -29
  41. package/.pi/harness/agents.manifest.json +37 -25
  42. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +4 -3
  43. package/.pi/harness/docs/adrs/0033-parent-orchestrated-planning.md +1 -1
  44. package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +27 -0
  45. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r1.yaml +25 -0
  46. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r4.yaml +26 -0
  47. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/sprint-audit-r4.yaml +5 -0
  48. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-packet.yaml +196 -0
  49. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/plan-review.md +14 -0
  50. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +32 -0
  51. package/.pi/harness/evals/smoke/run-context.fixture.json +1 -1
  52. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +88 -0
  53. package/.pi/harness/specs/harness-posthog-event.schema.json +6 -1
  54. package/.pi/harness/specs/plan-execution-plan-brief.schema.json +13 -0
  55. package/.pi/harness/specs/plan-execution-plan.schema.json +255 -0
  56. package/.pi/harness/specs/plan-packet.schema.json +14 -5
  57. package/.pi/harness/specs/plan-review-round-draft.schema.json +68 -0
  58. package/.pi/harness/specs/plan-sprint-audit-turn.schema.json +29 -0
  59. package/.pi/harness/specs/plan-stack-brief.schema.json +65 -0
  60. package/.pi/harness/specs/plan-validation-turn.schema.json +42 -0
  61. package/.pi/harness/specs/round-result.schema.json +16 -9
  62. package/.pi/lib/debate-orchestrator-types.ts +38 -0
  63. package/.pi/lib/harness-agent-discovery.mjs +81 -0
  64. package/.pi/lib/harness-run-context.ts +64 -38
  65. package/.pi/lib/harness-yaml.mjs +73 -0
  66. package/.pi/lib/harness-yaml.ts +90 -0
  67. package/.pi/prompts/harness-auto.md +13 -11
  68. package/.pi/prompts/harness-critic.md +2 -2
  69. package/.pi/prompts/harness-eval.md +3 -3
  70. package/.pi/prompts/harness-incident.md +2 -2
  71. package/.pi/prompts/harness-plan.md +79 -93
  72. package/.pi/prompts/harness-review.md +2 -2
  73. package/.pi/prompts/harness-router-tune.md +1 -1
  74. package/.pi/prompts/harness-run.md +2 -2
  75. package/.pi/prompts/harness-setup.md +15 -6
  76. package/.pi/prompts/harness-trace.md +2 -2
  77. package/.pi/scripts/harness-agents-manifest.mjs +1 -1
  78. package/.pi/scripts/harness-verify.mjs +28 -19
  79. package/.pi/scripts/validate-plan-dag.mjs +258 -0
  80. package/.pi/scripts/vendor-sync-pi-subagents.sh +19 -0
  81. package/CHANGELOG.md +12 -0
  82. package/THIRD_PARTY_NOTICES.md +8 -0
  83. package/biome.json +2 -2
  84. package/package.json +6 -4
  85. package/.pi/agents/harness/planner.md +0 -13
  86. package/.pi/agents/harness/planning/hypothesis-eval.md +0 -59
  87. package/.pi/agents/harness/planning/planner.md +0 -20
  88. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +0 -126
  89. package/.pi/extensions/lib/harness-subagents/agent-manifest.ts +0 -119
  90. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +0 -87
  91. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +0 -118
  92. package/.pi/extensions/lib/harness-subagents/blackboard.ts +0 -175
  93. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +0 -10
  94. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +0 -137
  95. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +0 -77
  96. package/.pi/extensions/lib/harness-subagents/types-blackboard.ts +0 -27
  97. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +0 -558
  98. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +0 -666
  99. package/.pi/extensions/lib/harness-subagents/vendored/agent-types.ts +0 -175
  100. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +0 -59
  101. package/.pi/extensions/lib/harness-subagents/vendored/cross-extension-rpc.ts +0 -134
  102. package/.pi/extensions/lib/harness-subagents/vendored/custom-agents.ts +0 -5
  103. package/.pi/extensions/lib/harness-subagents/vendored/default-agents.ts +0 -123
  104. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +0 -43
  105. package/.pi/extensions/lib/harness-subagents/vendored/group-join.ts +0 -144
  106. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +0 -2460
  107. package/.pi/extensions/lib/harness-subagents/vendored/invocation-config.ts +0 -52
  108. package/.pi/extensions/lib/harness-subagents/vendored/memory.ts +0 -182
  109. package/.pi/extensions/lib/harness-subagents/vendored/model-resolver.ts +0 -92
  110. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +0 -115
  111. package/.pi/extensions/lib/harness-subagents/vendored/prompts.ts +0 -103
  112. package/.pi/extensions/lib/harness-subagents/vendored/schedule-store.ts +0 -177
  113. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +0 -416
  114. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +0 -210
  115. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +0 -108
  116. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +0 -187
  117. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +0 -639
  118. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +0 -324
  119. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +0 -110
  120. package/.pi/extensions/lib/harness-subagents/vendored/usage.ts +0 -71
  121. package/.pi/extensions/lib/harness-subagents/vendored/worktree.ts +0 -195
  122. /package/.pi/extensions/{00-ultimate-pi-system-prompt.ts → custom-system-prompt.ts} +0 -0
@@ -0,0 +1,38 @@
1
+ /** Shared debate bus participant types (plan + post-execute). */
2
+
3
+ export type PostExecuteDebateParticipant =
4
+ | "EvaluatorAgent"
5
+ | "AdversaryAgent"
6
+ | "TieBreakerAgent";
7
+
8
+ export type PlanDebateParticipant =
9
+ | "PlanEvaluatorAgent"
10
+ | "PlanAdversaryAgent"
11
+ | "HypothesisValidatorAgent"
12
+ | "SprintContractAuditorAgent"
13
+ | "ReviewIntegratorAgent"
14
+ | "StackResearchAgent";
15
+
16
+ export type DebateParticipant =
17
+ | PostExecuteDebateParticipant
18
+ | PlanDebateParticipant;
19
+
20
+ export const PLAN_DEBATE_PARTICIPANTS: PlanDebateParticipant[] = [
21
+ "PlanEvaluatorAgent",
22
+ "PlanAdversaryAgent",
23
+ "HypothesisValidatorAgent",
24
+ "SprintContractAuditorAgent",
25
+ "ReviewIntegratorAgent",
26
+ "StackResearchAgent",
27
+ ];
28
+
29
+ export const POST_EXECUTE_DEBATE_PARTICIPANTS: PostExecuteDebateParticipant[] =
30
+ ["EvaluatorAgent", "AdversaryAgent", "TieBreakerAgent"];
31
+
32
+ export function isPlanDebateId(debateId: string): boolean {
33
+ return debateId.startsWith("plan-");
34
+ }
35
+
36
+ export function debatePhaseFromId(debateId: string): "plan" | "post_execute" {
37
+ return isPlanDebateId(debateId) ? "plan" : "post_execute";
38
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Shared agent discovery helpers (manifest + tests).
3
+ */
4
+
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
7
+ import { join, relative } from "node:path";
8
+
9
+ export function isSafeAgentId(id) {
10
+ if (!id || id.includes("..") || id.startsWith("/") || id.includes("\\")) {
11
+ return false;
12
+ }
13
+ return /^[a-zA-Z0-9][a-zA-Z0-9/_-]*$/.test(id);
14
+ }
15
+
16
+ export function sha256Content(content) {
17
+ return createHash("sha256").update(content, "utf8").digest("hex");
18
+ }
19
+
20
+ export function walkAgentsDir(rootDir, source, out) {
21
+ if (!existsSync(rootDir)) return;
22
+ const stack = [rootDir];
23
+ while (stack.length > 0) {
24
+ const dir = stack.pop();
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(dir, { withFileTypes: true });
28
+ } catch {
29
+ continue;
30
+ }
31
+ for (const entry of entries) {
32
+ const full = join(dir, entry.name);
33
+ if (entry.isDirectory()) {
34
+ stack.push(full);
35
+ continue;
36
+ }
37
+ if (!entry.name.endsWith(".md")) continue;
38
+ const rel = relative(rootDir, full).replace(/\\/g, "/");
39
+ const id = rel.replace(/\.md$/i, "");
40
+ if (!isSafeAgentId(id)) continue;
41
+ let content;
42
+ try {
43
+ content = readFileSync(full, "utf-8");
44
+ } catch {
45
+ continue;
46
+ }
47
+ out.set(id, { id, path: full, source, content });
48
+ }
49
+ }
50
+ }
51
+
52
+ export function discoverFromRoots(packageAgentsDir, projectAgentsDir, globalAgentsDir) {
53
+ const files = new Map();
54
+ walkAgentsDir(packageAgentsDir, "package", files);
55
+ if (globalAgentsDir) walkAgentsDir(globalAgentsDir, "global", files);
56
+ walkAgentsDir(projectAgentsDir, "project", files);
57
+ return files;
58
+ }
59
+
60
+ export function getDriftReport(manifest, onDiskHashes) {
61
+ const items = [];
62
+ if (!manifest) {
63
+ return { ok: false, items: [{ id: "*", kind: "missing_on_disk" }] };
64
+ }
65
+ for (const [id, entry] of onDiskHashes) {
66
+ const expected = manifest.agents[id];
67
+ if (!expected) {
68
+ items.push({ id, kind: "missing_in_manifest" });
69
+ continue;
70
+ }
71
+ if (expected.sha256 !== entry.sha256) {
72
+ items.push({ id, kind: "hash_mismatch" });
73
+ }
74
+ }
75
+ for (const id of Object.keys(manifest.agents)) {
76
+ if (!onDiskHashes.has(id)) {
77
+ items.push({ id, kind: "missing_on_disk" });
78
+ }
79
+ }
80
+ return { ok: items.length === 0, items };
81
+ }
@@ -2,12 +2,13 @@
2
2
  * harness-run-context — shared types and helpers for active harness runs.
3
3
  *
4
4
  * Session entry `harness-run-context` is the live source of truth; disk mirrors:
5
- * - `.pi/harness/runs/<run_id>/run-context.json`
5
+ * - `.pi/harness/runs/<run_id>/run-context.yaml`
6
6
  * - `.pi/harness/active-run.json` (cross-session pointer)
7
7
  */
8
8
 
9
9
  import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, relative, resolve } from "node:path";
11
+ import { readYamlFile, writeYamlFile } from "./harness-yaml.js";
11
12
 
12
13
  export type HarnessPhase =
13
14
  | "plan"
@@ -67,6 +68,7 @@ export interface PlanPacketLike {
67
68
  risk_level?: string;
68
69
  assumptions?: unknown[];
69
70
  rollback_plan?: unknown;
71
+ execution_plan?: unknown;
70
72
  }
71
73
 
72
74
  interface SessionEntryLike {
@@ -107,11 +109,22 @@ export function activeRunPointerPath(projectRoot: string): string {
107
109
  }
108
110
 
109
111
  export function runContextDiskPath(runId: string, projectRoot: string): string {
110
- return join(harnessRunsRoot(projectRoot), runId, "run-context.json");
112
+ return join(harnessRunsRoot(projectRoot), runId, RUN_CONTEXT_BASENAME);
111
113
  }
112
114
 
113
115
  export function canonicalPlanPath(runId: string, projectRoot: string): string {
114
- return join(harnessRunsRoot(projectRoot), runId, "plan-packet.json");
116
+ return join(harnessRunsRoot(projectRoot), runId, PLAN_PACKET_BASENAME);
117
+ }
118
+
119
+ export function canonicalResearchBriefPath(
120
+ runId: string,
121
+ projectRoot: string,
122
+ ): string {
123
+ return join(harnessRunsRoot(projectRoot), runId, RESEARCH_BRIEF_BASENAME);
124
+ }
125
+
126
+ export function runArtifactsDir(runId: string, projectRoot: string): string {
127
+ return join(harnessRunsRoot(projectRoot), runId, "artifacts");
115
128
  }
116
129
 
117
130
  export const PLAN_REVIEW_BASENAME = "plan-review.md";
@@ -123,7 +136,16 @@ export function canonicalPlanReviewPath(
123
136
  return join(harnessRunsRoot(projectRoot), runId, PLAN_REVIEW_BASENAME);
124
137
  }
125
138
 
126
- const PLAN_PACKET_BASENAME = "plan-packet.json";
139
+ export const PLAN_PACKET_BASENAME = "plan-packet.yaml";
140
+ export const RUN_CONTEXT_BASENAME = "run-context.yaml";
141
+ export const RESEARCH_BRIEF_BASENAME = "research-brief.yaml";
142
+
143
+ const PLAN_RUN_SCOPED_ROOT_FILES = new Set([
144
+ PLAN_PACKET_BASENAME,
145
+ RESEARCH_BRIEF_BASENAME,
146
+ "plan-dag-validation.yaml",
147
+ PLAN_REVIEW_BASENAME,
148
+ ]);
127
149
 
128
150
  const MUTATING_FILE_TOOLS = new Set(["write", "edit"]);
129
151
 
@@ -208,7 +230,21 @@ export function extractWritePathFromToolInput(
208
230
  return raw.trim();
209
231
  }
210
232
 
211
- /** True when absPath is the canonical plan-packet.json for the active run. */
233
+ /** True when absPath is a plan-phase artifact under the active run directory. */
234
+ export function isPlanRunScopedRelativePath(rel: string): boolean {
235
+ if (rel.startsWith("..") || isAbsolute(rel)) return false;
236
+ const parts = rel.split(/[/\\]/);
237
+ if (parts.length === 2 && PLAN_RUN_SCOPED_ROOT_FILES.has(parts[1])) {
238
+ return true;
239
+ }
240
+ if (parts.length === 3 && parts[1] === "artifacts") {
241
+ const file = parts[2];
242
+ return file.endsWith(".yaml") || file.endsWith(".yml");
243
+ }
244
+ return false;
245
+ }
246
+
247
+ /** True when absPath is a writable plan-run artifact for the active run. */
212
248
  export async function isPlanPhaseScopedWrite(
213
249
  absPath: string,
214
250
  runCtx: HarnessRunContext | null,
@@ -229,11 +265,9 @@ export async function isPlanPhaseScopedWrite(
229
265
  runsReal = runsRoot;
230
266
  }
231
267
  const rel = relative(runsReal, resolved);
232
- if (rel.startsWith("..") || isAbsolute(rel)) return false;
268
+ if (!isPlanRunScopedRelativePath(rel)) return false;
233
269
  const parts = rel.split(/[/\\]/);
234
- if (parts.length !== 2 || parts[1] !== PLAN_PACKET_BASENAME) return false;
235
- if (parts[0] !== runCtx.run_id) return false;
236
- return isCanonicalPlanPacketPath(resolved, projectRoot, runCtx.run_id);
270
+ return parts[0] === runCtx.run_id;
237
271
  }
238
272
 
239
273
  export function getLatestHarnessTurn(
@@ -497,19 +531,6 @@ export async function isPlanPhaseAllowedMutation(
497
531
  'policy-gate: no active harness run. Run /harness-plan "<task>" first.',
498
532
  };
499
533
  }
500
- if (
501
- !hasPlanUserApproval(opts.entries, {
502
- sincePlanCommand: true,
503
- planId: runCtx.plan_id,
504
- })
505
- ) {
506
- return {
507
- allowed: false,
508
- isScopedPlanWrite: true,
509
- reason:
510
- "policy-gate: plan-packet.json write blocked until the user approves via approve_plan or ask_user (present the full plan, then Approve).",
511
- };
512
- }
513
534
  if (opts.aborted) {
514
535
  return { allowed: true, isScopedPlanWrite: true };
515
536
  }
@@ -522,7 +543,7 @@ export async function isPlanPhaseAllowedMutation(
522
543
  return {
523
544
  allowed: false,
524
545
  isScopedPlanWrite: true,
525
- reason: `harness-run-context: plan-packet.json is read-only in phase '${phase}'.`,
546
+ reason: `harness-run-context: plan-packet.yaml is read-only in phase '${phase}'.`,
526
547
  };
527
548
  }
528
549
 
@@ -530,7 +551,7 @@ export async function isPlanPhaseAllowedMutation(
530
551
  return {
531
552
  allowed: false,
532
553
  reason:
533
- "policy-gate: mutating tool blocked because harness-abort lock is active. Attach a new approved plan via plan-packet.json first.",
554
+ "policy-gate: mutating tool blocked because harness-abort lock is active. Attach a new approved plan via plan-packet.yaml first.",
534
555
  };
535
556
  }
536
557
 
@@ -548,7 +569,7 @@ export async function isPlanPhaseAllowedMutation(
548
569
 
549
570
  const allowedPath = runCtx?.run_id
550
571
  ? canonicalPlanPath(runCtx.run_id, projectRoot)
551
- : ".pi/harness/runs/<run_id>/plan-packet.json";
572
+ : `.pi/harness/runs/<run_id>/${PLAN_PACKET_BASENAME}`;
552
573
  return {
553
574
  allowed: false,
554
575
  reason: `policy-gate: ${toolName} blocked in phase '${phase}'. In plan phase only ${allowedPath} is writable after ask_user approval.`,
@@ -754,8 +775,11 @@ export async function loadRunContextFromDisk(
754
775
  projectRoot: string,
755
776
  ): Promise<HarnessRunContext | null> {
756
777
  try {
757
- const raw = await readFile(runContextDiskPath(runId, projectRoot), "utf-8");
758
- return normalizeRunContext(JSON.parse(raw) as Partial<HarnessRunContext>);
778
+ const doc = await readYamlFile(
779
+ runContextDiskPath(runId, projectRoot),
780
+ "run-context",
781
+ );
782
+ return normalizeRunContext(doc as Partial<HarnessRunContext>);
759
783
  } catch {
760
784
  return null;
761
785
  }
@@ -766,11 +790,7 @@ export async function saveRunContextToDisk(
766
790
  ): Promise<void> {
767
791
  const dir = join(harnessRunsRoot(ctx.project_root), ctx.run_id);
768
792
  await mkdir(dir, { recursive: true });
769
- await writeFile(
770
- runContextDiskPath(ctx.run_id, ctx.project_root),
771
- `${JSON.stringify(ctx, null, 2)}\n`,
772
- "utf-8",
773
- );
793
+ await writeYamlFile(runContextDiskPath(ctx.run_id, ctx.project_root), ctx);
774
794
  }
775
795
 
776
796
  export async function loadProjectActiveRun(
@@ -828,8 +848,8 @@ export async function readPlanPacketFromPath(
828
848
  planPath: string,
829
849
  ): Promise<PlanPacketLike | null> {
830
850
  try {
831
- const raw = await readFile(planPath, "utf-8");
832
- return JSON.parse(raw) as PlanPacketLike;
851
+ const doc = await readYamlFile(planPath, planPath);
852
+ return doc as PlanPacketLike;
833
853
  } catch {
834
854
  return null;
835
855
  }
@@ -844,8 +864,11 @@ export function validatePlanPacket(packet: PlanPacketLike | null): {
844
864
  const errors: string[] = [];
845
865
  if (packet.schema_version !== "1.0.0")
846
866
  errors.push("schema_version must be 1.0.0");
847
- if (packet.contract_version !== "1.0.0")
848
- errors.push("contract_version must be 1.0.0");
867
+ if (
868
+ packet.contract_version !== "1.0.0" &&
869
+ packet.contract_version !== "1.1.0"
870
+ )
871
+ errors.push("contract_version must be 1.0.0 or 1.1.0");
849
872
  if (!packet.plan_id || typeof packet.plan_id !== "string")
850
873
  errors.push("plan_id required");
851
874
  if (!packet.task_id || typeof packet.task_id !== "string")
@@ -859,6 +882,9 @@ export function validatePlanPacket(packet: PlanPacketLike | null): {
859
882
  errors.push("acceptance_checks required");
860
883
  if (!packet.risk_level) errors.push("risk_level required");
861
884
  if (!packet.rollback_plan) errors.push("rollback_plan required");
885
+ if (packet.contract_version === "1.1.0" && !packet.execution_plan) {
886
+ errors.push("execution_plan required for contract_version 1.1.0");
887
+ }
862
888
  return { valid: errors.length === 0, errors };
863
889
  }
864
890
 
@@ -957,7 +983,7 @@ export function formatActivePlanBlock(
957
983
  );
958
984
  } else {
959
985
  lines.push(
960
- "Plan is read-only in this phase. Do not edit plan-packet.json.",
986
+ "Plan is read-only in this phase. Do not edit plan-packet.yaml.",
961
987
  );
962
988
  }
963
989
  if (ctx.plan_packet_path) {
@@ -1016,7 +1042,7 @@ export function validatePlanOverridePath(
1016
1042
  if (!isCanonicalPlanPacketPath(absPlan, projectRoot, runId)) {
1017
1043
  return {
1018
1044
  ok: false,
1019
- reason: `--plan must be runs/${runId}/plan-packet.json (canonical plan packet only)`,
1045
+ reason: `--plan must be runs/${runId}/${PLAN_PACKET_BASENAME} (canonical plan packet only)`,
1020
1046
  };
1021
1047
  }
1022
1048
  return { ok: true };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * YAML read/write for harness plan artifacts (no JSON plan fallbacks).
3
+ */
4
+
5
+ import { readFile, rename, writeFile } from "node:fs/promises";
6
+ import { parse, stringify } from "yaml";
7
+
8
+ const CODE_FENCE_RE = /^```(?:ya?ml|json)?\s*\n?([\s\S]*?)```\s*$/im;
9
+
10
+ export function stripYamlFences(text) {
11
+ return stripCodeFences(text);
12
+ }
13
+
14
+ export function stripCodeFences(text) {
15
+ const trimmed = text.trim();
16
+ const m = CODE_FENCE_RE.exec(trimmed);
17
+ return m ? m[1].trim() : trimmed;
18
+ }
19
+
20
+ export function parseStructuredDocument(text, label = "document") {
21
+ const body = stripCodeFences(text);
22
+ if (!body.trim()) {
23
+ throw new Error(`${label}: empty document`);
24
+ }
25
+
26
+ try {
27
+ const yamlDoc = parse(body, { uniqueKeys: true });
28
+ if (yamlDoc !== null && yamlDoc !== undefined) {
29
+ return yamlDoc;
30
+ }
31
+ } catch {
32
+ /* try JSON below */
33
+ }
34
+
35
+ const trimmed = body.trim();
36
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
37
+ try {
38
+ return JSON.parse(trimmed);
39
+ } catch (err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ throw new Error(`${label}: JSON parse failed — ${msg}`);
42
+ }
43
+ }
44
+
45
+ throw new Error(
46
+ `${label}: not valid YAML or JSON (use write_harness_yaml with a schema-shaped object)`,
47
+ );
48
+ }
49
+
50
+ export function parseYaml(text, label = "yaml") {
51
+ return parseStructuredDocument(text, label);
52
+ }
53
+
54
+ export async function readYamlFile(path, label) {
55
+ const raw = await readFile(path, "utf-8");
56
+ return parseStructuredDocument(raw, label ?? path);
57
+ }
58
+
59
+ export async function writeYamlFile(path, data) {
60
+ const tmp = `${path}.tmp`;
61
+ const content = `${stringify(data, { indent: 2 })}\n`;
62
+ await writeFile(tmp, content, "utf-8");
63
+ await rename(tmp, path);
64
+ }
65
+
66
+ export function stringifyYaml(data) {
67
+ return `${stringify(data, { indent: 2 })}\n`;
68
+ }
69
+
70
+ export function normalizeHarnessYamlContent(text, label = "yaml") {
71
+ const doc = parseStructuredDocument(text, label);
72
+ return stringifyYaml(doc);
73
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * YAML read/write for harness plan artifacts (no JSON plan fallbacks).
3
+ */
4
+
5
+ import { readFile, rename, writeFile } from "node:fs/promises";
6
+ import { parse, stringify } from "yaml";
7
+
8
+ const CODE_FENCE_RE = /^```(?:ya?ml|json)?\s*\n?([\s\S]*?)```\s*$/im;
9
+
10
+ /** @deprecated Use stripCodeFences */
11
+ export function stripYamlFences(text: string): string {
12
+ return stripCodeFences(text);
13
+ }
14
+
15
+ export function stripCodeFences(text: string): string {
16
+ const trimmed = text.trim();
17
+ const m = CODE_FENCE_RE.exec(trimmed);
18
+ return m ? m[1].trim() : trimmed;
19
+ }
20
+
21
+ /**
22
+ * Parse agent output or file body: fenced YAML/JSON, raw YAML, or raw JSON object/array.
23
+ */
24
+ export function parseStructuredDocument(
25
+ text: string,
26
+ label = "document",
27
+ ): unknown {
28
+ const body = stripCodeFences(text);
29
+ if (!body.trim()) {
30
+ throw new Error(`${label}: empty document`);
31
+ }
32
+
33
+ try {
34
+ const yamlDoc = parse(body, { uniqueKeys: true });
35
+ if (yamlDoc !== null && yamlDoc !== undefined) {
36
+ return yamlDoc;
37
+ }
38
+ } catch {
39
+ /* try JSON below */
40
+ }
41
+
42
+ const trimmed = body.trim();
43
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
44
+ try {
45
+ return JSON.parse(trimmed) as unknown;
46
+ } catch (err) {
47
+ const msg = err instanceof Error ? err.message : String(err);
48
+ throw new Error(`${label}: JSON parse failed — ${msg}`);
49
+ }
50
+ }
51
+
52
+ throw new Error(
53
+ `${label}: not valid YAML or JSON (use write_harness_yaml with a schema-shaped object)`,
54
+ );
55
+ }
56
+
57
+ export function parseYaml(text: string, label = "yaml"): unknown {
58
+ return parseStructuredDocument(text, label);
59
+ }
60
+
61
+ export async function readYamlFile(
62
+ path: string,
63
+ label?: string,
64
+ ): Promise<unknown> {
65
+ const raw = await readFile(path, "utf-8");
66
+ return parseStructuredDocument(raw, label ?? path);
67
+ }
68
+
69
+ export async function writeYamlFile(
70
+ path: string,
71
+ data: unknown,
72
+ ): Promise<void> {
73
+ const tmp = `${path}.tmp`;
74
+ const content = `${stringify(data, { indent: 2 })}\n`;
75
+ await writeFile(tmp, content, "utf-8");
76
+ await rename(tmp, path);
77
+ }
78
+
79
+ export function stringifyYaml(data: unknown): string {
80
+ return `${stringify(data, { indent: 2 })}\n`;
81
+ }
82
+
83
+ /** Normalize arbitrary agent text to canonical YAML file bytes. */
84
+ export function normalizeHarnessYamlContent(
85
+ text: string,
86
+ label = "yaml",
87
+ ): string {
88
+ const doc = parseStructuredDocument(text, label);
89
+ return stringifyYaml(doc);
90
+ }
@@ -5,7 +5,7 @@ argument-hint: "\"<task>\" [--quick] [--risk low|med|high] [--budget <amount>]"
5
5
 
6
6
  # harness-auto
7
7
 
8
- Pipeline orchestrator — one session, sequential `Agent` spawns. Invoke **harness-orchestration** skill for agent IDs. Do **not** implement or review inline.
8
+ Pipeline orchestrator — one session, sequential phase handoffs. Invoke **harness-orchestration** skill for agent IDs. Do **not** implement or review inline.
9
9
 
10
10
  ## Step 0 — Parse arguments
11
11
 
@@ -18,20 +18,22 @@ If task missing:
18
18
 
19
19
  ## Orchestration (required) — same session
20
20
 
21
- 1. **Plan** — follow `/harness-plan` parent orchestration (parallel `harness/planning/scout-*`, `decompose`, `hypothesis`, draft PlanPacket, `ask_user` on fork, parallel `plan-adversary` + `hypothesis-eval`, parent `approve_plan` + `create_plan`). Do not spawn `harness/planner`. No second approval pass.
22
- 2. **Execute** — spawn `harness/executor` with `HarnessSpawnContext` (`mode: execute`). Summarize handoff bullets for next spawn (do not paste full subagent log).
23
- 3. **Eval** — spawn `harness/evaluator` (`mode: benchmark`) after parent scripts if needed.
24
- 4. **Review** — spawn `harness/evaluator` (`mode: verdict`) OR rely on eval verdict if policy allows prefer both when strict gates require.
25
- 5. **Adversary** — spawn `harness/adversary` with artifact paths.
26
- 6. **Tie-breaker** — spawn `harness/tie-breaker` only if debate unresolved.
21
+ Follow **harness-plan** performance rules (`subagent` with parallel `tasks`, `agentScope: "both"`).
22
+
23
+ 1. **Plan** — follow `/harness-plan` (parallel scouts → parallel decompose/hypothesis → draft PlanPacket → debate rounds → parent `approve_plan` + `create_plan`). No second approval pass.
24
+ 2. **Execute** — `subagent({ agent: "harness/executor", task: "<HarnessSpawnContext mode execute>" })`; summarize handoff bullets only (do not paste full subprocess log).
25
+ 3. **Eval** — `subagent({ agent: "harness/evaluator", task: "<mode benchmark>" })` after parent scripts if needed.
26
+ 4. **Review** — `subagent({ agent: "harness/evaluator", task: "<mode verdict>" })` when strict gates require.
27
+ 5. **Adversary** — `subagent({ agent: "harness/adversary", ... })`. **Skip when `--quick`**.
28
+ 6. **Tie-breaker** — `subagent({ agent: "harness/tie-breaker", ... })` only if debate unresolved and **not** `--quick`.
27
29
  7. **Parent** — apply locked strict gates below; commit/PR only if all pass.
28
30
 
29
- No new Pi session for review subagents use isolated context (`inherit_context: false`).
31
+ Review agents run in isolated subprocesses via `subagent` (same parent session).
30
32
 
31
33
  ## Locked decisions (do not change)
32
34
 
33
35
  - Always produce and approve plan before mutation.
34
- - Adversarial review always required.
36
+ - Adversarial review always required **except** `--quick` (evaluator-only gate).
35
37
  - Severity-policy-engine blocks merge.
36
38
  - Router tuning propose-and-approve only.
37
39
  - Plan ambiguity → parent `ask_user` (harness-decisions).
@@ -41,11 +43,11 @@ No new Pi session for review — subagents use isolated context (`inherit_contex
41
43
 
42
44
  ## Strict gates
43
45
 
44
- Block commit/PR if any fails: plan gate, execution in scope, evaluator pass, adversary complete, severity-policy pass/conditional_pass, benchmark deltas, rollback artifacts.
46
+ Block commit/PR if any fails: plan gate, execution in scope, evaluator pass, adversary complete (unless `--quick`), severity-policy pass/conditional_pass, benchmark deltas, rollback artifacts.
45
47
 
46
48
  ## Notes
47
49
 
48
- - `--quick` reduces breadth, never safety gates.
50
+ - `--quick` reduces breadth (skips semantic scout, post-run adversary, tie-breaker), never core safety gates on plan approval or evaluator.
49
51
  - High risk/ambiguity → stop and recommend manual `/harness-plan` with `ask_user`.
50
52
  - Interrupt: `/harness-abort [reason]` then `/harness-plan`.
51
53
  - Artifact refs under active run dir; `/harness-run-status` or `/harness-trace-last` for handoff.
@@ -20,10 +20,10 @@ Happy path: omit `--run`.
20
20
  2. Spawn:
21
21
 
22
22
  ```
23
- Agent({ subagent_type: "harness/adversary", prompt: "…" })
23
+ subagent({ agentScope: "both", agent: "harness/adversary", task: "…" })
24
24
  ```
25
25
 
26
- 3. `get_subagent_result` parse `AdversaryReport` JSON; parent persists for severity policy.
26
+ 3. Parse `AdversaryReport` JSON from tool result; parent persists for severity policy.
27
27
 
28
28
  ## Parent rules
29
29
 
@@ -26,11 +26,11 @@ If no active run:
26
26
  4. Spawn:
27
27
 
28
28
  ```
29
- Agent({ subagent_type: "harness/evaluator", prompt: "" })
29
+ subagent({ agentScope: "both", agent: "harness/evaluator", task: "<HarnessSpawnContext + eval brief>" })
30
30
  ```
31
31
 
32
- 5. `get_subagent_result` parse eval JSON; parent writes structured artifacts under run dir.
33
- 6. Do not edit `plan-packet.json`.
32
+ 5. Parse eval JSON from tool result; parent writes structured artifacts under run dir.
33
+ 6. Do not edit `plan-packet.yaml`.
34
34
 
35
35
  ## Parent rules
36
36
 
@@ -22,10 +22,10 @@ If `--trigger` missing:
22
22
  2. Spawn:
23
23
 
24
24
  ```
25
- Agent({ subagent_type: "harness/incident-recorder", prompt: "…" })
25
+ subagent({ agentScope: "both", agent: "harness/incident-recorder", task: "…" })
26
26
  ```
27
27
 
28
- 3. `get_subagent_result` validate `IncidentRecord` draft; parent writes under `.pi/harness/incidents/`.
28
+ 3. Parse `IncidentRecord` JSON from tool result; parent writes under `.pi/harness/incidents/`.
29
29
 
30
30
  ## Completion
31
31