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.
- package/.agents/skills/harness-context/SKILL.md +13 -6
- package/.agents/skills/harness-debate-plan/SKILL.md +37 -20
- package/.agents/skills/harness-eval/SKILL.md +6 -21
- package/.agents/skills/harness-governor/SKILL.md +4 -3
- package/.agents/skills/harness-orchestration/SKILL.md +39 -51
- package/.agents/skills/harness-plan/SKILL.md +23 -12
- package/.agents/skills/harness-review/SKILL.md +52 -0
- package/.agents/skills/harness-sentrux-setup/SKILL.md +13 -1
- package/.agents/skills/harness-steer/SKILL.md +14 -0
- package/.pi/agents/harness/adversary.md +3 -10
- package/.pi/agents/harness/evaluator.md +3 -12
- package/.pi/agents/harness/executor.md +12 -14
- package/.pi/agents/harness/planning/decompose.md +7 -4
- package/.pi/agents/harness/planning/hypothesis-validator.md +2 -0
- package/.pi/agents/harness/planning/hypothesis.md +4 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +1 -1
- package/.pi/agents/harness/planning/plan-adversary.md +2 -0
- package/.pi/agents/harness/planning/plan-evaluator.md +2 -0
- package/.pi/agents/harness/planning/plan-synthesizer.md +25 -0
- package/.pi/agents/harness/planning/planning-context.md +48 -0
- package/.pi/agents/harness/planning/review-integrator.md +2 -0
- package/.pi/agents/harness/planning/scout-graphify.md +3 -1
- package/.pi/agents/harness/planning/scout-semantic.md +3 -1
- package/.pi/agents/harness/planning/scout-structure.md +3 -1
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +2 -0
- package/.pi/agents/harness/sentrux-steward.md +51 -0
- package/.pi/extensions/00-posthog-network-bootstrap.ts +11 -0
- package/.pi/extensions/harness-debate-tools.ts +12 -3
- package/.pi/extensions/harness-live-widget.ts +27 -1
- package/.pi/extensions/harness-plan-approval.ts +62 -56
- package/.pi/extensions/harness-run-context.ts +553 -84
- package/.pi/extensions/harness-subagent-submit.ts +43 -33
- package/.pi/extensions/harness-telemetry.ts +29 -4
- package/.pi/extensions/lib/debate-bus-core.ts +15 -9
- package/.pi/extensions/lib/harness-artifact-gate.ts +182 -0
- package/.pi/extensions/lib/harness-posthog.ts +9 -5
- package/.pi/extensions/lib/harness-spawn-topology.ts +188 -0
- package/.pi/extensions/lib/harness-subagent-auth.ts +105 -19
- package/.pi/extensions/lib/harness-subagent-policy.ts +37 -19
- package/.pi/extensions/lib/harness-subagent-precheck.ts +35 -9
- package/.pi/extensions/lib/harness-subagent-submit-pipeline.ts +66 -2
- package/.pi/extensions/lib/harness-subagent-submit-registry.ts +21 -3
- package/.pi/extensions/lib/harness-subagents-bridge.ts +91 -28
- package/.pi/extensions/lib/harness-subprocess-bootstrap.ts +73 -0
- package/.pi/extensions/lib/plan-approval/create-plan.ts +2 -3
- package/.pi/extensions/lib/plan-approval/resolve-disk.ts +102 -0
- package/.pi/extensions/lib/plan-approval/schema.ts +22 -8
- package/.pi/extensions/lib/plan-approval/types.ts +1 -1
- package/.pi/extensions/lib/plan-approval/validate.ts +2 -2
- package/.pi/extensions/lib/plan-approval-readiness.ts +241 -0
- package/.pi/extensions/lib/plan-debate-eligibility.ts +67 -7
- package/.pi/extensions/lib/plan-debate-focus.ts +21 -9
- package/.pi/extensions/lib/plan-debate-gate.ts +101 -17
- package/.pi/extensions/lib/plan-debate-lanes.ts +57 -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 +59 -0
- package/.pi/extensions/lib/posthog-client.ts +76 -0
- package/.pi/extensions/policy-gate.ts +24 -19
- package/.pi/extensions/trace-recorder.ts +1 -0
- package/.pi/harness/agents.manifest.json +24 -16
- package/.pi/harness/corpus/cron.example +8 -0
- package/.pi/harness/corpus/graphify-kb-updater.config.json +159 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.env.template +4 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.service +17 -0
- package/.pi/harness/corpus/systemd/graphify-kb-updater.timer +11 -0
- package/.pi/harness/docs/adrs/0001-harness-constitution.md +2 -1
- package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +7 -6
- package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +6 -1
- package/.pi/harness/docs/adrs/0031-harness-run-context.md +1 -1
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +7 -0
- package/.pi/harness/docs/adrs/0034-darwin-plan-research-pipeline.md +3 -3
- package/.pi/harness/docs/adrs/0036-implementation-research-and-selective-debate.md +8 -5
- package/.pi/harness/docs/adrs/0039-harness-post-run-review-gate.md +47 -0
- package/.pi/harness/docs/adrs/0040-practice-grounded-orchestration.md +40 -0
- package/.pi/harness/docs/adrs/0041-intelligent-planning-reconnaissance.md +39 -0
- package/.pi/harness/docs/adrs/0042-agent-native-orchestration.md +35 -0
- package/.pi/harness/docs/adrs/0043-path-first-harness-tools.md +38 -0
- package/.pi/harness/docs/adrs/0044-harness-steer-loop.md +36 -0
- package/.pi/harness/docs/adrs/README.md +10 -0
- package/.pi/harness/docs/graphify-kb-updater-runbook.md +157 -0
- package/.pi/harness/docs/practice-map.md +110 -0
- package/.pi/harness/env.harness.template +5 -3
- 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/sentrux-stub.json +1 -1
- package/.pi/harness/evals/smoke/smoke-harness-plan.mjs +43 -17
- package/.pi/harness/specs/README.md +1 -1
- package/.pi/harness/specs/harness-run-context.schema.json +11 -0
- package/.pi/harness/specs/harness-spawn-context.schema.json +14 -0
- package/.pi/harness/specs/plan-execution-plan.schema.json +39 -1
- package/.pi/harness/specs/plan-packet.schema.json +4 -0
- package/.pi/harness/specs/plan-phase-status.schema.json +17 -0
- package/.pi/harness/specs/plan-phase-waiver.schema.json +25 -0
- package/.pi/harness/specs/plan-planning-context.schema.json +50 -0
- package/.pi/harness/specs/plan-review-round-draft.schema.json +1 -1
- package/.pi/harness/specs/repair-brief.schema.json +45 -0
- package/.pi/harness/specs/review-outcome.schema.json +46 -0
- package/.pi/harness/specs/sentrux-manifest-proposal.schema.json +80 -0
- package/.pi/harness/specs/sentrux-signal.schema.json +43 -0
- package/.pi/harness/specs/steer-state.schema.json +20 -0
- package/.pi/lib/harness-context-mode-policy.ts +256 -0
- package/.pi/lib/harness-repair-brief.ts +145 -0
- package/.pi/lib/harness-run-context.ts +591 -32
- package/.pi/lib/harness-ui-state.ts +87 -9
- package/.pi/model-router.example.json +13 -4
- package/.pi/prompts/harness-auto.md +9 -9
- package/.pi/prompts/harness-critic.md +3 -30
- package/.pi/prompts/harness-eval.md +4 -37
- package/.pi/prompts/harness-plan.md +139 -57
- package/.pi/prompts/harness-review.md +150 -15
- package/.pi/prompts/harness-run.md +62 -10
- package/.pi/prompts/harness-sentrux-steward.md +55 -0
- package/.pi/prompts/harness-setup.md +4 -4
- package/.pi/prompts/harness-steer.md +30 -0
- package/.pi/scripts/graphify-kb-updater.mjs +358 -0
- 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 +51 -6
- package/.pi/scripts/harness-web-policy-guard.mjs +68 -0
- package/.pi/scripts/validate-plan-dag.mjs +3 -3
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +22 -0
- package/package.json +5 -4
- 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/.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
|
-
"
|
|
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.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" | "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
|
-
|
|
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 &&
|
|
@@ -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
|
|
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 === "
|
|
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
|
}
|