ultimate-pi 0.16.0 → 0.18.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 (137) hide show
  1. package/.agents/skills/harness-context/SKILL.md +13 -6
  2. package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
  3. package/.agents/skills/harness-eval/SKILL.md +6 -21
  4. package/.agents/skills/harness-governor/SKILL.md +4 -3
  5. package/.agents/skills/harness-orchestration/SKILL.md +39 -51
  6. package/.agents/skills/harness-plan/SKILL.md +23 -12
  7. package/.agents/skills/harness-review/SKILL.md +52 -0
  8. package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
  9. package/.agents/skills/harness-steer/SKILL.md +14 -0
  10. package/.pi/agents/harness/adversary.md +3 -10
  11. package/.pi/agents/harness/evaluator.md +3 -12
  12. package/.pi/agents/harness/executor.md +12 -14
  13. package/.pi/agents/harness/planning/decompose.md +7 -4
  14. package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
  15. package/.pi/agents/harness/planning/hypothesis.md +4 -2
  16. package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
  17. package/.pi/agents/harness/planning/plan-adversary.md +2 -0
  18. package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
  19. package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
  20. package/.pi/agents/harness/planning/planning-context.md +48 -0
  21. package/.pi/agents/harness/planning/review-integrator.md +2 -0
  22. package/.pi/agents/harness/planning/scout-graphify.md +3 -1
  23. package/.pi/agents/harness/planning/scout-semantic.md +3 -1
  24. package/.pi/agents/harness/planning/scout-structure.md +3 -1
  25. package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
  26. package/.pi/agents/harness/sentrux-steward.md +51 -0
  27. package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
  28. package/.pi/extensions/harness-debate-tools.ts +12 -3
  29. package/.pi/extensions/harness-live-widget.ts +27 -1
  30. package/.pi/extensions/harness-plan-approval.ts +62 -56
  31. package/.pi/extensions/harness-run-context.ts +553 -84
  32. package/.pi/extensions/harness-subagent-submit.ts +43 -33
  33. package/.pi/extensions/harness-telemetry.ts +29 -4
  34. package/.pi/extensions/lib/debate-bus-core.ts +15 -9
  35. package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
  36. package/.pi/extensions/lib/harness-posthog.ts +9 -5
  37. package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
  38. package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
  39. package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
  40. package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
  41. package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
  42. package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
  43. package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
  44. package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
  45. package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
  46. package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
  47. package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
  48. package/.pi/extensions/lib/plan-approval/types.ts +1 -1
  49. package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
  50. package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
  51. package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
  52. package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
  53. package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
  54. package/.pi/extensions/lib/plan-debate-lanes.ts +57 -3
  55. package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
  56. package/.pi/extensions/lib/plan-messenger.ts +4 -0
  57. package/.pi/extensions/lib/plan-review-gate.ts +59 -0
  58. package/.pi/extensions/lib/posthog-client.ts +76 -0
  59. package/.pi/extensions/policy-gate.ts +24 -19
  60. package/.pi/extensions/trace-recorder.ts +1 -0
  61. package/.pi/harness/agents.manifest.json +24 -16
  62. package/.pi/harness/corpus/cron.example +8 -0
  63. package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
  64. package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
  65. package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
  66. package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
  67. package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
  68. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
  69. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
  70. package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
  71. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
  72. package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
  73. package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
  74. package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
  75. package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
  76. package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
  77. package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
  78. package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
  79. package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
  80. package/.pi/harness/docs/adrs/README.md +10 -0
  81. package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
  82. package/.pi/harness/docs/practice-map.md +110 -0
  83. package/.pi/harness/env.harness.template +5 -3
  84. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
  85. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
  86. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
  87. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
  88. package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
  89. package/.pi/harness/evals/smoke/sentrux-stub.json +1 -1
  90. package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
  91. package/.pi/harness/specs/README.md +1 -1
  92. package/.pi/harness/specs/harness-run-context.schema.json +11 -0
  93. package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
  94. package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
  95. package/.pi/harness/specs/plan-packet.schema.json +4 -0
  96. package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
  97. package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
  98. package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
  99. package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
  100. package/.pi/harness/specs/repair-brief.schema.json +45 -0
  101. package/.pi/harness/specs/review-outcome.schema.json +46 -0
  102. package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
  103. package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
  104. package/.pi/harness/specs/steer-state.schema.json +20 -0
  105. package/.pi/lib/harness-context-mode-policy.ts +256 -0
  106. package/.pi/lib/harness-repair-brief.ts +145 -0
  107. package/.pi/lib/harness-run-context.ts +591 -32
  108. package/.pi/lib/harness-ui-state.ts +87 -9
  109. package/.pi/model-router.example.json +13 -4
  110. package/.pi/prompts/harness-auto.md +9 -9
  111. package/.pi/prompts/harness-critic.md +3 -30
  112. package/.pi/prompts/harness-eval.md +4 -37
  113. package/.pi/prompts/harness-plan.md +139 -57
  114. package/.pi/prompts/harness-review.md +150 -15
  115. package/.pi/prompts/harness-run.md +62 -10
  116. package/.pi/prompts/harness-sentrux-steward.md +55 -0
  117. package/.pi/prompts/harness-setup.md +4 -4
  118. package/.pi/prompts/harness-steer.md +30 -0
  119. package/.pi/scripts/graphify-kb-updater.mjs +358 -0
  120. package/.pi/scripts/harness-generate-model-router.mjs +118 -36
  121. package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
  122. package/.pi/scripts/harness-sync-model-router.mjs +15 -2
  123. package/.pi/scripts/harness-verify.mjs +51 -6
  124. package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
  125. package/.pi/scripts/validate-plan-dag.mjs +3 -3
  126. package/AGENTS.md +1 -0
  127. package/CHANGELOG.md +22 -0
  128. package/package.json +5 -4
  129. package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
  130. package/vendor/pi-model-router/extensions/commands.ts +4 -4
  131. package/vendor/pi-model-router/extensions/index.ts +21 -0
  132. package/vendor/pi-model-router/extensions/provider.ts +130 -79
  133. package/vendor/pi-model-router/extensions/routing.ts +148 -0
  134. package/vendor/pi-model-router/extensions/state.ts +3 -0
  135. package/vendor/pi-model-router/extensions/types.ts +9 -0
  136. package/vendor/pi-model-router/extensions/ui.ts +16 -2
  137. package/.pi/prompts/git-sync.md +0 -124
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Seed harness-run-context + policy-gate session entries in subagent subprocesses.
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import {
10
+ getLatestRunContext,
11
+ type HarnessRunContext,
12
+ isHarnessSubprocess,
13
+ loadRunContextForSubprocess,
14
+ nowIso,
15
+ policyBootstrapFromRunContext,
16
+ } from "../../lib/harness-run-context.js";
17
+
18
+ type PolicyState = {
19
+ phase: "plan" | "execute" | "evaluate" | "adversary" | "merge";
20
+ approvedPlan: boolean;
21
+ planId: string | null;
22
+ budgetBypass: boolean;
23
+ aborted: boolean;
24
+ abortReason: string | null;
25
+ abortedAt: string | null;
26
+ updatedAt: string;
27
+ };
28
+
29
+ function defaultPolicyState(): PolicyState {
30
+ return {
31
+ phase: "plan",
32
+ approvedPlan: false,
33
+ planId: null,
34
+ budgetBypass: false,
35
+ aborted: false,
36
+ abortReason: null,
37
+ abortedAt: null,
38
+ updatedAt: nowIso(),
39
+ };
40
+ }
41
+
42
+ /** Append disk-backed run + policy entries when subprocess has no session context yet. */
43
+ export async function bootstrapHarnessSubprocessFromEnv(
44
+ pi: ExtensionAPI,
45
+ ctx: ExtensionContext,
46
+ ): Promise<HarnessRunContext | null> {
47
+ if (!isHarnessSubprocess()) return null;
48
+ const entries = ctx.sessionManager.getEntries();
49
+ if (getLatestRunContext(entries)) return getLatestRunContext(entries);
50
+
51
+ const projectRoot = ctx.cwd;
52
+ const sessionId = ctx.sessionManager.getSessionId();
53
+ const disk = await loadRunContextForSubprocess(projectRoot);
54
+ if (!disk?.plan_ready) return null;
55
+
56
+ const runCtx: HarnessRunContext = {
57
+ ...disk,
58
+ pi_session_id: sessionId,
59
+ };
60
+ pi.appendEntry("harness-run-context", runCtx);
61
+
62
+ const boot = policyBootstrapFromRunContext(runCtx);
63
+ const policy: PolicyState = {
64
+ ...defaultPolicyState(),
65
+ phase: boot.phase,
66
+ approvedPlan: boot.approvedPlan,
67
+ planId: boot.planId,
68
+ updatedAt: nowIso(),
69
+ };
70
+ pi.appendEntry("harness-policy-state", policy);
71
+
72
+ return runCtx;
73
+ }
@@ -12,12 +12,11 @@ import {
12
12
  import { writeYamlFile } from "../../../lib/harness-yaml.js";
13
13
  import { writePlanReviewMarkdown } from "./plan-review.js";
14
14
 
15
- export const CREATE_PLAN_SNIPPET =
16
- "create_plan({ plan_packet: { ...approved PlanPacket } })";
15
+ export const CREATE_PLAN_SNIPPET = "create_plan()";
17
16
 
18
17
  export const CREATE_PLAN_GUIDELINES = [
19
18
  "Call create_plan only after the user approves via approve_plan (Approve selection).",
20
- "Pass the same plan_packet you showed in approve_plan — path is resolved automatically.",
19
+ "Uses plan-packet.yaml on disk at plan_packet_path (path-first; no inline packet).",
21
20
  "Never use write or edit for plan-packet.yaml; create_plan is the only allowed plan write.",
22
21
  ];
23
22
 
@@ -0,0 +1,102 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ canonicalPlanPath,
4
+ getLatestRunContext,
5
+ harnessRunsRoot,
6
+ type PlanPacketLike,
7
+ RESEARCH_BRIEF_BASENAME,
8
+ readPlanPacketFromPath,
9
+ validatePlanPacket,
10
+ } from "../../../lib/harness-run-context.js";
11
+ import { readYamlFile } from "../../../lib/harness-yaml.js";
12
+ import type { ApprovePlanParams, PlanResearchBrief } from "./types.js";
13
+
14
+ function isNonEmptyPacket(
15
+ packet: PlanPacketLike | null | undefined,
16
+ ): packet is PlanPacketLike {
17
+ return Boolean(
18
+ packet &&
19
+ typeof packet === "object" &&
20
+ Object.keys(packet).length > 0 &&
21
+ packet.plan_id,
22
+ );
23
+ }
24
+
25
+ export async function loadResearchBriefFromRun(
26
+ runId: string,
27
+ projectRoot: string,
28
+ ): Promise<PlanResearchBrief | undefined> {
29
+ try {
30
+ const path = join(
31
+ harnessRunsRoot(projectRoot),
32
+ runId,
33
+ RESEARCH_BRIEF_BASENAME,
34
+ );
35
+ return (await readYamlFile(
36
+ path,
37
+ RESEARCH_BRIEF_BASENAME,
38
+ )) as PlanResearchBrief;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ /** Path-first approve_plan: load packet + research brief from active run dir. */
45
+ export async function resolveApprovePlanParamsFromDisk(
46
+ params: ApprovePlanParams,
47
+ entries: unknown[],
48
+ projectRoot: string,
49
+ ): Promise<
50
+ | {
51
+ ok: true;
52
+ plan_packet: PlanPacketLike;
53
+ research_brief?: PlanResearchBrief;
54
+ }
55
+ | { ok: false; error: string }
56
+ > {
57
+ const inline = params.plan_packet;
58
+ if (isNonEmptyPacket(inline)) {
59
+ const validation = validatePlanPacket(inline);
60
+ if (!validation.valid) {
61
+ return {
62
+ ok: false,
63
+ error: `approve_plan: invalid plan_packet — ${validation.errors.join("; ")}`,
64
+ };
65
+ }
66
+ return {
67
+ ok: true,
68
+ plan_packet: inline,
69
+ research_brief: params.research_brief ?? undefined,
70
+ };
71
+ }
72
+
73
+ const runCtx = getLatestRunContext(entries);
74
+ if (!runCtx?.run_id) {
75
+ return {
76
+ ok: false,
77
+ error:
78
+ 'approve_plan: no active harness run. Run /harness-plan "<task>" first.',
79
+ };
80
+ }
81
+ const planPath =
82
+ runCtx.plan_packet_path ?? canonicalPlanPath(runCtx.run_id, projectRoot);
83
+ const packet = await readPlanPacketFromPath(planPath);
84
+ if (!isNonEmptyPacket(packet)) {
85
+ return {
86
+ ok: false,
87
+ error:
88
+ "approve_plan: plan_packet missing on disk. Write plan-packet.yaml draft before approve_plan.",
89
+ };
90
+ }
91
+ const validation = validatePlanPacket(packet);
92
+ if (!validation.valid) {
93
+ return {
94
+ ok: false,
95
+ error: `approve_plan: invalid plan_packet on disk — ${validation.errors.join("; ")}`,
96
+ };
97
+ }
98
+ const research_brief =
99
+ params.research_brief ??
100
+ (await loadResearchBriefFromRun(runCtx.run_id, projectRoot));
101
+ return { ok: true, plan_packet: packet, research_brief };
102
+ }
@@ -1,12 +1,14 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
 
3
3
  export const ApprovePlanParamsSchema = Type.Object({
4
- plan_packet: Type.Object(
5
- {},
6
- {
7
- description:
8
- "Full PlanPacket object (schema_version, plan_id, task_id, scope, assumptions, risk_level, acceptance_checks, rollback_plan).",
9
- },
4
+ plan_packet: Type.Optional(
5
+ Type.Object(
6
+ {},
7
+ {
8
+ description:
9
+ "Optional inline PlanPacket (deprecated). Default: read plan-packet.yaml from active run (ADR 0043).",
10
+ },
11
+ ),
10
12
  ),
11
13
  human_summary: Type.Optional(
12
14
  Type.String({
@@ -45,10 +47,22 @@ export const ApprovePlanParamsSchema = Type.Object({
45
47
  });
46
48
 
47
49
  export const PROMPT_SNIPPET =
48
- "approve_plan({ plan_packet: { ...PlanPacket fields... }, human_summary?: string, research_brief?: { decomposition, hypothesis, eval } })";
50
+ "approve_plan({ human_summary?: string }) loads plan-packet.yaml from active run";
49
51
 
50
52
  export const PROMPT_GUIDELINES = [
51
- "Call approve_plan once with the complete plan_packet when ready for user approval.",
53
+ "Call approve_plan once when plan-packet.yaml is on disk (path-first; do not embed full packet in tool args).",
52
54
  "Use ask_user only for clarification — not for final plan approval.",
53
55
  "On Request changes, revise the plan and call approve_plan again.",
54
56
  ];
57
+
58
+ export const CreatePlanParamsSchema = Type.Object({
59
+ plan_packet: Type.Optional(
60
+ Type.Object(
61
+ {},
62
+ {
63
+ description:
64
+ "Optional inline packet (deprecated). Default: read approved plan from plan_packet_path.",
65
+ },
66
+ ),
67
+ ),
68
+ });
@@ -22,7 +22,7 @@ export interface PlanResearchBrief {
22
22
  }
23
23
 
24
24
  export interface ApprovePlanParams {
25
- plan_packet: PlanPacketLike;
25
+ plan_packet?: PlanPacketLike;
26
26
  human_summary?: string;
27
27
  research_brief?: PlanResearchBrief | null;
28
28
  options?: Array<string | { title: string; description?: string }>;
@@ -15,8 +15,8 @@ export function validateApprovePlanParams(
15
15
  params: ApprovePlanParams,
16
16
  ): ValidatedApprovePlanParams | string {
17
17
  const packet = params.plan_packet;
18
- if (!packet || typeof packet !== "object") {
19
- return "approve_plan: plan_packet object is required.";
18
+ if (!packet || typeof packet !== "object" || !packet.plan_id) {
19
+ return "approve_plan: plan_packet must be resolved from disk before validate (use resolveApprovePlanParamsFromDisk).";
20
20
  }
21
21
  const validation = validatePlanPacket(packet as PlanPacketLike);
22
22
  if (!validation.valid) {
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Pre-approve_plan readiness checks (artifacts, scouts, phase status).
3
+ */
4
+
5
+ import { constants } from "node:fs";
6
+ import { access, readFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { parse as parseYaml } from "yaml";
9
+
10
+ export interface PlanApprovalReadiness {
11
+ ok: boolean;
12
+ errors: string[];
13
+ warnings: string[];
14
+ }
15
+
16
+ const LEGACY_SCOUT_ARTIFACTS = [
17
+ "artifacts/scout-graphify.yaml",
18
+ "artifacts/scout-structure.yaml",
19
+ "artifacts/scout-semantic.yaml",
20
+ ] as const;
21
+
22
+ const PLANNING_CONTEXT_ARTIFACT = "artifacts/planning-context.yaml";
23
+
24
+ const PHASE35_ARTIFACTS = [
25
+ "artifacts/implementation-research.yaml",
26
+ "artifacts/stack.yaml",
27
+ ] as const;
28
+
29
+ async function fileExists(path: string): Promise<boolean> {
30
+ try {
31
+ await access(path, constants.R_OK);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async function readYamlObject(
39
+ path: string,
40
+ ): Promise<Record<string, unknown> | null> {
41
+ try {
42
+ const raw = await readFile(path, "utf-8");
43
+ const doc = parseYaml(raw) as unknown;
44
+ return doc && typeof doc === "object" && !Array.isArray(doc)
45
+ ? (doc as Record<string, unknown>)
46
+ : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ async function hasPhaseWaiver(
53
+ runDir: string,
54
+ reason: string,
55
+ ): Promise<boolean> {
56
+ const path = join(runDir, "artifacts", "plan-phase-waiver.yaml");
57
+ const doc = await readYamlObject(path);
58
+ if (!doc) return false;
59
+ const waived = doc.waived as unknown;
60
+ if (!Array.isArray(waived)) return false;
61
+ return waived.some((w) => {
62
+ if (!w || typeof w !== "object") return false;
63
+ const entry = w as Record<string, unknown>;
64
+ return String(entry.reason ?? "") === reason;
65
+ });
66
+ }
67
+
68
+ function artifactStatusBad(
69
+ doc: Record<string, unknown> | null,
70
+ label: string,
71
+ ): string | null {
72
+ const status = String(doc?.status ?? "ok").toLowerCase();
73
+ if (status === "partial" || status === "failed" || status === "error") {
74
+ return `${label}: status "${status}" without waiver`;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function coverageLaneStatus(
80
+ doc: Record<string, unknown> | null,
81
+ lane: string,
82
+ ): string {
83
+ const coverage = doc?.coverage as Record<string, unknown> | undefined;
84
+ if (!coverage || typeof coverage !== "object") return "";
85
+ const laneDoc = coverage[lane] as Record<string, unknown> | undefined;
86
+ return String(laneDoc?.status ?? "").toLowerCase();
87
+ }
88
+
89
+ async function validateLegacyScouts(
90
+ runDir: string,
91
+ quick: boolean,
92
+ errors: string[],
93
+ warnings: string[],
94
+ ): Promise<boolean> {
95
+ let anyPresent = false;
96
+ for (const rel of LEGACY_SCOUT_ARTIFACTS) {
97
+ if (rel === "artifacts/scout-semantic.yaml" && quick) continue;
98
+ const abs = join(runDir, rel);
99
+ if (!(await fileExists(abs))) {
100
+ const waived = await hasPhaseWaiver(runDir, `missing:${rel}`);
101
+ if (!waived) {
102
+ errors.push(`missing ${rel}`);
103
+ }
104
+ continue;
105
+ }
106
+ anyPresent = true;
107
+ const doc = await readYamlObject(abs);
108
+ const bad = artifactStatusBad(doc, rel);
109
+ if (bad) {
110
+ const waived = await hasPhaseWaiver(
111
+ runDir,
112
+ `scout:${rel}:${String(doc?.status ?? "")}`,
113
+ );
114
+ if (!waived) {
115
+ errors.push(bad);
116
+ }
117
+ }
118
+ }
119
+ if (anyPresent) {
120
+ warnings.push(
121
+ "legacy scout YAML artifacts detected — prefer artifacts/planning-context.yaml (see ADR 0041)",
122
+ );
123
+ }
124
+ return anyPresent;
125
+ }
126
+
127
+ async function validatePlanningContext(
128
+ runDir: string,
129
+ quick: boolean,
130
+ errors: string[],
131
+ ): Promise<boolean> {
132
+ const rel = PLANNING_CONTEXT_ARTIFACT;
133
+ const abs = join(runDir, rel);
134
+ if (!(await fileExists(abs))) {
135
+ return false;
136
+ }
137
+ const doc = await readYamlObject(abs);
138
+ const bad = artifactStatusBad(doc, rel);
139
+ if (bad) {
140
+ const waived = await hasPhaseWaiver(
141
+ runDir,
142
+ `planning-context:${String(doc?.status ?? "")}`,
143
+ );
144
+ if (!waived) {
145
+ errors.push(bad);
146
+ }
147
+ }
148
+ const arch = coverageLaneStatus(doc, "architecture");
149
+ const structure = coverageLaneStatus(doc, "structure");
150
+ if (arch !== "ok" && arch !== "partial") {
151
+ errors.push(
152
+ `${rel}: coverage.architecture.status must be ok or partial (got "${arch || "missing"}")`,
153
+ );
154
+ }
155
+ if (structure !== "ok" && structure !== "partial") {
156
+ errors.push(
157
+ `${rel}: coverage.structure.status must be ok or partial (got "${structure || "missing"}")`,
158
+ );
159
+ }
160
+ if (!quick) {
161
+ const semantic = coverageLaneStatus(doc, "semantic");
162
+ if (
163
+ semantic &&
164
+ semantic !== "ok" &&
165
+ semantic !== "partial" &&
166
+ semantic !== "skipped"
167
+ ) {
168
+ errors.push(
169
+ `${rel}: coverage.semantic.status must be ok, partial, or skipped (got "${semantic}")`,
170
+ );
171
+ }
172
+ }
173
+ return true;
174
+ }
175
+
176
+ export async function validatePlanApprovalReadiness(
177
+ projectRoot: string,
178
+ runId: string,
179
+ opts?: { risk_level?: string; quick?: boolean },
180
+ ): Promise<PlanApprovalReadiness> {
181
+ const runDir = join(projectRoot, ".pi", "harness", "runs", runId);
182
+ const errors: string[] = [];
183
+ const warnings: string[] = [];
184
+ const risk = String(opts?.risk_level ?? "med").toLowerCase();
185
+ const quick = opts?.quick === true;
186
+
187
+ const statusPath = join(runDir, "artifacts", "plan-phase-status.yaml");
188
+ const statusDoc = await readYamlObject(statusPath);
189
+ if (statusDoc) {
190
+ const planStatus = String(statusDoc.plan_status ?? "").toLowerCase();
191
+ if (planStatus === "partial" || planStatus === "needs_clarification") {
192
+ const waived = await hasPhaseWaiver(runDir, `plan_status:${planStatus}`);
193
+ if (!waived) {
194
+ errors.push(
195
+ `plan phase status is "${planStatus}" — resolve gaps, set plan_status ready, or write artifacts/plan-phase-waiver.yaml`,
196
+ );
197
+ }
198
+ }
199
+ }
200
+
201
+ const hasPlanningContext = await validatePlanningContext(
202
+ runDir,
203
+ quick,
204
+ errors,
205
+ );
206
+ const hasLegacyScouts = hasPlanningContext
207
+ ? false
208
+ : await validateLegacyScouts(runDir, quick, errors, warnings);
209
+
210
+ if (!hasPlanningContext && !hasLegacyScouts) {
211
+ const waived = await hasPhaseWaiver(
212
+ runDir,
213
+ "missing:planning-reconnaissance",
214
+ );
215
+ if (!waived) {
216
+ errors.push(
217
+ `missing ${PLANNING_CONTEXT_ARTIFACT} (or legacy scout-graphify/structure/semantic trio)`,
218
+ );
219
+ }
220
+ }
221
+
222
+ for (const rel of PHASE35_ARTIFACTS) {
223
+ const abs = join(runDir, rel);
224
+ if (!(await fileExists(abs))) {
225
+ if (risk === "high" || risk === "med") {
226
+ errors.push(`missing ${rel} (Phase 3.5 required for risk ${risk})`);
227
+ } else {
228
+ warnings.push(`missing ${rel} (recommended for risk ${risk})`);
229
+ }
230
+ }
231
+ }
232
+
233
+ if (!(await fileExists(join(runDir, "artifacts/decomposition.yaml")))) {
234
+ errors.push("missing artifacts/decomposition.yaml");
235
+ }
236
+ if (!(await fileExists(join(runDir, "artifacts/hypothesis.yaml")))) {
237
+ errors.push("missing artifacts/hypothesis.yaml");
238
+ }
239
+
240
+ return { ok: errors.length === 0, errors, warnings };
241
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { PLAN_FOCUS_AREAS, type PlanDebateFocus } from "./plan-debate-focus.js";
6
6
 
7
- export type DebateProfile = "full" | "standard" | "light";
7
+ export type DebateProfile = "full" | "standard" | "light" | "fast";
8
8
 
9
9
  export interface DebateEligibilityInput {
10
10
  risk_level?: string;
@@ -26,6 +26,7 @@ export interface DebateEligibilityResult {
26
26
  debate_global_cap: number;
27
27
  human_required: boolean;
28
28
  rationale: string[];
29
+ review_gate_strategy: PlanReviewGateStrategy;
29
30
  }
30
31
 
31
32
  const LIGHT_FOCUS: PlanDebateFocus[] = ["spec", "quality"];
@@ -75,7 +76,7 @@ function confidenceAllowsLight(brief: Record<string, unknown> | null): boolean {
75
76
  if (!rationale || refs.length < 2) return false;
76
77
  if (implementationOpenQuestions(brief).length > 0) return false;
77
78
  const patterns = Array.isArray(brief?.solution_patterns)
78
- ? (brief!.solution_patterns as unknown[])
79
+ ? (brief?.solution_patterns as unknown[])
79
80
  : [];
80
81
  for (const p of patterns) {
81
82
  const pat = asRecord(p);
@@ -85,7 +86,7 @@ function confidenceAllowsLight(brief: Record<string, unknown> | null): boolean {
85
86
  }
86
87
  }
87
88
  const similar = Array.isArray(brief?.similar_implementations)
88
- ? (brief!.similar_implementations as unknown[])
89
+ ? (brief?.similar_implementations as unknown[])
89
90
  : [];
90
91
  if (similar.length === 0) return false;
91
92
  return true;
@@ -116,17 +117,46 @@ export const PLAN_BUDGET_LIGHT = {
116
117
  debate_global_cap: 40000,
117
118
  } as const;
118
119
 
120
+ export const PLAN_BUDGET_FAST = {
121
+ min_focus_rounds: 1,
122
+ max_rounds: 2,
123
+ max_exchanges_per_round: 1,
124
+ round_token_cap: 3500,
125
+ debate_global_cap: 20000,
126
+ } as const;
127
+
128
+ export interface PlanReviewGateStrategy {
129
+ mode: "consolidated" | "threaded" | "parallel_probes";
130
+ profile: DebateProfile;
131
+ required_focuses: PlanDebateFocus[];
132
+ min_focus_rounds: number;
133
+ max_rounds: number;
134
+ max_exchanges_per_round: number;
135
+ round_token_cap: number;
136
+ debate_global_cap: number;
137
+ rationale: string[];
138
+ }
139
+
119
140
  function capsForProfile(
120
141
  profile: DebateProfile,
121
142
  ): Omit<
122
143
  DebateEligibilityResult,
123
- "profile" | "required_focuses" | "human_required" | "rationale"
144
+ | "profile"
145
+ | "required_focuses"
146
+ | "human_required"
147
+ | "rationale"
148
+ | "review_gate_strategy"
124
149
  > {
125
150
  if (profile === "light") {
126
151
  return {
127
152
  ...PLAN_BUDGET_LIGHT,
128
153
  };
129
154
  }
155
+ if (profile === "fast") {
156
+ return {
157
+ ...PLAN_BUDGET_FAST,
158
+ };
159
+ }
130
160
  return {
131
161
  ...PLAN_BUDGET_STANDARD,
132
162
  };
@@ -161,7 +191,7 @@ export function harnessPlanDebateEligibility(
161
191
 
162
192
  const conflictingPatterns =
163
193
  Array.isArray(impl?.solution_patterns) &&
164
- (impl!.solution_patterns as unknown[]).length >= 2 &&
194
+ (impl?.solution_patterns as unknown[]).length >= 2 &&
165
195
  openQs.length > 0;
166
196
  if (conflictingPatterns) {
167
197
  human_required = true;
@@ -182,6 +212,18 @@ export function harnessPlanDebateEligibility(
182
212
  rationale.push(
183
213
  "full: high risk, material fork, open questions, DAG patch, or tensions",
184
214
  );
215
+ } else if (
216
+ risk === "med" &&
217
+ !materialFork &&
218
+ !dagPatched &&
219
+ input.dag_pass !== false &&
220
+ openQs.length === 0 &&
221
+ stackHasClearPrimary(stack)
222
+ ) {
223
+ profile = "fast";
224
+ rationale.push(
225
+ "fast: medium risk with clear stack and no open questions; use consolidated review with escalation on blockers",
226
+ );
185
227
  } else if (
186
228
  risk === "low" &&
187
229
  !materialFork &&
@@ -192,7 +234,7 @@ export function harnessPlanDebateEligibility(
192
234
  ) {
193
235
  profile = "light";
194
236
  rationale.push(
195
- "light: low risk, clear stack, high-confidence implementation approach",
237
+ "light: low risk, clear stack, high-confidence implementation (threaded spec+quality)",
196
238
  );
197
239
  } else if (risk === "med") {
198
240
  profile = "standard";
@@ -200,7 +242,9 @@ export function harnessPlanDebateEligibility(
200
242
  }
201
243
 
202
244
  const required_focuses: PlanDebateFocus[] =
203
- profile === "light" ? [...LIGHT_FOCUS] : [...PLAN_FOCUS_AREAS];
245
+ profile === "fast" || profile === "light"
246
+ ? [...LIGHT_FOCUS]
247
+ : [...PLAN_FOCUS_AREAS];
204
248
 
205
249
  const caps = capsForProfile(profile);
206
250
 
@@ -210,5 +254,21 @@ export function harnessPlanDebateEligibility(
210
254
  ...caps,
211
255
  human_required,
212
256
  rationale,
257
+ review_gate_strategy: {
258
+ mode:
259
+ profile === "fast"
260
+ ? "consolidated"
261
+ : profile === "standard"
262
+ ? "parallel_probes"
263
+ : "threaded",
264
+ profile,
265
+ required_focuses: [...required_focuses],
266
+ min_focus_rounds: caps.min_focus_rounds,
267
+ max_rounds: caps.max_rounds,
268
+ max_exchanges_per_round: caps.max_exchanges_per_round,
269
+ round_token_cap: caps.round_token_cap,
270
+ debate_global_cap: caps.debate_global_cap,
271
+ rationale: [...rationale],
272
+ },
213
273
  };
214
274
  }