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

@@ -11,6 +11,7 @@ const repoRoot = path.resolve(__dirname, '..');
const handlerPath = path.join(repoRoot, 'hooks', 'force-recall', 'handler.ts');
const wrapperPath = path.join(repoRoot, 'scripts', 'long_task_governor_wrapper.mjs');
const gateLockPath = path.join(repoRoot, 'scripts', 'long_task_gate_lock.mjs');
const plannerPath = path.join(repoRoot, 'scripts', 'plan_long_task_auto_chain.mjs');
async function importTsModule(tsPath) {
const source = await fs.readFile(tsPath, 'utf8');
@@ -55,7 +56,7 @@ function buildWrapperScript(wrapperResult) {
}
async function main() {
await Promise.all([fs.access(wrapperPath), fs.access(gateLockPath)]);
await Promise.all([fs.access(wrapperPath), fs.access(gateLockPath), fs.access(plannerPath)]);
const { default: forceRecall } = await importTsModule(handlerPath);
assert.equal(typeof forceRecall, 'function', 'force-recall handler should export default function');
@@ -74,6 +75,13 @@ async function main() {
'handoff.mode=button_path',
'[LONG_TASK_GATE_LOCK]',
'gateStatus=fail',
'[LONG_TASK_AUTO_CHAIN_PLAN]',
'plannerStatus=blocked_by_gate',
'derivedAction=none',
'dispatchMode=no_dispatch',
'autoChainAllowed=false',
'reason=gateStatus must pass before auto-chain planning can proceed',
'requiredEvidence=gateStatus=pass',
'requiredEvidence=externalizedCheckpoint',
'requiredEvidence=concreteNextAction',
'requiredEvidence=buttonPathMode',
@@ -201,6 +209,11 @@ async function main() {
handoff: { mode: 'direct_reply' },
}), async () => runScenario(forceRecall, requestText));
assert.match(passInjected, /gateStatus=pass/, 'hook pass-path should pass when wrapper provides concrete progressEvidence');
assert.match(passInjected, /\[LONG_TASK_AUTO_CHAIN_PLAN\]/, 'hook pass-path should emit auto-chain plan block');
assert.match(passInjected, /plannerStatus=pass/, 'hook pass-path should expose planner pass result');
assert.match(passInjected, /derivedAction=dispatch_spec_review/, 'hook pass-path should derive dry-run spec review dispatch');
assert.match(passInjected, /dispatchMode=dry_run_dispatch/, 'hook pass-path should stay in dry-run dispatch mode');
assert.match(passInjected, /autoChainAllowed=true/, 'hook pass-path should allow auto-chain in dry-run planner output');
const failInjected = await withPatchedWrapper(buildWrapperScript({
classification: 'long_task',
@@ -213,6 +226,11 @@ async function main() {
handoff: { mode: 'direct_reply' },
}), async () => runScenario(forceRecall, requestText));
assert.match(failInjected, /gateStatus=fail/, 'hook fail-path should fail when wrapper exposes explicit auto-chain action without dispatch evidence');
assert.match(failInjected, /\[LONG_TASK_AUTO_CHAIN_PLAN\]/, 'hook fail-path should emit auto-chain plan block');
assert.match(failInjected, /plannerStatus=blocked_by_gate/, 'hook fail-path should report planner blocked by gate');
assert.match(failInjected, /derivedAction=none/, 'hook fail-path should not derive a dry-run action');
assert.match(failInjected, /dispatchMode=no_dispatch/, 'hook fail-path should remain no-dispatch');
assert.match(failInjected, /autoChainAllowed=false/, 'hook fail-path should not allow auto-chain');
assert.match(failInjected, /reason=explicit auto-chain next action requires dispatched-action evidence/, 'hook fail-path should mention missing dispatched-action evidence');
assert.match(failInjected, /requiredEvidence=autoChainDispatchEvidence/, 'hook fail-path should require autoChainDispatchEvidence');
@@ -227,6 +245,11 @@ async function main() {
handoff: { mode: 'direct_reply' },
}), async () => runScenario(forceRecall, requestText));
assert.match(neutralInjected, /gateStatus=pass/, 'hook neutral-path should pass when wrapper does not expose an explicit auto-chain action');
assert.match(neutralInjected, /\[LONG_TASK_AUTO_CHAIN_PLAN\]/, 'hook neutral-path should emit auto-chain plan block');
assert.match(neutralInjected, /plannerStatus=none/, 'hook neutral-path should report no derived auto-chain action');
assert.match(neutralInjected, /derivedAction=none/, 'hook neutral-path should keep derivedAction as none');
assert.match(neutralInjected, /dispatchMode=no_dispatch/, 'hook neutral-path should remain no-dispatch');
assert.match(neutralInjected, /autoChainAllowed=false/, 'hook neutral-path should keep auto-chain disabled');
assert.doesNotMatch(neutralInjected, /reason=explicit auto-chain next action requires dispatched-action evidence/, 'hook neutral-path should not fail on auto-chain evidence when no explicit tool action exists');
const originalGateLock = await fs.readFile(gateLockPath, 'utf8');