feat: enforce approved-plan continuity at reply closure

This commit is contained in:
Eve
2026-04-24 13:56:50 +08:00
parent 0146d36b51
commit e6261fe2c0

View File

@@ -8,6 +8,7 @@ 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;
const APPROVED_PLAN_CONTINUITY_TIMEOUT_MS = 8000;
type AutoChainPlanResult = {
plannerStatus: string;
@@ -30,6 +31,14 @@ type GateLockResult = {
allowedResponseModes?: string[];
};
type ApprovedPlanContinuityResult = {
ok: boolean;
status: string;
verdict: string;
reason?: string;
gate?: string;
};
function clamp(s: string, max = 1200): string {
if (!s) return s;
if (s.length <= max) return s;
@@ -328,6 +337,68 @@ async function runAutoChainPlanner(workspaceDir: string, gateLockResult: GateLoc
return runJsonScript(plannerPath, workspaceDir, input, LONG_TASK_AUTO_CHAIN_PLANNER_TIMEOUT_MS);
}
function buildApprovedPlanContinuityInput(wrapperResult: any, autoChainPlanResult: AutoChainPlanResult | null): Record<string, unknown> | null {
if (!wrapperResult || wrapperResult.classification !== "long_task") return null;
const wrapperNextAction = wrapperResult?.nextDerivedAction ?? wrapperResult?.derivedAction ?? null;
const plannerDerivedAction = autoChainPlanResult?.derivedAction && autoChainPlanResult.derivedAction !== "none"
? {
type: autoChainPlanResult.dispatchMode ?? "no_dispatch",
action: autoChainPlanResult.derivedAction,
}
: null;
const nextDerivedAction = wrapperNextAction ?? plannerDerivedAction;
if (nextDerivedAction == null) return null;
const replyClosureState = typeof wrapperResult?.replyClosureState === "string"
? wrapperResult.replyClosureState
: (wrapperResult?.handoff?.mode === "button_path" ? "waiting_user" : "completed");
const dispatchReceipt = wrapperResult?.dispatchReceipt ?? (autoChainPlanResult?.dispatchMode && autoChainPlanResult.dispatchMode !== "no_dispatch"
? {
dispatchMode: autoChainPlanResult.dispatchMode,
derivedAction: autoChainPlanResult.derivedAction,
}
: null);
return {
planId: wrapperResult?.planId ?? "hook-preflight-approved-plan",
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? "hook-preflight-task",
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? "complete" : null),
nextDerivedAction,
replyClosureState,
dispatchReceipt,
};
}
async function runApprovedPlanContinuityGate(workspaceDir: string, wrapperResult: any, autoChainPlanResult: AutoChainPlanResult | null): Promise<ApprovedPlanContinuityResult | null> {
const continuityPath = path.join(workspaceDir, "scripts", "approved_plan_continuity_gate.mjs");
const input = buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult);
if (!input) return null;
return runJsonScript(continuityPath, workspaceDir, input, APPROVED_PLAN_CONTINUITY_TIMEOUT_MS);
}
function buildApprovedPlanContinuityBlock(result: ApprovedPlanContinuityResult | null): string {
if (!result) return "";
const lines = [
"[APPROVED_PLAN_CONTINUITY_GATE]",
`status=${result.status}`,
`verdict=${result.verdict}`,
];
if (result.reason) lines.push(`reason=${result.reason}`);
if (result.ok === false) {
lines.push("- HARD_GATE: Do not close out this reply as normal completion.");
lines.push("- HARD_GATE: Route back to continuity failure until a real next dispatch receipt exists, unless closure state is waiting_user, blocked, or pending_verification.");
}
lines.push("[/APPROVED_PLAN_CONTINUITY_GATE]", "");
return lines.join("\n");
}
function buildAutoChainPlanBlock(planResult: AutoChainPlanResult | null): string {
if (!planResult) {
return [
@@ -473,8 +544,11 @@ const forceRecall = async (event: any) => {
]);
const gateLockResult = wrapperResult ? await runLongTaskGateLock(workspaceDir, wrapperResult) : null;
const autoChainPlanResult = wrapperResult ? await runAutoChainPlanner(workspaceDir, gateLockResult, wrapperResult) : null;
const approvedPlanContinuityResult = wrapperResult
? await runApprovedPlanContinuityGate(workspaceDir, wrapperResult, autoChainPlanResult)
: null;
if (!rulebook && !soul && !wrapperResult && !gateLockResult && !autoChainPlanResult) return;
if (!rulebook && !soul && !wrapperResult && !gateLockResult && !autoChainPlanResult && !approvedPlanContinuityResult) return;
const wrapperBlock = wrapperResult
? [
@@ -500,6 +574,7 @@ const forceRecall = async (event: any) => {
const gateLockBlock = buildGateLockBlock(gateLockResult);
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
const approvedPlanContinuityBlock = buildApprovedPlanContinuityBlock(approvedPlanContinuityResult);
const recallBlock = [
"[RECALL_GATE] Mandatory recall before ANY technical action/tool use.",
@@ -509,6 +584,7 @@ const forceRecall = async (event: any) => {
wrapperBlock || null,
gateLockBlock,
autoChainPlanBlock,
approvedPlanContinuityBlock || null,
rulebook ? `RULEBOOK (source: ${rulebookPath}):\n${clamp(rulebook, 1200)}` : null,
soul ? `SOUL (source: ${soulPath}):\n${clamp(soul, 1200)}` : null,
"[/RECALL_GATE]",