From a77141f021489c8bdb97942df1dfb97d585c97d1 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 24 Apr 2026 19:34:15 +0800 Subject: [PATCH] fix: gate main-agent self-stop on auto-next obligation --- hooks/force-recall/handler.ts | 2 ++ plugins/continuity/src/continuity/evaluator.mjs | 2 ++ scripts/test_force_recall_long_task_preflight.mjs | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/hooks/force-recall/handler.ts b/hooks/force-recall/handler.ts index 605f342..654b3db 100644 --- a/hooks/force-recall/handler.ts +++ b/hooks/force-recall/handler.ts @@ -484,6 +484,8 @@ async function buildApprovedPlanContinuityBlock(workspaceDir: string, wrapperRes if (result.reason === 'missing_auto_next_dispatch') { lines.push("- HARD_GATE: Do not stop at this completed-task boundary."); lines.push("- HARD_GATE: Auto-dispatch the next task in the same approved plan, unless waiting_user, blocked, pending_verification, or high-risk stop applies."); + lines.push("- HARD_GATE: Do not hand control back to the user with an ordinary progress update while auto-next is still obligatory."); + lines.push("- HARD_GATE: If you cannot prove the next dispatch, convert this into an explicit continuity failure instead of a normal status report."); } else { 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."); } diff --git a/plugins/continuity/src/continuity/evaluator.mjs b/plugins/continuity/src/continuity/evaluator.mjs index f2f4f87..aada80d 100644 --- a/plugins/continuity/src/continuity/evaluator.mjs +++ b/plugins/continuity/src/continuity/evaluator.mjs @@ -110,6 +110,8 @@ export function buildContinuityGateBlock(result, options = {}) { if (result.reason === 'missing_auto_next_dispatch') { lines.push('- HARD_GATE: Do not stop at this completed-task boundary.'); lines.push(`- HARD_GATE: Auto-dispatch the next task in the same approved plan, unless ${terminalStates.join(', ')}, or high-risk stop applies.`); + lines.push('- HARD_GATE: Do not hand control back to the user with an ordinary progress update while auto-next is still obligatory.'); + lines.push('- HARD_GATE: If you cannot prove the next dispatch, convert this into an explicit continuity failure instead of a normal status report.'); } else { lines.push(`- HARD_GATE: Route back to continuity failure until a real next dispatch receipt exists, unless closure state is ${terminalStates.join(', ')}.`); } diff --git a/scripts/test_force_recall_long_task_preflight.mjs b/scripts/test_force_recall_long_task_preflight.mjs index f275ce1..ec9d265 100755 --- a/scripts/test_force_recall_long_task_preflight.mjs +++ b/scripts/test_force_recall_long_task_preflight.mjs @@ -65,11 +65,13 @@ async function prepareTempWorkspace() { [path.join(repoRoot, 'SOUL.md'), path.join(tempWorkspace, 'SOUL.md')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'index.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'index.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'adapters', 'force-recall.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'adapters', 'force-recall.mjs')], + [path.join(repoRoot, 'plugins', 'continuity', 'src', 'adapters', 'generic-preflight.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'adapters', 'generic-preflight.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'config', 'defaults.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'config', 'defaults.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'config', 'schema.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'config', 'schema.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'continuity', 'evaluator.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'continuity', 'evaluator.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'continuity', 'receipt-validator.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'continuity', 'receipt-validator.mjs')], [path.join(repoRoot, 'plugins', 'continuity', 'src', 'continuity', 'receipt-store.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'continuity', 'receipt-store.mjs')], + [path.join(repoRoot, 'plugins', 'continuity', 'src', 'continuity', 'engine.mjs'), path.join(tempWorkspace, 'plugins', 'continuity', 'src', 'continuity', 'engine.mjs')], ]; for (const [src, dest] of copies) { @@ -370,6 +372,8 @@ async function main() { assert.match(passInjected, /Do not stop at this completed-task boundary/, 'hook pass-path should explicitly forbid stopping at the completed-task boundary'); assert.match(passInjected, /Auto-dispatch the next task in the same approved plan, unless waiting_user, blocked, pending_verification, or high-risk stop applies/, 'hook pass-path should explain the auto-next obligation exceptions'); assert.match(passInjected, /Do not stop at this completed-task boundary/, 'hook pass-path should hard-gate the completed-task boundary'); + assert.match(passInjected, /Do not hand control back to the user with an ordinary progress update while auto-next is still obligatory/, 'hook pass-path should forbid ordinary progress handoff when auto-next obligation is active'); + assert.match(passInjected, /If you cannot prove the next dispatch, convert this into an explicit continuity failure instead of a normal status report/, 'hook pass-path should require failure conversion instead of normal progress reporting'); assert.doesNotMatch(passInjected, /\[APPROVED_PLAN_CONTINUITY_GATE\][\s\S]*status=pass/, 'hook pass-path should not let approved-plan continuity pass on dry-run dispatch alone'); const failInjected = await withPatchedWrapper(buildWrapperScript({