feat: enforce approved-plan continuity at reply closure
This commit is contained in:
@@ -8,6 +8,7 @@ const execFileAsync = promisify(execFile);
|
|||||||
const LONG_TASK_WRAPPER_TIMEOUT_MS = 8000;
|
const LONG_TASK_WRAPPER_TIMEOUT_MS = 8000;
|
||||||
const LONG_TASK_GATE_LOCK_TIMEOUT_MS = 8000;
|
const LONG_TASK_GATE_LOCK_TIMEOUT_MS = 8000;
|
||||||
const LONG_TASK_AUTO_CHAIN_PLANNER_TIMEOUT_MS = 8000;
|
const LONG_TASK_AUTO_CHAIN_PLANNER_TIMEOUT_MS = 8000;
|
||||||
|
const APPROVED_PLAN_CONTINUITY_TIMEOUT_MS = 8000;
|
||||||
|
|
||||||
type AutoChainPlanResult = {
|
type AutoChainPlanResult = {
|
||||||
plannerStatus: string;
|
plannerStatus: string;
|
||||||
@@ -30,6 +31,14 @@ type GateLockResult = {
|
|||||||
allowedResponseModes?: string[];
|
allowedResponseModes?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ApprovedPlanContinuityResult = {
|
||||||
|
ok: boolean;
|
||||||
|
status: string;
|
||||||
|
verdict: string;
|
||||||
|
reason?: string;
|
||||||
|
gate?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function clamp(s: string, max = 1200): string {
|
function clamp(s: string, max = 1200): string {
|
||||||
if (!s) return s;
|
if (!s) return s;
|
||||||
if (s.length <= max) 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);
|
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 {
|
function buildAutoChainPlanBlock(planResult: AutoChainPlanResult | null): string {
|
||||||
if (!planResult) {
|
if (!planResult) {
|
||||||
return [
|
return [
|
||||||
@@ -473,8 +544,11 @@ const forceRecall = async (event: any) => {
|
|||||||
]);
|
]);
|
||||||
const gateLockResult = wrapperResult ? await runLongTaskGateLock(workspaceDir, wrapperResult) : null;
|
const gateLockResult = wrapperResult ? await runLongTaskGateLock(workspaceDir, wrapperResult) : null;
|
||||||
const autoChainPlanResult = wrapperResult ? await runAutoChainPlanner(workspaceDir, gateLockResult, 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
|
const wrapperBlock = wrapperResult
|
||||||
? [
|
? [
|
||||||
@@ -500,6 +574,7 @@ const forceRecall = async (event: any) => {
|
|||||||
|
|
||||||
const gateLockBlock = buildGateLockBlock(gateLockResult);
|
const gateLockBlock = buildGateLockBlock(gateLockResult);
|
||||||
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
|
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
|
||||||
|
const approvedPlanContinuityBlock = buildApprovedPlanContinuityBlock(approvedPlanContinuityResult);
|
||||||
|
|
||||||
const recallBlock = [
|
const recallBlock = [
|
||||||
"[RECALL_GATE] Mandatory recall before ANY technical action/tool use.",
|
"[RECALL_GATE] Mandatory recall before ANY technical action/tool use.",
|
||||||
@@ -509,6 +584,7 @@ const forceRecall = async (event: any) => {
|
|||||||
wrapperBlock || null,
|
wrapperBlock || null,
|
||||||
gateLockBlock,
|
gateLockBlock,
|
||||||
autoChainPlanBlock,
|
autoChainPlanBlock,
|
||||||
|
approvedPlanContinuityBlock || null,
|
||||||
rulebook ? `RULEBOOK (source: ${rulebookPath}):\n${clamp(rulebook, 1200)}` : null,
|
rulebook ? `RULEBOOK (source: ${rulebookPath}):\n${clamp(rulebook, 1200)}` : null,
|
||||||
soul ? `SOUL (source: ${soulPath}):\n${clamp(soul, 1200)}` : null,
|
soul ? `SOUL (source: ${soulPath}):\n${clamp(soul, 1200)}` : null,
|
||||||
"[/RECALL_GATE]",
|
"[/RECALL_GATE]",
|
||||||
|
|||||||
Reference in New Issue
Block a user