feat: add execution-layer auto-chain dry-run planner

This commit is contained in:
Eve
2026-04-23 22:40:08 +08:00
parent 245f7385d9
commit 13bc748a83
2 changed files with 118 additions and 2 deletions

View File

@@ -7,6 +7,16 @@ import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const LONG_TASK_WRAPPER_TIMEOUT_MS = 8000;
const LONG_TASK_GATE_LOCK_TIMEOUT_MS = 8000;
const LONG_TASK_AUTO_CHAIN_PLANNER_TIMEOUT_MS = 8000;
type AutoChainPlanResult = {
plannerStatus: string;
derivedAction: string;
dispatchMode: string;
reason: string;
requiredEvidence?: string[];
autoChainAllowed: boolean;
};
type GateLockResult = {
gateRequired: boolean;
@@ -210,6 +220,86 @@ async function runLongTaskGateLock(workspaceDir: string, wrapperResult: any): Pr
return runJsonScript(gateLockPath, workspaceDir, input, LONG_TASK_GATE_LOCK_TIMEOUT_MS);
}
function buildAutoChainPlannerInput(gateLockResult: GateLockResult | null, wrapperResult: any): Record<string, unknown> {
const requiredNextAction = typeof wrapperResult?.requiredNextAction === "string"
? wrapperResult.requiredNextAction.trim()
: "";
const plannerInput: Record<string, unknown> = {
gateStatus: gateLockResult?.gateStatus ?? "not_applicable",
actorStage: "hook_preflight",
requiredNextAction,
};
if (!requiredNextAction) return plannerInput;
if (requiredNextAction === "dispatch_follow_up_subagent") {
plannerInput.actorStage = "implementer_result";
plannerInput.requiredNextAction = "request_spec_review";
if (wrapperResult?.autoChainDispatchEvidence && typeof wrapperResult.autoChainDispatchEvidence === "object" && !Array.isArray(wrapperResult.autoChainDispatchEvidence)) {
plannerInput.executionEvidence = wrapperResult.autoChainDispatchEvidence;
}
return plannerInput;
}
if (requiredNextAction === "dispatch_code_quality_review") {
plannerInput.actorStage = "spec_review";
plannerInput.requiredNextAction = "request_code_quality_review";
plannerInput.reviewOutcome = "pass";
plannerInput.reviewEvidence = wrapperResult?.reviewEvidence && typeof wrapperResult.reviewEvidence === "object" && !Array.isArray(wrapperResult.reviewEvidence)
? wrapperResult.reviewEvidence
: { source: "hook_preflight", verdict: "pass" };
return plannerInput;
}
if (requiredNextAction === "dispatch_fix_slice") {
plannerInput.actorStage = "review_result";
plannerInput.requiredNextAction = "fix_review_findings";
plannerInput.blocker = typeof wrapperResult?.silentLaunchReason === "string" && wrapperResult.silentLaunchReason.trim()
? wrapperResult.silentLaunchReason.trim()
: "hook_preflight_blocker";
plannerInput.blockerEvidence = wrapperResult?.blockerEvidence && typeof wrapperResult.blockerEvidence === "object" && !Array.isArray(wrapperResult.blockerEvidence)
? wrapperResult.blockerEvidence
: { source: "hook_preflight", blocker: plannerInput.blocker };
return plannerInput;
}
return plannerInput;
}
async function runAutoChainPlanner(workspaceDir: string, gateLockResult: GateLockResult | null, wrapperResult: any): Promise<AutoChainPlanResult | null> {
if (!wrapperResult || wrapperResult.classification !== "long_task") return null;
const plannerPath = path.join(workspaceDir, "scripts", "plan_long_task_auto_chain.mjs");
const input = buildAutoChainPlannerInput(gateLockResult, wrapperResult);
return runJsonScript(plannerPath, workspaceDir, input, LONG_TASK_AUTO_CHAIN_PLANNER_TIMEOUT_MS);
}
function buildAutoChainPlanBlock(planResult: AutoChainPlanResult | null): string {
if (!planResult) {
return [
"[LONG_TASK_AUTO_CHAIN_PLAN]",
"plannerStatus=degraded",
"derivedAction=none",
"dispatchMode=no_dispatch",
"autoChainAllowed=false",
"reason=auto-chain planner unavailable during hook preflight",
"[/LONG_TASK_AUTO_CHAIN_PLAN]",
"",
].join("\n");
}
return [
"[LONG_TASK_AUTO_CHAIN_PLAN]",
`plannerStatus=${planResult.plannerStatus}`,
`derivedAction=${planResult.derivedAction}`,
`dispatchMode=${planResult.dispatchMode}`,
`autoChainAllowed=${planResult.autoChainAllowed}`,
`reason=${planResult.reason}`,
...((planResult.requiredEvidence ?? []).map((entry) => `requiredEvidence=${entry}`)),
"[/LONG_TASK_AUTO_CHAIN_PLAN]",
"",
].join("\n");
}
function buildWrapperEnforcement(wrapperResult: any): string[] {
const lines = [
"- Treat this as ingress preflight guidance from the wrapper MVP.",
@@ -327,8 +417,9 @@ const forceRecall = async (event: any) => {
runLongTaskWrapper(workspaceDir, ctx),
]);
const gateLockResult = wrapperResult ? await runLongTaskGateLock(workspaceDir, wrapperResult) : null;
const autoChainPlanResult = wrapperResult ? await runAutoChainPlanner(workspaceDir, gateLockResult, wrapperResult) : null;
if (!rulebook && !soul && !wrapperResult && !gateLockResult) return;
if (!rulebook && !soul && !wrapperResult && !gateLockResult && !autoChainPlanResult) return;
const wrapperBlock = wrapperResult
? [
@@ -353,6 +444,7 @@ const forceRecall = async (event: any) => {
: "";
const gateLockBlock = buildGateLockBlock(gateLockResult);
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
const recallBlock = [
"[RECALL_GATE] Mandatory recall before ANY technical action/tool use.",
@@ -361,6 +453,7 @@ const forceRecall = async (event: any) => {
"",
wrapperBlock || null,
gateLockBlock,
autoChainPlanBlock,
rulebook ? `RULEBOOK (source: ${rulebookPath}):\n${clamp(rulebook, 1200)}` : null,
soul ? `SOUL (source: ${soulPath}):\n${clamp(soul, 1200)}` : null,
"[/RECALL_GATE]",