ultimate-pi 0.14.0 → 0.15.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 +41 -61
- package/.agents/skills/harness-orchestration/SKILL.md +2 -2
- package/.agents/skills/harness-plan/SKILL.md +10 -8
- package/.pi/agents/harness/planning/decompose.md +4 -2
- package/.pi/agents/harness/planning/execution-plan-author.md +25 -14
- package/.pi/agents/harness/planning/hypothesis-validator.md +21 -5
- package/.pi/agents/harness/planning/implementation-researcher.md +42 -0
- package/.pi/agents/harness/planning/plan-adversary.md +19 -3
- package/.pi/agents/harness/planning/plan-evaluator.md +26 -5
- package/.pi/agents/harness/planning/review-integrator.md +23 -9
- package/.pi/agents/harness/planning/scout-graphify.md +1 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +19 -4
- package/.pi/agents/harness/planning/stack-researcher.md +19 -10
- package/.pi/extensions/harness-debate-tools.ts +238 -16
- package/.pi/extensions/harness-live-widget.ts +39 -159
- package/.pi/extensions/harness-plan-approval.ts +47 -5
- package/.pi/extensions/lib/debate-bus-core.ts +69 -15
- package/.pi/extensions/lib/debate-bus-state.ts +6 -0
- package/.pi/extensions/lib/plan-approval/plan-review.ts +56 -0
- package/.pi/extensions/lib/plan-approval/types.ts +1 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +214 -0
- package/.pi/extensions/lib/plan-debate-focus.ts +151 -0
- package/.pi/extensions/lib/plan-debate-gate.ts +77 -34
- package/.pi/extensions/lib/plan-debate-lanes.ts +44 -0
- package/.pi/extensions/lib/plan-debate-round-status.ts +63 -20
- package/.pi/extensions/lib/plan-messenger.ts +93 -17
- package/.pi/extensions/policy-gate.ts +1 -1
- package/.pi/harness/README.md +1 -1
- package/.pi/harness/agents.manifest.json +15 -11
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +1 -3
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +13 -5
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +51 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r1.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/artifacts/review-round-r2.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-low-light/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r2.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/artifacts/review-round-r3.yaml +24 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med/research-brief.yaml +29 -0
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +97 -16
- package/.pi/harness/specs/plan-implementation-research-brief.schema.json +128 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/round-result.schema.json +15 -2
- package/.pi/lib/harness-ui-state.ts +92 -0
- package/.pi/prompts/harness-plan.md +87 -37
- package/.pi/prompts/planning-rubrics.md +31 -0
- package/CHANGELOG.md +11 -0
- package/package.json +2 -2
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* harness-plan-approval — PlanPacket approval UI and transcript renderer for parent sessions.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
5
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
9
|
import { Text } from "@earendil-works/pi-tui";
|
|
7
10
|
import { Type } from "@sinclair/typebox";
|
|
@@ -146,6 +149,43 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
146
149
|
`Plan ${planId} — pending your approval`;
|
|
147
150
|
const runCtx = getLatestRunContext(entries);
|
|
148
151
|
const projectRoot = process.cwd();
|
|
152
|
+
const implWarnings: string[] = [];
|
|
153
|
+
if (runCtx?.run_id) {
|
|
154
|
+
const implPath = join(
|
|
155
|
+
projectRoot,
|
|
156
|
+
".pi",
|
|
157
|
+
"harness",
|
|
158
|
+
"runs",
|
|
159
|
+
runCtx.run_id,
|
|
160
|
+
"artifacts",
|
|
161
|
+
"implementation-research.yaml",
|
|
162
|
+
);
|
|
163
|
+
let implExists = false;
|
|
164
|
+
try {
|
|
165
|
+
await access(implPath, constants.R_OK);
|
|
166
|
+
implExists = true;
|
|
167
|
+
} catch {
|
|
168
|
+
implExists = false;
|
|
169
|
+
}
|
|
170
|
+
const risk = String(
|
|
171
|
+
validated.plan_packet.risk_level ?? "med",
|
|
172
|
+
).toLowerCase();
|
|
173
|
+
if (!implExists) {
|
|
174
|
+
const msg =
|
|
175
|
+
"approve_plan: missing artifacts/implementation-research.yaml (Phase 3.5 required)";
|
|
176
|
+
if (risk === "high") {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: msg }],
|
|
179
|
+
details: {
|
|
180
|
+
plan_packet: validated.plan_packet,
|
|
181
|
+
cancelled: true,
|
|
182
|
+
},
|
|
183
|
+
isError: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
implWarnings.push(msg);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
149
189
|
if (runCtx?.run_id) {
|
|
150
190
|
const gate = await validatePlanDebateGate(projectRoot, runCtx.run_id);
|
|
151
191
|
if (!gate.ok) {
|
|
@@ -237,13 +277,15 @@ export default function harnessPlanApproval(pi: ExtensionAPI) {
|
|
|
237
277
|
);
|
|
238
278
|
}
|
|
239
279
|
|
|
240
|
-
const text =
|
|
241
|
-
outcome.response,
|
|
242
|
-
|
|
243
|
-
|
|
280
|
+
const text = [
|
|
281
|
+
formatApprovePlanResultText(outcome.response, outcome.cancelled),
|
|
282
|
+
...implWarnings,
|
|
283
|
+
]
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join("\n\n");
|
|
244
286
|
return {
|
|
245
287
|
content: [{ type: "text", text }],
|
|
246
|
-
details,
|
|
288
|
+
details: { ...details, implementation_warnings: implWarnings },
|
|
247
289
|
};
|
|
248
290
|
},
|
|
249
291
|
|
|
@@ -19,6 +19,17 @@ import {
|
|
|
19
19
|
setDebateState,
|
|
20
20
|
setLastSeverity,
|
|
21
21
|
} from "./debate-bus-state.js";
|
|
22
|
+
import {
|
|
23
|
+
type DebateProfile,
|
|
24
|
+
PLAN_BUDGET_LIGHT,
|
|
25
|
+
PLAN_BUDGET_STANDARD,
|
|
26
|
+
} from "./plan-debate-eligibility.js";
|
|
27
|
+
import {
|
|
28
|
+
getPlanFocusCoverage,
|
|
29
|
+
PLAN_FOCUS_AREAS,
|
|
30
|
+
type PlanDebateFocus,
|
|
31
|
+
planDebateOutcomeComplete,
|
|
32
|
+
} from "./plan-debate-focus.js";
|
|
22
33
|
|
|
23
34
|
export type PolicyDecision =
|
|
24
35
|
| "pass"
|
|
@@ -66,11 +77,7 @@ const THRESHOLDS = {
|
|
|
66
77
|
};
|
|
67
78
|
const HARD_STOP_DEBATE_CAPS = process.env.HARNESS_DEBATE_HARD_STOP === "true";
|
|
68
79
|
|
|
69
|
-
const PLAN_BUDGET =
|
|
70
|
-
max_rounds: 4,
|
|
71
|
-
round_token_cap: 2000,
|
|
72
|
-
debate_global_cap: 12000,
|
|
73
|
-
} as const;
|
|
80
|
+
const PLAN_BUDGET = PLAN_BUDGET_STANDARD;
|
|
74
81
|
|
|
75
82
|
const AGGRESSIVE_BUDGET = {
|
|
76
83
|
max_rounds: 6,
|
|
@@ -88,16 +95,28 @@ function toSafeFloat(value: unknown): number {
|
|
|
88
95
|
return Math.max(0, Math.min(1, n));
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
export function capsForDebate(
|
|
98
|
+
export function capsForDebate(
|
|
99
|
+
debateId: string,
|
|
100
|
+
profile?: DebateProfile,
|
|
101
|
+
): {
|
|
92
102
|
name: "plan" | "aggressive";
|
|
103
|
+
min_focus_rounds: number;
|
|
93
104
|
max_rounds: number;
|
|
105
|
+
max_exchanges_per_round: number;
|
|
94
106
|
round_token_cap: number;
|
|
95
107
|
debate_global_cap: number;
|
|
96
108
|
} {
|
|
97
109
|
if (isPlanDebateId(debateId)) {
|
|
98
|
-
|
|
110
|
+
const active = profile ?? getDebateState()?.debate_profile ?? "standard";
|
|
111
|
+
const budget = active === "light" ? PLAN_BUDGET_LIGHT : PLAN_BUDGET;
|
|
112
|
+
return { name: "plan", ...budget };
|
|
99
113
|
}
|
|
100
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
name: "aggressive",
|
|
116
|
+
min_focus_rounds: 1,
|
|
117
|
+
max_exchanges_per_round: 1,
|
|
118
|
+
...AGGRESSIVE_BUDGET,
|
|
119
|
+
};
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
function participantAllowed(
|
|
@@ -161,23 +180,40 @@ export interface DebateBusHooks {
|
|
|
161
180
|
appendEntry: (customType: string, data: unknown) => void;
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
export interface OpenDebateBusOptions {
|
|
184
|
+
debate_profile?: DebateProfile;
|
|
185
|
+
required_focuses?: DebateState["required_focuses"];
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
export async function openDebateBus(
|
|
165
189
|
runId: string,
|
|
166
190
|
debateId: string,
|
|
167
191
|
hooks: DebateBusHooks,
|
|
192
|
+
opts?: OpenDebateBusOptions,
|
|
168
193
|
): Promise<DebateState> {
|
|
169
|
-
const
|
|
194
|
+
const profile = opts?.debate_profile ?? "standard";
|
|
195
|
+
const caps = capsForDebate(debateId, profile);
|
|
170
196
|
const debate_phase = debatePhaseFromId(debateId);
|
|
197
|
+
const defaultFocuses: PlanDebateFocus[] =
|
|
198
|
+
profile === "light" ? ["spec", "quality"] : [...PLAN_FOCUS_AREAS];
|
|
199
|
+
const required_focuses =
|
|
200
|
+
opts?.required_focuses && opts.required_focuses.length > 0
|
|
201
|
+
? opts.required_focuses
|
|
202
|
+
: defaultFocuses;
|
|
171
203
|
const next: DebateState = {
|
|
172
204
|
run_id: runId,
|
|
173
205
|
debate_id: debateId,
|
|
174
206
|
debate_phase,
|
|
175
207
|
round_count: 0,
|
|
176
208
|
budget_used: 0,
|
|
209
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
177
210
|
max_rounds: caps.max_rounds,
|
|
211
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
178
212
|
round_token_cap: caps.round_token_cap,
|
|
179
213
|
debate_global_cap: caps.debate_global_cap,
|
|
180
214
|
last_review_gate_ready: false,
|
|
215
|
+
debate_profile: profile,
|
|
216
|
+
required_focuses,
|
|
181
217
|
};
|
|
182
218
|
setDebateState(next);
|
|
183
219
|
setLastSeverity({
|
|
@@ -199,6 +235,8 @@ export async function openDebateBus(
|
|
|
199
235
|
opened_at: nowIso(),
|
|
200
236
|
debate_phase,
|
|
201
237
|
budget_profile: caps.name,
|
|
238
|
+
debate_profile: profile,
|
|
239
|
+
required_focuses,
|
|
202
240
|
},
|
|
203
241
|
};
|
|
204
242
|
hooks.appendEntry("harness-debate-envelope", envelope);
|
|
@@ -230,7 +268,9 @@ async function emitBudgetExhausted(
|
|
|
230
268
|
budget_used: state.budget_used,
|
|
231
269
|
exhaustion_reason: reason,
|
|
232
270
|
caps: {
|
|
271
|
+
min_focus_rounds: state.min_focus_rounds,
|
|
233
272
|
max_rounds: state.max_rounds,
|
|
273
|
+
max_exchanges_per_round: state.max_exchanges_per_round,
|
|
234
274
|
round_token_cap: state.round_token_cap,
|
|
235
275
|
debate_global_cap: state.debate_global_cap,
|
|
236
276
|
},
|
|
@@ -327,7 +367,9 @@ export async function acceptDebateRound(
|
|
|
327
367
|
token_usage: envelope.payload.token_usage,
|
|
328
368
|
budget_profile: {
|
|
329
369
|
name: profileName,
|
|
370
|
+
min_focus_rounds: state.min_focus_rounds,
|
|
330
371
|
max_rounds: state.max_rounds,
|
|
372
|
+
max_exchanges_per_round: state.max_exchanges_per_round,
|
|
331
373
|
round_token_cap: state.round_token_cap,
|
|
332
374
|
debate_global_cap: state.debate_global_cap,
|
|
333
375
|
},
|
|
@@ -363,12 +405,24 @@ export async function finalizeDebateConsensus(
|
|
|
363
405
|
);
|
|
364
406
|
const decision = decidePolicy(lastSeverity, evidenceScore);
|
|
365
407
|
const planPhase = state.debate_phase === "plan";
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
408
|
+
let evaluatorPassed = true;
|
|
409
|
+
let debateComplete = state.round_count > 0;
|
|
410
|
+
if (planPhase) {
|
|
411
|
+
const runDir = join(process.cwd(), ".pi", "harness", "runs", state.run_id);
|
|
412
|
+
const requiredFocuses =
|
|
413
|
+
state.required_focuses && state.required_focuses.length > 0
|
|
414
|
+
? state.required_focuses
|
|
415
|
+
: undefined;
|
|
416
|
+
const coverage = await getPlanFocusCoverage(runDir, {
|
|
417
|
+
requiredFocuses,
|
|
418
|
+
});
|
|
419
|
+
evaluatorPassed =
|
|
420
|
+
coverage.last_review_gate_ready || Boolean(state.last_review_gate_ready);
|
|
421
|
+
debateComplete = planDebateOutcomeComplete(coverage, {
|
|
422
|
+
requiredFocuses,
|
|
423
|
+
minRoundIndex: state.min_focus_rounds,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
372
426
|
|
|
373
427
|
const consensus = {
|
|
374
428
|
schema_version: "1.0.0",
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { DebateParticipant } from "../../lib/debate-orchestrator-types.js";
|
|
6
|
+
import type { DebateProfile } from "./plan-debate-eligibility.js";
|
|
7
|
+
import type { PlanDebateFocus } from "./plan-debate-focus.js";
|
|
6
8
|
|
|
7
9
|
export type DebatePhase = "plan" | "post_execute";
|
|
8
10
|
|
|
@@ -12,10 +14,14 @@ export interface DebateState {
|
|
|
12
14
|
debate_phase: DebatePhase;
|
|
13
15
|
round_count: number;
|
|
14
16
|
budget_used: number;
|
|
17
|
+
min_focus_rounds: number;
|
|
15
18
|
max_rounds: number;
|
|
19
|
+
max_exchanges_per_round: number;
|
|
16
20
|
round_token_cap: number;
|
|
17
21
|
debate_global_cap: number;
|
|
18
22
|
last_review_gate_ready?: boolean;
|
|
23
|
+
debate_profile?: DebateProfile;
|
|
24
|
+
required_focuses?: PlanDebateFocus[];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export interface SeverityScores {
|
|
@@ -160,6 +160,62 @@ export function formatResearchBriefMarkdown(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
const impl = asRecord(research.implementation);
|
|
164
|
+
if (impl) {
|
|
165
|
+
lines.push("## Phase 3.5 — Implementation research");
|
|
166
|
+
lines.push("");
|
|
167
|
+
const framing = str(impl.problem_framing);
|
|
168
|
+
if (framing) {
|
|
169
|
+
lines.push("**Problem framing:**");
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push(framing);
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
const rec = asRecord(impl.recommended_approach);
|
|
175
|
+
if (rec) {
|
|
176
|
+
const summary = str(rec.summary);
|
|
177
|
+
const conf = str(rec.recommended_approach_confidence);
|
|
178
|
+
if (summary) {
|
|
179
|
+
lines.push(
|
|
180
|
+
`**Recommended approach**${conf ? ` (${conf} confidence)` : ""}:`,
|
|
181
|
+
);
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push(summary);
|
|
184
|
+
lines.push("");
|
|
185
|
+
}
|
|
186
|
+
const rationale = str(rec.confidence_rationale);
|
|
187
|
+
if (rationale) {
|
|
188
|
+
lines.push(`*Rationale:* ${rationale}`);
|
|
189
|
+
lines.push("");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const patterns = Array.isArray(impl.solution_patterns)
|
|
193
|
+
? impl.solution_patterns
|
|
194
|
+
: [];
|
|
195
|
+
if (patterns.length) {
|
|
196
|
+
lines.push("**Solution patterns:**");
|
|
197
|
+
for (const p of patterns) {
|
|
198
|
+
const pat = asRecord(p);
|
|
199
|
+
const name = pat ? str(pat.name) : null;
|
|
200
|
+
const fit = pat ? str(pat.fit) : null;
|
|
201
|
+
if (name) lines.push(`- **${name}**${fit ? `: ${fit}` : ""}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
}
|
|
205
|
+
const openQs = strList(impl.open_questions);
|
|
206
|
+
if (openQs.length) {
|
|
207
|
+
lines.push("**Open questions:**");
|
|
208
|
+
for (const q of openQs) lines.push(`- ${q}`);
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
const anti = strList(impl.anti_patterns);
|
|
212
|
+
if (anti.length) {
|
|
213
|
+
lines.push("**Anti-patterns:**");
|
|
214
|
+
for (const a of anti) lines.push(`- ${a}`);
|
|
215
|
+
lines.push("");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
163
219
|
if (evalBrief) {
|
|
164
220
|
lines.push("## Self-evaluation");
|
|
165
221
|
lines.push("");
|
|
@@ -13,6 +13,7 @@ export interface PlanResearchBrief {
|
|
|
13
13
|
hypothesis?: Record<string, unknown> | null;
|
|
14
14
|
eval?: Record<string, unknown> | null;
|
|
15
15
|
stack?: Record<string, unknown> | null;
|
|
16
|
+
implementation?: Record<string, unknown> | null;
|
|
16
17
|
debate?: {
|
|
17
18
|
rounds?: Record<string, unknown>[];
|
|
18
19
|
hypothesis_validations?: Record<string, unknown>[];
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-debate profile selection (full | standard | light).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PLAN_FOCUS_AREAS, type PlanDebateFocus } from "./plan-debate-focus.js";
|
|
6
|
+
|
|
7
|
+
export type DebateProfile = "full" | "standard" | "light";
|
|
8
|
+
|
|
9
|
+
export interface DebateEligibilityInput {
|
|
10
|
+
risk_level?: string;
|
|
11
|
+
material_fork?: boolean;
|
|
12
|
+
dag_pass?: boolean;
|
|
13
|
+
dag_manually_patched?: boolean;
|
|
14
|
+
implementation_brief?: Record<string, unknown> | null;
|
|
15
|
+
stack_brief?: Record<string, unknown> | null;
|
|
16
|
+
decomposition?: Record<string, unknown> | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DebateEligibilityResult {
|
|
20
|
+
profile: DebateProfile;
|
|
21
|
+
required_focuses: PlanDebateFocus[];
|
|
22
|
+
min_focus_rounds: number;
|
|
23
|
+
max_rounds: number;
|
|
24
|
+
max_exchanges_per_round: number;
|
|
25
|
+
round_token_cap: number;
|
|
26
|
+
debate_global_cap: number;
|
|
27
|
+
human_required: boolean;
|
|
28
|
+
rationale: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const LIGHT_FOCUS: PlanDebateFocus[] = ["spec", "quality"];
|
|
32
|
+
|
|
33
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
34
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
35
|
+
? (value as Record<string, unknown>)
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function strList(value: unknown): string[] {
|
|
40
|
+
if (!Array.isArray(value)) return [];
|
|
41
|
+
return value
|
|
42
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function implementationOpenQuestions(
|
|
47
|
+
brief: Record<string, unknown> | null,
|
|
48
|
+
): string[] {
|
|
49
|
+
if (!brief) return [];
|
|
50
|
+
return strList(brief.open_questions);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recommendedApproach(
|
|
54
|
+
brief: Record<string, unknown> | null,
|
|
55
|
+
): Record<string, unknown> | null {
|
|
56
|
+
return asRecord(brief?.recommended_approach);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stackHasClearPrimary(stack: Record<string, unknown> | null): boolean {
|
|
60
|
+
if (!stack) return false;
|
|
61
|
+
const primary = stack.recommended_primary;
|
|
62
|
+
return typeof primary === "string" && primary.trim().length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function confidenceAllowsLight(brief: Record<string, unknown> | null): boolean {
|
|
66
|
+
const rec = recommendedApproach(brief);
|
|
67
|
+
if (!rec) return false;
|
|
68
|
+
const conf = String(rec.recommended_approach_confidence ?? "").toLowerCase();
|
|
69
|
+
if (conf !== "high") return false;
|
|
70
|
+
const rationale =
|
|
71
|
+
typeof rec.confidence_rationale === "string"
|
|
72
|
+
? rec.confidence_rationale.trim()
|
|
73
|
+
: "";
|
|
74
|
+
const refs = strList(rec.evidence_refs);
|
|
75
|
+
if (!rationale || refs.length < 2) return false;
|
|
76
|
+
if (implementationOpenQuestions(brief).length > 0) return false;
|
|
77
|
+
const patterns = Array.isArray(brief?.solution_patterns)
|
|
78
|
+
? (brief!.solution_patterns as unknown[])
|
|
79
|
+
: [];
|
|
80
|
+
for (const p of patterns) {
|
|
81
|
+
const pat = asRecord(p);
|
|
82
|
+
const risks = pat ? strList(pat.risks) : [];
|
|
83
|
+
if (risks.some((r) => /unmitigated|critical|blocker/i.test(r))) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const similar = Array.isArray(brief?.similar_implementations)
|
|
88
|
+
? (brief!.similar_implementations as unknown[])
|
|
89
|
+
: [];
|
|
90
|
+
if (similar.length === 0) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function decompositionTensionCount(
|
|
95
|
+
decomposition: Record<string, unknown> | null,
|
|
96
|
+
): number {
|
|
97
|
+
if (!decomposition) return 0;
|
|
98
|
+
return Array.isArray(decomposition.tensions)
|
|
99
|
+
? decomposition.tensions.length
|
|
100
|
+
: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const PLAN_BUDGET_STANDARD = {
|
|
104
|
+
min_focus_rounds: 4,
|
|
105
|
+
max_rounds: 12,
|
|
106
|
+
max_exchanges_per_round: 3,
|
|
107
|
+
round_token_cap: 8000,
|
|
108
|
+
debate_global_cap: 80000,
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
export const PLAN_BUDGET_LIGHT = {
|
|
112
|
+
min_focus_rounds: 2,
|
|
113
|
+
max_rounds: 8,
|
|
114
|
+
max_exchanges_per_round: 3,
|
|
115
|
+
round_token_cap: 6000,
|
|
116
|
+
debate_global_cap: 40000,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
function capsForProfile(
|
|
120
|
+
profile: DebateProfile,
|
|
121
|
+
): Omit<
|
|
122
|
+
DebateEligibilityResult,
|
|
123
|
+
"profile" | "required_focuses" | "human_required" | "rationale"
|
|
124
|
+
> {
|
|
125
|
+
if (profile === "light") {
|
|
126
|
+
return {
|
|
127
|
+
...PLAN_BUDGET_LIGHT,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...PLAN_BUDGET_STANDARD,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Select debate profile from pre-debate signals only (no R1 hypothesis output).
|
|
137
|
+
*/
|
|
138
|
+
export function harnessPlanDebateEligibility(
|
|
139
|
+
input: DebateEligibilityInput,
|
|
140
|
+
): DebateEligibilityResult {
|
|
141
|
+
const rationale: string[] = [];
|
|
142
|
+
const risk = String(input.risk_level ?? "med").toLowerCase();
|
|
143
|
+
const impl = input.implementation_brief ?? null;
|
|
144
|
+
const stack = input.stack_brief ?? null;
|
|
145
|
+
const openQs = implementationOpenQuestions(impl);
|
|
146
|
+
const materialFork = input.material_fork === true;
|
|
147
|
+
const dagPatched = input.dag_manually_patched === true;
|
|
148
|
+
const dagFail = input.dag_pass === false;
|
|
149
|
+
|
|
150
|
+
let human_required = false;
|
|
151
|
+
|
|
152
|
+
if (dagFail) {
|
|
153
|
+
rationale.push("DAG validation failed — use standard profile until fixed");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (openQs.length > 0) {
|
|
157
|
+
rationale.push(
|
|
158
|
+
`implementation open_questions (${openQs.length}) — not eligible for light`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const conflictingPatterns =
|
|
163
|
+
Array.isArray(impl?.solution_patterns) &&
|
|
164
|
+
(impl!.solution_patterns as unknown[]).length >= 2 &&
|
|
165
|
+
openQs.length > 0;
|
|
166
|
+
if (conflictingPatterns) {
|
|
167
|
+
human_required = true;
|
|
168
|
+
rationale.push("conflicting external patterns with open questions");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let profile: DebateProfile = "standard";
|
|
172
|
+
rationale.push("default profile: standard (fail-safe)");
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
risk === "high" ||
|
|
176
|
+
materialFork ||
|
|
177
|
+
openQs.length > 0 ||
|
|
178
|
+
dagPatched ||
|
|
179
|
+
decompositionTensionCount(input.decomposition ?? null) >= 3
|
|
180
|
+
) {
|
|
181
|
+
profile = "full";
|
|
182
|
+
rationale.push(
|
|
183
|
+
"full: high risk, material fork, open questions, DAG patch, or tensions",
|
|
184
|
+
);
|
|
185
|
+
} else if (
|
|
186
|
+
risk === "low" &&
|
|
187
|
+
!materialFork &&
|
|
188
|
+
!dagPatched &&
|
|
189
|
+
input.dag_pass !== false &&
|
|
190
|
+
confidenceAllowsLight(impl) &&
|
|
191
|
+
stackHasClearPrimary(stack)
|
|
192
|
+
) {
|
|
193
|
+
profile = "light";
|
|
194
|
+
rationale.push(
|
|
195
|
+
"light: low risk, clear stack, high-confidence implementation approach",
|
|
196
|
+
);
|
|
197
|
+
} else if (risk === "med") {
|
|
198
|
+
profile = "standard";
|
|
199
|
+
rationale.push("standard: med risk default");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const required_focuses: PlanDebateFocus[] =
|
|
203
|
+
profile === "light" ? [...LIGHT_FOCUS] : [...PLAN_FOCUS_AREAS];
|
|
204
|
+
|
|
205
|
+
const caps = capsForProfile(profile);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
profile,
|
|
209
|
+
required_focuses,
|
|
210
|
+
...caps,
|
|
211
|
+
human_required,
|
|
212
|
+
rationale,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-phase Review Gate focus coverage (spec | wbs | schedule | quality).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
|
|
10
|
+
export const PLAN_FOCUS_AREAS = ["spec", "wbs", "schedule", "quality"] as const;
|
|
11
|
+
export type PlanDebateFocus = (typeof PLAN_FOCUS_AREAS)[number];
|
|
12
|
+
|
|
13
|
+
export interface PlanFocusCoverage {
|
|
14
|
+
covered: PlanDebateFocus[];
|
|
15
|
+
missing: PlanDebateFocus[];
|
|
16
|
+
rounds_by_focus: Partial<Record<PlanDebateFocus, number>>;
|
|
17
|
+
focus_by_round: Partial<Record<number, PlanDebateFocus>>;
|
|
18
|
+
last_review_gate_ready: boolean;
|
|
19
|
+
last_round_index: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PlanFocusCoverageOptions {
|
|
23
|
+
requiredFocuses?: readonly PlanDebateFocus[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
await access(path, constants.R_OK);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function focusFromDraft(
|
|
36
|
+
draft: Record<string, unknown>,
|
|
37
|
+
): PlanDebateFocus | null {
|
|
38
|
+
const focus = String(draft.debate_round_focus ?? "").trim();
|
|
39
|
+
if ((PLAN_FOCUS_AREAS as readonly string[]).includes(focus)) {
|
|
40
|
+
return focus as PlanDebateFocus;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Scan submitted review-round artifacts for focus coverage and last gate flag.
|
|
47
|
+
*/
|
|
48
|
+
export async function getPlanFocusCoverage(
|
|
49
|
+
runDir: string,
|
|
50
|
+
opts?: PlanFocusCoverageOptions,
|
|
51
|
+
): Promise<PlanFocusCoverage> {
|
|
52
|
+
const required =
|
|
53
|
+
opts?.requiredFocuses && opts.requiredFocuses.length > 0
|
|
54
|
+
? opts.requiredFocuses
|
|
55
|
+
: PLAN_FOCUS_AREAS;
|
|
56
|
+
const artifactsDir = join(runDir, "artifacts");
|
|
57
|
+
const covered = new Set<PlanDebateFocus>();
|
|
58
|
+
const rounds_by_focus: Partial<Record<PlanDebateFocus, number>> = {};
|
|
59
|
+
const focus_by_round: Partial<Record<number, PlanDebateFocus>> = {};
|
|
60
|
+
let last_review_gate_ready = false;
|
|
61
|
+
let last_round_index = 0;
|
|
62
|
+
|
|
63
|
+
let files: string[] = [];
|
|
64
|
+
try {
|
|
65
|
+
files = (await readdir(artifactsDir)).filter((f) =>
|
|
66
|
+
/^review-round-r\d+\.yaml$/i.test(f),
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
return {
|
|
70
|
+
covered: [],
|
|
71
|
+
missing: [...required],
|
|
72
|
+
rounds_by_focus: {},
|
|
73
|
+
focus_by_round: {},
|
|
74
|
+
last_review_gate_ready: false,
|
|
75
|
+
last_round_index: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const name of files.sort()) {
|
|
80
|
+
const m = /^review-round-r(\d+)\.yaml$/i.exec(name);
|
|
81
|
+
if (!m) continue;
|
|
82
|
+
const roundIndex = Number(m[1]);
|
|
83
|
+
if (roundIndex > last_round_index) last_round_index = roundIndex;
|
|
84
|
+
const raw = await readFile(join(artifactsDir, name), "utf-8");
|
|
85
|
+
let draft: Record<string, unknown>;
|
|
86
|
+
try {
|
|
87
|
+
draft = parseYaml(raw) as Record<string, unknown>;
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const focus = focusFromDraft(draft);
|
|
92
|
+
if (focus) {
|
|
93
|
+
covered.add(focus);
|
|
94
|
+
rounds_by_focus[focus] = roundIndex;
|
|
95
|
+
focus_by_round[roundIndex] = focus;
|
|
96
|
+
}
|
|
97
|
+
if (roundIndex === last_round_index) {
|
|
98
|
+
last_review_gate_ready = draft.review_gate_ready === true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const coveredList = required.filter((f) => covered.has(f));
|
|
103
|
+
const missing = required.filter((f) => !covered.has(f));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
covered: coveredList,
|
|
107
|
+
missing,
|
|
108
|
+
rounds_by_focus,
|
|
109
|
+
focus_by_round,
|
|
110
|
+
last_review_gate_ready,
|
|
111
|
+
last_round_index,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PlanDebateOutcomeOptions {
|
|
116
|
+
requiredFocuses?: readonly PlanDebateFocus[];
|
|
117
|
+
minRoundIndex?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function planDebateOutcomeComplete(
|
|
121
|
+
coverage: PlanFocusCoverage,
|
|
122
|
+
opts?: PlanDebateOutcomeOptions,
|
|
123
|
+
): boolean {
|
|
124
|
+
const required =
|
|
125
|
+
opts?.requiredFocuses && opts.requiredFocuses.length > 0
|
|
126
|
+
? opts.requiredFocuses
|
|
127
|
+
: PLAN_FOCUS_AREAS;
|
|
128
|
+
const minRounds = opts?.minRoundIndex ?? required.length;
|
|
129
|
+
const missing = required.filter((f) => !coverage.covered.includes(f));
|
|
130
|
+
return (
|
|
131
|
+
missing.length === 0 &&
|
|
132
|
+
coverage.last_review_gate_ready === true &&
|
|
133
|
+
coverage.last_round_index >= minRounds
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read debate_round_focus from an existing review-round artifact. */
|
|
138
|
+
export async function readDebateRoundFocus(
|
|
139
|
+
runDir: string,
|
|
140
|
+
roundIndex: number,
|
|
141
|
+
): Promise<PlanDebateFocus | null> {
|
|
142
|
+
const path = join(runDir, "artifacts", `review-round-r${roundIndex}.yaml`);
|
|
143
|
+
if (!(await fileExists(path))) return null;
|
|
144
|
+
try {
|
|
145
|
+
const raw = await readFile(path, "utf-8");
|
|
146
|
+
const draft = parseYaml(raw) as Record<string, unknown>;
|
|
147
|
+
return focusFromDraft(draft);
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|