ultimate-pi 0.15.0 → 0.17.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-governor/SKILL.md +11 -0
- package/.agents/skills/harness-orchestration/SKILL.md +3 -1
- package/.agents/skills/harness-plan/SKILL.md +5 -5
- package/.pi/agents/harness/adversary.md +1 -1
- package/.pi/agents/harness/evaluator.md +1 -1
- package/.pi/agents/harness/executor.md +1 -1
- package/.pi/agents/harness/incident-recorder.md +1 -1
- package/.pi/agents/harness/meta-optimizer.md +1 -1
- package/.pi/agents/harness/planning/decompose.md +4 -33
- package/.pi/agents/harness/planning/execution-plan-author.md +3 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +3 -2
- package/.pi/agents/harness/planning/hypothesis.md +4 -27
- package/.pi/agents/harness/planning/implementation-researcher.md +3 -2
- package/.pi/agents/harness/planning/plan-adversary.md +2 -3
- package/.pi/agents/harness/planning/plan-evaluator.md +3 -2
- package/.pi/agents/harness/planning/review-integrator.md +2 -3
- package/.pi/agents/harness/planning/scout-graphify.md +3 -22
- package/.pi/agents/harness/planning/scout-semantic.md +3 -18
- package/.pi/agents/harness/planning/scout-structure.md +3 -18
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +3 -2
- package/.pi/agents/harness/planning/stack-researcher.md +3 -2
- package/.pi/agents/harness/tie-breaker.md +1 -1
- package/.pi/agents/harness/trace-librarian.md +1 -1
- package/.pi/extensions/budget-guard.ts +33 -19
- package/.pi/extensions/harness-debate-tools.ts +54 -6
- package/.pi/extensions/harness-run-context.ts +108 -2
- package/.pi/extensions/harness-subagent-submit.ts +172 -0
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +49 -6
- package/.pi/extensions/lib/harness-subagent-auth.ts +104 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +59 -0
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +82 -0
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +172 -0
- package/.pi/extensions/lib/harness-subagents-bridge.ts +127 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +61 -8
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +92 -18
- package/.pi/extensions/lib/plan-debate-lane.ts +15 -0
- package/.pi/extensions/lib/plan-debate-lanes.ts +27 -3
- package/.pi/extensions/lib/plan-debate-round-status.ts +18 -7
- package/.pi/extensions/lib/plan-messenger.ts +4 -0
- package/.pi/extensions/lib/plan-review-gate.ts +51 -0
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +22 -22
- package/.pi/harness/docs/adrs/0037-subagent-submit-tools.md +31 -0
- package/.pi/harness/docs/adrs/0038-budget-telemetry-only.md +23 -0
- package/.pi/harness/docs/adrs/README.md +2 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/implementation-research.yaml +28 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/artifacts/review-round-consolidated.yaml +25 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-packet.yaml +196 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/plan-review.md +14 -0
- package/.pi/harness/evals/smoke/fixtures/plan-phase/minimal-med-fast/research-brief.yaml +62 -0
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +40 -17
- package/.pi/harness/specs/harness-executor-handoff.schema.json +19 -0
- package/.pi/harness/specs/harness-human-required.schema.json +16 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/plan-scout-findings.schema.json +19 -0
- package/.pi/lib/harness-agent-output.ts +45 -0
- package/.pi/lib/harness-budget-enforce.ts +18 -0
- package/.pi/lib/harness-schema-validate.ts +89 -0
- package/.pi/lib/harness-spawn-parse.ts +86 -0
- package/.pi/lib/harness-subagent-submit-path.ts +41 -0
- package/.pi/lib/harness-ui-state.ts +15 -2
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +2 -2
- package/.pi/prompts/harness-plan.md +34 -14
- package/.pi/prompts/harness-run.md +2 -2
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/scripts/harness-generate-model-router.mjs +118 -36
- package/.pi/scripts/harness-model-router-routing.test.mjs +97 -0
- package/.pi/scripts/harness-sync-model-router.mjs +15 -2
- package/.pi/scripts/harness-verify.mjs +31 -0
- package/.pi/scripts/harness_web/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/config.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/output.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/scrape.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_ddg.cpython-314.pyc +0 -0
- package/.pi/scripts/harness_web/__pycache__/search_searxng.cpython-314.pyc +0 -0
- package/CHANGELOG.md +21 -0
- package/package.json +4 -2
- package/vendor/pi-model-router/UPSTREAM_PIN.md +3 -1
- package/vendor/pi-model-router/extensions/commands.ts +4 -4
- package/vendor/pi-model-router/extensions/index.ts +21 -0
- package/vendor/pi-model-router/extensions/provider.ts +130 -79
- package/vendor/pi-model-router/extensions/routing.ts +148 -0
- package/vendor/pi-model-router/extensions/state.ts +3 -0
- package/vendor/pi-model-router/extensions/types.ts +9 -0
- package/vendor/pi-model-router/extensions/ui.ts +16 -2
- package/vendor/pi-subagents/src/subagents.ts +29 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* ultimate-pi harness wrapper around vendored pi-subagents.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { join } from "node:path";
|
|
5
6
|
import type {
|
|
6
7
|
ExtensionAPI,
|
|
7
8
|
ExtensionContext,
|
|
@@ -12,6 +13,13 @@ import {
|
|
|
12
13
|
type HarnessSubagentsOptions,
|
|
13
14
|
type SpawnAuthForward,
|
|
14
15
|
} from "../../../vendor/pi-subagents/src/subagents.js";
|
|
16
|
+
import {
|
|
17
|
+
getLatestRunContext,
|
|
18
|
+
getRunIdFromSession,
|
|
19
|
+
type HarnessPhase,
|
|
20
|
+
} from "../../lib/harness-run-context.js";
|
|
21
|
+
import { parseSpawnContextFromTask } from "../../lib/harness-spawn-parse.js";
|
|
22
|
+
import { harnessSubagentSubmitExtensionPath } from "../harness-subagent-submit.js";
|
|
15
23
|
import { refreshHarnessCocoindexIndex } from "./harness-cocoindex-refresh.js";
|
|
16
24
|
import { captureHarnessEvent } from "./harness-posthog.js";
|
|
17
25
|
import {
|
|
@@ -32,6 +40,51 @@ import {
|
|
|
32
40
|
|
|
33
41
|
const spawnBudget = createSpawnBudgetState();
|
|
34
42
|
let lastSessionId = "harness";
|
|
43
|
+
let spawnGroupCounter = 0;
|
|
44
|
+
type PendingSpawnTelemetry = {
|
|
45
|
+
harness_run_id: string;
|
|
46
|
+
run_id: string;
|
|
47
|
+
harness_plan_id: string;
|
|
48
|
+
harness_phase: HarnessPhase;
|
|
49
|
+
agent_ids: string[];
|
|
50
|
+
spawn_group_id: string;
|
|
51
|
+
};
|
|
52
|
+
let pendingSpawnTelemetry: PendingSpawnTelemetry | null = null;
|
|
53
|
+
|
|
54
|
+
function collectHarnessAgentIds(params: Record<string, unknown>): string[] {
|
|
55
|
+
const out = new Set<string>();
|
|
56
|
+
const maybe = params as {
|
|
57
|
+
agent?: string;
|
|
58
|
+
chain?: Array<{ agent?: string }>;
|
|
59
|
+
tasks?: Array<{ agent?: string }>;
|
|
60
|
+
aggregator?: { agent?: string };
|
|
61
|
+
};
|
|
62
|
+
if (typeof maybe.agent === "string" && maybe.agent.startsWith("harness/")) {
|
|
63
|
+
out.add(maybe.agent);
|
|
64
|
+
}
|
|
65
|
+
for (const item of maybe.chain ?? []) {
|
|
66
|
+
if (typeof item?.agent === "string" && item.agent.startsWith("harness/")) {
|
|
67
|
+
out.add(item.agent);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const item of maybe.tasks ?? []) {
|
|
71
|
+
if (typeof item?.agent === "string" && item.agent.startsWith("harness/")) {
|
|
72
|
+
out.add(item.agent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (
|
|
76
|
+
typeof maybe.aggregator?.agent === "string" &&
|
|
77
|
+
maybe.aggregator.agent.startsWith("harness/")
|
|
78
|
+
) {
|
|
79
|
+
out.add(maybe.aggregator.agent);
|
|
80
|
+
}
|
|
81
|
+
return Array.from(out.values()).sort();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function nextSpawnGroupId(sessionId: string): string {
|
|
85
|
+
spawnGroupCounter += 1;
|
|
86
|
+
return `${sessionId}-${Date.now()}-${spawnGroupCounter}`;
|
|
87
|
+
}
|
|
35
88
|
|
|
36
89
|
async function resolveHarnessSpawnAuth(
|
|
37
90
|
ctx: ExtensionContext,
|
|
@@ -58,8 +111,47 @@ async function resolveHarnessSpawnAuth(
|
|
|
58
111
|
export function createHarnessSubagentsExtension(
|
|
59
112
|
packageRoot: string,
|
|
60
113
|
): (pi: ExtensionAPI) => void {
|
|
114
|
+
const submitExtPath = harnessSubagentSubmitExtensionPath(packageRoot);
|
|
61
115
|
const options: HarnessSubagentsOptions = {
|
|
62
116
|
packageRoot,
|
|
117
|
+
harnessSubprocessExtensionPath: submitExtPath,
|
|
118
|
+
resolveSubprocessEnv: (task, agent) => {
|
|
119
|
+
if (!agent.name.startsWith("harness/")) return undefined;
|
|
120
|
+
const ctx = parseSpawnContextFromTask(task);
|
|
121
|
+
// #region agent log
|
|
122
|
+
fetch(
|
|
123
|
+
"http://127.0.0.1:7928/ingest/a5d40896-34cb-4f12-97db-df7ada0b22f0",
|
|
124
|
+
{
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"X-Debug-Session-Id": "2ca12b",
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
sessionId: "2ca12b",
|
|
132
|
+
hypothesisId: "H1",
|
|
133
|
+
location: "harness-subagents-bridge.ts:resolveSubprocessEnv",
|
|
134
|
+
message: "parsed spawn context for subprocess env",
|
|
135
|
+
data: {
|
|
136
|
+
agent: agent.name,
|
|
137
|
+
hasCtx: Boolean(ctx?.run_id),
|
|
138
|
+
run_id: ctx?.run_id ?? null,
|
|
139
|
+
run_dir: ctx?.run_dir ?? null,
|
|
140
|
+
taskPrefix: task.slice(0, 160),
|
|
141
|
+
},
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
).catch(() => {});
|
|
146
|
+
// #endregion
|
|
147
|
+
if (!ctx?.run_id) return undefined;
|
|
148
|
+
return {
|
|
149
|
+
HARNESS_RUN_ID: ctx.run_id,
|
|
150
|
+
HARNESS_RUN_DIR:
|
|
151
|
+
ctx.run_dir ??
|
|
152
|
+
join(packageRoot, ".pi", "harness", "runs", ctx.run_id),
|
|
153
|
+
};
|
|
154
|
+
},
|
|
63
155
|
defaultAgentScope: "both",
|
|
64
156
|
defaultConfirmProjectAgents: false,
|
|
65
157
|
truncateDetails: true,
|
|
@@ -69,11 +161,13 @@ export function createHarnessSubagentsExtension(
|
|
|
69
161
|
const { harnessCount } = countHarnessAgentsInRequest(
|
|
70
162
|
params as Parameters<typeof countHarnessAgentsInRequest>[0],
|
|
71
163
|
);
|
|
164
|
+
pendingSpawnTelemetry = null;
|
|
72
165
|
if (harnessCount > 0) {
|
|
73
166
|
const budget = checkHarnessSpawnBudget(spawnBudget, harnessCount);
|
|
74
167
|
if (!budget.ok) {
|
|
75
168
|
return { ok: false, message: budget.message };
|
|
76
169
|
}
|
|
170
|
+
const entries = ctx.sessionManager.getEntries();
|
|
77
171
|
const phase = inferPhaseForPrecheck(ctx.sessionManager.getEntries());
|
|
78
172
|
const pre = precheckHarnessSubagentSpawn(
|
|
79
173
|
params as Parameters<typeof precheckHarnessSubagentSpawn>[0],
|
|
@@ -91,6 +185,19 @@ export function createHarnessSubagentsExtension(
|
|
|
91
185
|
return { ok: false, message: refreshMsg };
|
|
92
186
|
}
|
|
93
187
|
}
|
|
188
|
+
const runCtx = getLatestRunContext(entries);
|
|
189
|
+
const runId =
|
|
190
|
+
runCtx?.run_id ??
|
|
191
|
+
getRunIdFromSession(entries, lastSessionId) ??
|
|
192
|
+
lastSessionId;
|
|
193
|
+
pendingSpawnTelemetry = {
|
|
194
|
+
harness_run_id: runId,
|
|
195
|
+
run_id: runId,
|
|
196
|
+
harness_plan_id: runCtx?.plan_id ?? "plan-unknown",
|
|
197
|
+
harness_phase: phase,
|
|
198
|
+
agent_ids: collectHarnessAgentIds(params as Record<string, unknown>),
|
|
199
|
+
spawn_group_id: nextSpawnGroupId(lastSessionId),
|
|
200
|
+
};
|
|
94
201
|
}
|
|
95
202
|
return { ok: true };
|
|
96
203
|
},
|
|
@@ -100,6 +207,16 @@ export function createHarnessSubagentsExtension(
|
|
|
100
207
|
captureHarnessEvent(lastSessionId, "harness_subagent_spawned", {
|
|
101
208
|
active_after: spawnBudget.active,
|
|
102
209
|
spawn_count: harnessCount,
|
|
210
|
+
harness_run_id: pendingSpawnTelemetry?.harness_run_id ?? lastSessionId,
|
|
211
|
+
run_id: pendingSpawnTelemetry?.run_id ?? lastSessionId,
|
|
212
|
+
harness_plan_id:
|
|
213
|
+
pendingSpawnTelemetry?.harness_plan_id ?? "plan-unknown",
|
|
214
|
+
harness_phase: pendingSpawnTelemetry?.harness_phase ?? "plan",
|
|
215
|
+
agent_ids: pendingSpawnTelemetry?.agent_ids ?? [],
|
|
216
|
+
agent_count: pendingSpawnTelemetry?.agent_ids.length ?? harnessCount,
|
|
217
|
+
spawn_group_id:
|
|
218
|
+
pendingSpawnTelemetry?.spawn_group_id ??
|
|
219
|
+
nextSpawnGroupId(lastSessionId),
|
|
103
220
|
});
|
|
104
221
|
},
|
|
105
222
|
onSpawnEnd: (harnessCount) => {
|
|
@@ -112,7 +229,17 @@ export function createHarnessSubagentsExtension(
|
|
|
112
229
|
mode,
|
|
113
230
|
duration_ms: durationMs,
|
|
114
231
|
agent_count: agents.length,
|
|
232
|
+
agent_ids: agents,
|
|
233
|
+
harness_run_id: pendingSpawnTelemetry?.harness_run_id ?? lastSessionId,
|
|
234
|
+
run_id: pendingSpawnTelemetry?.run_id ?? lastSessionId,
|
|
235
|
+
harness_plan_id:
|
|
236
|
+
pendingSpawnTelemetry?.harness_plan_id ?? "plan-unknown",
|
|
237
|
+
harness_phase: pendingSpawnTelemetry?.harness_phase ?? "plan",
|
|
238
|
+
spawn_group_id:
|
|
239
|
+
pendingSpawnTelemetry?.spawn_group_id ??
|
|
240
|
+
nextSpawnGroupId(lastSessionId),
|
|
115
241
|
});
|
|
242
|
+
pendingSpawnTelemetry = null;
|
|
116
243
|
},
|
|
117
244
|
};
|
|
118
245
|
|
|
@@ -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
|
|
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
|
|
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";
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
@@ -190,9 +232,9 @@ export function harnessPlanDebateEligibility(
|
|
|
190
232
|
confidenceAllowsLight(impl) &&
|
|
191
233
|
stackHasClearPrimary(stack)
|
|
192
234
|
) {
|
|
193
|
-
profile = "
|
|
235
|
+
profile = "fast";
|
|
194
236
|
rationale.push(
|
|
195
|
-
"
|
|
237
|
+
"fast: low risk, clear stack, high-confidence implementation approach",
|
|
196
238
|
);
|
|
197
239
|
} else if (risk === "med") {
|
|
198
240
|
profile = "standard";
|
|
@@ -200,7 +242,7 @@ export function harnessPlanDebateEligibility(
|
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
const required_focuses: PlanDebateFocus[] =
|
|
203
|
-
profile === "
|
|
245
|
+
profile === "fast" ? [...LIGHT_FOCUS] : [...PLAN_FOCUS_AREAS];
|
|
204
246
|
|
|
205
247
|
const caps = capsForProfile(profile);
|
|
206
248
|
|
|
@@ -210,5 +252,16 @@ export function harnessPlanDebateEligibility(
|
|
|
210
252
|
...caps,
|
|
211
253
|
human_required,
|
|
212
254
|
rationale,
|
|
255
|
+
review_gate_strategy: {
|
|
256
|
+
mode: profile === "fast" ? "consolidated" : "threaded",
|
|
257
|
+
profile,
|
|
258
|
+
required_focuses: [...required_focuses],
|
|
259
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
260
|
+
max_rounds: caps.max_rounds,
|
|
261
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
262
|
+
round_token_cap: caps.round_token_cap,
|
|
263
|
+
debate_global_cap: caps.debate_global_cap,
|
|
264
|
+
rationale: [...rationale],
|
|
265
|
+
},
|
|
213
266
|
};
|
|
214
267
|
}
|
|
@@ -9,12 +9,13 @@ import { parse as parseYaml } from "yaml";
|
|
|
9
9
|
|
|
10
10
|
export const PLAN_FOCUS_AREAS = ["spec", "wbs", "schedule", "quality"] as const;
|
|
11
11
|
export type PlanDebateFocus = (typeof PLAN_FOCUS_AREAS)[number];
|
|
12
|
+
export type PlanDebateRoundFocus = PlanDebateFocus | "all";
|
|
12
13
|
|
|
13
14
|
export interface PlanFocusCoverage {
|
|
14
15
|
covered: PlanDebateFocus[];
|
|
15
16
|
missing: PlanDebateFocus[];
|
|
16
17
|
rounds_by_focus: Partial<Record<PlanDebateFocus, number>>;
|
|
17
|
-
focus_by_round: Partial<Record<number,
|
|
18
|
+
focus_by_round: Partial<Record<number, PlanDebateRoundFocus>>;
|
|
18
19
|
last_review_gate_ready: boolean;
|
|
19
20
|
last_round_index: number;
|
|
20
21
|
}
|
|
@@ -34,8 +35,9 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
34
35
|
|
|
35
36
|
function focusFromDraft(
|
|
36
37
|
draft: Record<string, unknown>,
|
|
37
|
-
):
|
|
38
|
+
): PlanDebateRoundFocus | null {
|
|
38
39
|
const focus = String(draft.debate_round_focus ?? "").trim();
|
|
40
|
+
if (focus === "all") return "all";
|
|
39
41
|
if ((PLAN_FOCUS_AREAS as readonly string[]).includes(focus)) {
|
|
40
42
|
return focus as PlanDebateFocus;
|
|
41
43
|
}
|
|
@@ -56,14 +58,14 @@ export async function getPlanFocusCoverage(
|
|
|
56
58
|
const artifactsDir = join(runDir, "artifacts");
|
|
57
59
|
const covered = new Set<PlanDebateFocus>();
|
|
58
60
|
const rounds_by_focus: Partial<Record<PlanDebateFocus, number>> = {};
|
|
59
|
-
const focus_by_round: Partial<Record<number,
|
|
61
|
+
const focus_by_round: Partial<Record<number, PlanDebateRoundFocus>> = {};
|
|
60
62
|
let last_review_gate_ready = false;
|
|
61
63
|
let last_round_index = 0;
|
|
62
64
|
|
|
63
65
|
let files: string[] = [];
|
|
64
66
|
try {
|
|
65
67
|
files = (await readdir(artifactsDir)).filter((f) =>
|
|
66
|
-
/^review-round
|
|
68
|
+
/^review-round(?:-r\d+|-consolidated)\.yaml$/i.test(f),
|
|
67
69
|
);
|
|
68
70
|
} catch {
|
|
69
71
|
return {
|
|
@@ -77,9 +79,12 @@ export async function getPlanFocusCoverage(
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
for (const name of files.sort()) {
|
|
80
|
-
const
|
|
82
|
+
const consolidated = /^review-round-consolidated\.yaml$/i.test(name);
|
|
83
|
+
const m = consolidated
|
|
84
|
+
? ["review-round-consolidated.yaml", "1"]
|
|
85
|
+
: /^review-round-r(\d+)\.yaml$/i.exec(name);
|
|
81
86
|
if (!m) continue;
|
|
82
|
-
const roundIndex = Number(m[1]);
|
|
87
|
+
const roundIndex = consolidated ? 1 : Number(m[1]);
|
|
83
88
|
if (roundIndex > last_round_index) last_round_index = roundIndex;
|
|
84
89
|
const raw = await readFile(join(artifactsDir, name), "utf-8");
|
|
85
90
|
let draft: Record<string, unknown>;
|
|
@@ -90,8 +95,15 @@ export async function getPlanFocusCoverage(
|
|
|
90
95
|
}
|
|
91
96
|
const focus = focusFromDraft(draft);
|
|
92
97
|
if (focus) {
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
if (focus === "all") {
|
|
99
|
+
for (const requiredFocus of required) {
|
|
100
|
+
covered.add(requiredFocus);
|
|
101
|
+
rounds_by_focus[requiredFocus] = roundIndex;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
covered.add(focus);
|
|
105
|
+
rounds_by_focus[focus] = roundIndex;
|
|
106
|
+
}
|
|
95
107
|
focus_by_round[roundIndex] = focus;
|
|
96
108
|
}
|
|
97
109
|
if (roundIndex === last_round_index) {
|
|
@@ -138,7 +150,7 @@ export function planDebateOutcomeComplete(
|
|
|
138
150
|
export async function readDebateRoundFocus(
|
|
139
151
|
runDir: string,
|
|
140
152
|
roundIndex: number,
|
|
141
|
-
): Promise<
|
|
153
|
+
): Promise<PlanDebateRoundFocus | null> {
|
|
142
154
|
const path = join(runDir, "artifacts", `review-round-r${roundIndex}.yaml`);
|
|
143
155
|
if (!(await fileExists(path))) return null;
|
|
144
156
|
try {
|
|
@@ -5,19 +5,29 @@
|
|
|
5
5
|
import { constants } from "node:fs";
|
|
6
6
|
import { access, readFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
import { isHarnessBudgetEnforceOn } from "../../lib/harness-budget-enforce.js";
|
|
8
9
|
import { capsForDebate } from "./debate-bus-core.js";
|
|
10
|
+
import type { DebateEligibilityResult } from "./plan-debate-eligibility.js";
|
|
9
11
|
import {
|
|
10
12
|
getPlanFocusCoverage,
|
|
11
13
|
type PlanDebateFocus,
|
|
12
14
|
planDebateOutcomeComplete,
|
|
13
15
|
} from "./plan-debate-focus.js";
|
|
14
16
|
import { planDebateIdForRun } from "./plan-debate-id.js";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
laneArtifactPathsForConsolidatedRound,
|
|
19
|
+
laneArtifactPathsForRound,
|
|
20
|
+
} from "./plan-debate-lanes.js";
|
|
16
21
|
import {
|
|
17
22
|
getMessengerRoundState,
|
|
18
23
|
loadMessengerState,
|
|
19
24
|
messengerRoundDebateReady,
|
|
20
25
|
} from "./plan-messenger.js";
|
|
26
|
+
import {
|
|
27
|
+
CONSOLIDATED_REVIEW_ARTIFACT,
|
|
28
|
+
isConsolidatedReviewStrategy,
|
|
29
|
+
planReviewGateStrategyFromEligibility,
|
|
30
|
+
} from "./plan-review-gate.js";
|
|
21
31
|
|
|
22
32
|
async function fileExists(path: string): Promise<boolean> {
|
|
23
33
|
try {
|
|
@@ -63,6 +73,7 @@ export interface PlanDebateGateResult {
|
|
|
63
73
|
export async function validatePlanDebateGate(
|
|
64
74
|
projectRoot: string,
|
|
65
75
|
runId: string,
|
|
76
|
+
eligibility?: DebateEligibilityResult,
|
|
66
77
|
): Promise<PlanDebateGateResult> {
|
|
67
78
|
const errors: string[] = [];
|
|
68
79
|
const warnings: string[] = [];
|
|
@@ -76,6 +87,33 @@ export async function validatePlanDebateGate(
|
|
|
76
87
|
? messenger.required_focuses
|
|
77
88
|
: (["spec", "wbs", "schedule", "quality"] as const);
|
|
78
89
|
const caps = capsForDebate(debateId, debateProfile);
|
|
90
|
+
const reviewStrategy =
|
|
91
|
+
eligibility != null
|
|
92
|
+
? planReviewGateStrategyFromEligibility(eligibility)
|
|
93
|
+
: messenger?.review_gate_mode === "consolidated"
|
|
94
|
+
? {
|
|
95
|
+
mode: "consolidated" as const,
|
|
96
|
+
profile: debateProfile as DebateEligibilityResult["profile"],
|
|
97
|
+
required_focuses: [...requiredFocuses],
|
|
98
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
99
|
+
max_rounds: caps.max_rounds,
|
|
100
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
101
|
+
round_token_cap: caps.round_token_cap,
|
|
102
|
+
debate_global_cap: caps.debate_global_cap,
|
|
103
|
+
rationale: ["messenger review_gate_mode=consolidated"],
|
|
104
|
+
}
|
|
105
|
+
: {
|
|
106
|
+
mode: "threaded" as const,
|
|
107
|
+
profile: debateProfile as DebateEligibilityResult["profile"],
|
|
108
|
+
required_focuses: [...requiredFocuses],
|
|
109
|
+
min_focus_rounds: caps.min_focus_rounds,
|
|
110
|
+
max_rounds: caps.max_rounds,
|
|
111
|
+
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
112
|
+
round_token_cap: caps.round_token_cap,
|
|
113
|
+
debate_global_cap: caps.debate_global_cap,
|
|
114
|
+
rationale: [],
|
|
115
|
+
};
|
|
116
|
+
const consolidated = isConsolidatedReviewStrategy(reviewStrategy);
|
|
79
117
|
const coverage = await getPlanFocusCoverage(runDir, { requiredFocuses });
|
|
80
118
|
const dialogueOpts = {
|
|
81
119
|
max_exchanges_per_round: caps.max_exchanges_per_round,
|
|
@@ -88,39 +126,73 @@ export async function validatePlanDebateGate(
|
|
|
88
126
|
errors.push("last submitted review round has review_gate_ready !== true");
|
|
89
127
|
}
|
|
90
128
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
];
|
|
98
|
-
for (const r of roundIndices) {
|
|
99
|
-
const focus = coverage.focus_by_round[r] ?? null;
|
|
100
|
-
for (const rel of laneArtifactPathsForRound(r, focus)) {
|
|
129
|
+
if (consolidated) {
|
|
130
|
+
const absConsolidated = join(runDir, CONSOLIDATED_REVIEW_ARTIFACT);
|
|
131
|
+
if (!(await fileExists(absConsolidated))) {
|
|
132
|
+
errors.push(`missing ${CONSOLIDATED_REVIEW_ARTIFACT}`);
|
|
133
|
+
}
|
|
134
|
+
for (const rel of laneArtifactPathsForConsolidatedRound()) {
|
|
101
135
|
const abs = join(runDir, rel);
|
|
102
136
|
if (!(await fileExists(abs))) {
|
|
103
137
|
errors.push(`missing ${rel}`);
|
|
104
138
|
}
|
|
105
139
|
}
|
|
106
|
-
const roundState = await getMessengerRoundState(runDir,
|
|
107
|
-
const requireSprint = focus === "quality" || r >= 4;
|
|
140
|
+
const roundState = await getMessengerRoundState(runDir, 1);
|
|
108
141
|
const messengerCheck = messengerRoundDebateReady(
|
|
109
142
|
roundState,
|
|
110
|
-
|
|
143
|
+
true,
|
|
111
144
|
dialogueOpts,
|
|
112
145
|
);
|
|
113
146
|
if (!messengerCheck.ok) {
|
|
114
147
|
for (const e of messengerCheck.errors) {
|
|
115
|
-
errors.push(`round
|
|
148
|
+
errors.push(`consolidated round messenger: ${e}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
const roundIndices = [
|
|
153
|
+
...new Set(
|
|
154
|
+
Object.values(coverage.rounds_by_focus).filter(
|
|
155
|
+
(v): v is number => typeof v === "number",
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
];
|
|
159
|
+
for (const r of roundIndices) {
|
|
160
|
+
const focus = coverage.focus_by_round[r] ?? null;
|
|
161
|
+
for (const rel of laneArtifactPathsForRound(r, focus)) {
|
|
162
|
+
const abs = join(runDir, rel);
|
|
163
|
+
if (!(await fileExists(abs))) {
|
|
164
|
+
errors.push(`missing ${rel}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const roundState = await getMessengerRoundState(runDir, r);
|
|
168
|
+
const requireSprint = focus === "quality" || r >= 4;
|
|
169
|
+
const messengerCheck = messengerRoundDebateReady(
|
|
170
|
+
roundState,
|
|
171
|
+
requireSprint,
|
|
172
|
+
dialogueOpts,
|
|
173
|
+
);
|
|
174
|
+
if (!messengerCheck.ok) {
|
|
175
|
+
for (const e of messengerCheck.errors) {
|
|
176
|
+
errors.push(`round ${r} messenger: ${e}`);
|
|
177
|
+
}
|
|
116
178
|
}
|
|
117
179
|
}
|
|
118
180
|
}
|
|
119
181
|
|
|
120
|
-
if (
|
|
182
|
+
if (
|
|
183
|
+
isHarnessBudgetEnforceOn() &&
|
|
184
|
+
coverage.last_round_index > caps.max_rounds
|
|
185
|
+
) {
|
|
121
186
|
errors.push(
|
|
122
187
|
`round_count ${coverage.last_round_index} exceeds max_rounds ${caps.max_rounds}`,
|
|
123
188
|
);
|
|
189
|
+
} else if (
|
|
190
|
+
!isHarnessBudgetEnforceOn() &&
|
|
191
|
+
coverage.last_round_index > caps.max_rounds
|
|
192
|
+
) {
|
|
193
|
+
warnings.push(
|
|
194
|
+
`round_count ${coverage.last_round_index} exceeds advisory max_rounds ${caps.max_rounds} (budget enforce off)`,
|
|
195
|
+
);
|
|
124
196
|
}
|
|
125
197
|
|
|
126
198
|
if (!messenger) {
|
|
@@ -192,7 +264,9 @@ export async function validatePlanDebateGate(
|
|
|
192
264
|
}
|
|
193
265
|
|
|
194
266
|
export function isReviewRoundArtifactPath(relPath: string): boolean {
|
|
195
|
-
|
|
196
|
-
|
|
267
|
+
const norm = relPath.replace(/\\/g, "/");
|
|
268
|
+
return (
|
|
269
|
+
/^artifacts\/review-round-r\d+\.yaml$/i.test(norm) ||
|
|
270
|
+
norm === CONSOLIDATED_REVIEW_ARTIFACT
|
|
197
271
|
);
|
|
198
272
|
}
|
|
@@ -45,6 +45,21 @@ export function laneArtifactPath(
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Apply messenger side effects when artifact YAML was already written via submit tool. */
|
|
49
|
+
export async function applyDebateLaneFromDoc(opts: {
|
|
50
|
+
runDir: string;
|
|
51
|
+
lane: DebateLaneKind;
|
|
52
|
+
doc: Record<string, unknown>;
|
|
53
|
+
roundIndex?: number;
|
|
54
|
+
}): Promise<ApplyDebateLaneResult> {
|
|
55
|
+
return applyDebateLane({
|
|
56
|
+
runDir: opts.runDir,
|
|
57
|
+
lane: opts.lane,
|
|
58
|
+
content: JSON.stringify(opts.doc),
|
|
59
|
+
roundIndex: opts.roundIndex,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function extractClaimIds(doc: Record<string, unknown>): string[] {
|
|
49
64
|
const explicit = doc.messenger_claim_ids;
|
|
50
65
|
if (Array.isArray(explicit)) {
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Shared Review Gate lane list for a round (gate + round-status).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { PlanDebateRoundFocus } from "./plan-debate-focus.js";
|
|
6
6
|
import type { DebateLaneKind } from "./plan-debate-lane.js";
|
|
7
7
|
|
|
8
8
|
/** Lanes required before review-integrator for this round. */
|
|
9
9
|
export function lanesForRound(
|
|
10
10
|
roundIndex: number,
|
|
11
|
-
debateRoundFocus?:
|
|
11
|
+
debateRoundFocus?: PlanDebateRoundFocus | null,
|
|
12
12
|
): DebateLaneKind[] {
|
|
13
13
|
const lanes: DebateLaneKind[] = ["validation-turn", "adversary-brief"];
|
|
14
14
|
if (roundIndex === 1) {
|
|
@@ -23,7 +23,7 @@ export function lanesForRound(
|
|
|
23
23
|
/** Relative artifact paths for lane YAML + review-round. */
|
|
24
24
|
export function laneArtifactPathsForRound(
|
|
25
25
|
roundIndex: number,
|
|
26
|
-
debateRoundFocus?:
|
|
26
|
+
debateRoundFocus?: PlanDebateRoundFocus | null,
|
|
27
27
|
): string[] {
|
|
28
28
|
const paths = lanesForRound(roundIndex, debateRoundFocus).map((lane) => {
|
|
29
29
|
switch (lane) {
|
|
@@ -42,3 +42,27 @@ export function laneArtifactPathsForRound(
|
|
|
42
42
|
paths.push(`artifacts/review-round-r${roundIndex}.yaml`);
|
|
43
43
|
return paths;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Lanes for consolidated Review Gate (single round, parallel-friendly). */
|
|
47
|
+
export function lanesForConsolidatedRound(): DebateLaneKind[] {
|
|
48
|
+
return ["validation-turn", "adversary-brief", "sprint-audit"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function laneArtifactPathsForConsolidatedRound(): string[] {
|
|
52
|
+
const roundIndex = 1;
|
|
53
|
+
return [
|
|
54
|
+
...lanesForConsolidatedRound().map((lane) => {
|
|
55
|
+
switch (lane) {
|
|
56
|
+
case "validation-turn":
|
|
57
|
+
return `artifacts/validation-turn-r${roundIndex}.yaml`;
|
|
58
|
+
case "adversary-brief":
|
|
59
|
+
return `artifacts/adversary-brief-r${roundIndex}.yaml`;
|
|
60
|
+
case "sprint-audit":
|
|
61
|
+
return `artifacts/sprint-audit-r${roundIndex}.yaml`;
|
|
62
|
+
default:
|
|
63
|
+
return `artifacts/${lane}-r${roundIndex}.yaml`;
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
"artifacts/review-round-consolidated.yaml",
|
|
67
|
+
];
|
|
68
|
+
}
|