feat: enforce proactive report gate during force-recall preflight
This commit is contained in:
@@ -12,6 +12,7 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||
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 proactiveGatePath = path.join(repoRoot, 'scripts', 'proactive_report_gate_lock.mjs');
|
||||
const gateLockPath = path.join(repoRoot, 'scripts', 'long_task_gate_lock.mjs');
|
||||
const plannerPath = path.join(repoRoot, 'scripts', 'plan_long_task_auto_chain.mjs');
|
||||
const continuityGatePath = path.join(repoRoot, 'scripts', 'approved_plan_continuity_gate.mjs');
|
||||
@@ -57,12 +58,11 @@ async function prepareTempWorkspace() {
|
||||
|
||||
const copies = [
|
||||
[wrapperPath, path.join(tempWorkspace, 'scripts', 'long_task_governor_wrapper.mjs')],
|
||||
[proactiveGatePath, path.join(tempWorkspace, 'scripts', 'proactive_report_gate_lock.mjs')],
|
||||
[gateLockPath, path.join(tempWorkspace, 'scripts', 'long_task_gate_lock.mjs')],
|
||||
[plannerPath, path.join(tempWorkspace, 'scripts', 'plan_long_task_auto_chain.mjs')],
|
||||
[continuityGatePath, path.join(tempWorkspace, 'scripts', 'approved_plan_continuity_gate.mjs')],
|
||||
[handlerPath, path.join(tempWorkspace, 'hooks', 'force-recall', 'handler.ts')],
|
||||
[path.join(repoRoot, 'docs', 'RULEBOOK.md'), path.join(tempWorkspace, 'docs', 'RULEBOOK.md')],
|
||||
[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')],
|
||||
@@ -78,6 +78,17 @@ async function prepareTempWorkspace() {
|
||||
await fs.copyFile(src, dest);
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempWorkspace, 'docs', 'RULEBOOK.md'),
|
||||
'# Test Fixture RULEBOOK\n\nMinimal clean-room fixture generated by scripts/test_force_recall_long_task_preflight.mjs.\n',
|
||||
'utf8',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempWorkspace, 'SOUL.md'),
|
||||
'# Test Fixture SOUL\n\nMinimal clean-room fixture generated by scripts/test_force_recall_long_task_preflight.mjs.\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return tempWorkspace;
|
||||
}
|
||||
|
||||
@@ -166,6 +177,9 @@ async function main() {
|
||||
} finally {
|
||||
await fs.rm(checkpointWorkspace, { recursive: true, force: true });
|
||||
}
|
||||
assert.match(realWrapperInjected, /\[PROACTIVE_REPORT_GATE\]/, 'real wrapper integration should inject proactive report gate block');
|
||||
assert.match(realWrapperInjected, /reportBindingStatus=bound/, 'real wrapper integration should bind proactive report fields before silent continuation');
|
||||
assert.match(realWrapperInjected, /allowedResponseMode=silent_continuation/, 'real wrapper integration should allow silent continuation only after proactive binding');
|
||||
assert.match(realWrapperInjected, /classification=long_task/, 'real wrapper integration should classify subagent wait as long_task');
|
||||
assert.match(realWrapperInjected, /gateStatus=pass/, 'real wrapper integration should pass gate with real progress evidence');
|
||||
assert.match(realWrapperInjected, /allowedResponseMode=silent_continuation/, 'real wrapper integration should preserve silent continuation allowance');
|
||||
@@ -177,6 +191,10 @@ async function main() {
|
||||
|
||||
const expectedSnippets = [
|
||||
'[LONG_TASK_GOVERNOR_PREFLIGHT]',
|
||||
'[PROACTIVE_REPORT_GATE]',
|
||||
'gateStatus=fail',
|
||||
'reason=missing first proactive report trigger',
|
||||
'requiredEvidence=firstReportTrigger',
|
||||
'classification=long_task',
|
||||
'silentLaunchOk=false',
|
||||
'handoff.mode=button_path',
|
||||
@@ -505,6 +523,62 @@ async function main() {
|
||||
assert.match(specReviewWithoutImplementationEvidenceInjected, /reason=implementation evidence missing for review-required next action/, 'hook implementation missing-evidence path should mention missing implementation evidence');
|
||||
assert.match(specReviewWithoutImplementationEvidenceInjected, /requiredEvidence=executionEvidence/, 'hook implementation missing-evidence path should require executionEvidence');
|
||||
|
||||
const continuityCannotMaskReportBindingFailureInjected = await withPatchedWrapperWorkspace({
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
needsCheckpoint: true,
|
||||
needsSubagent: false,
|
||||
needsOwnerDecision: false,
|
||||
silentLaunchOk: true,
|
||||
requiredNextAction: 'dispatch_follow_up_subagent',
|
||||
autoChainDispatchEvidence: {
|
||||
action: 'dispatch_follow_up_subagent',
|
||||
dispatched: true,
|
||||
event: 'dispatch',
|
||||
},
|
||||
progressEvidence: { sessionKey: 'task-proactive-missing' },
|
||||
externalizedCheckpointPath: 'checkpoints/task-proactive-missing.json',
|
||||
nextReportCondition: 'after verifier output arrives',
|
||||
reportMode: 'watchdog',
|
||||
handoff: { mode: 'direct_reply' },
|
||||
dispatchReceipt: {
|
||||
planId: 'plan-proactive-missing',
|
||||
currentTask: 'task-proactive-missing',
|
||||
nextDerivedAction: { type: 'dry_run_dispatch', action: 'dispatch_spec_review' },
|
||||
dispatchedAt: '2026-05-04T11:30:00+08:00',
|
||||
},
|
||||
}, async (workspaceDir) => runScenario(forceRecall, requestText, workspaceDir));
|
||||
assert.match(continuityCannotMaskReportBindingFailureInjected, /\[PROACTIVE_REPORT_GATE\]/, 'continuity regression should include proactive report gate block');
|
||||
assert.match(continuityCannotMaskReportBindingFailureInjected, /reportBindingStatus=missing/, 'continuity regression should fail early on missing proactive report binding');
|
||||
assert.match(continuityCannotMaskReportBindingFailureInjected, /reason=missing first proactive report trigger/, 'continuity regression should expose missing first report trigger');
|
||||
assert.match(continuityCannotMaskReportBindingFailureInjected, /reason=missing fallback state for stalled reporting/, 'continuity regression should expose missing fallback state');
|
||||
|
||||
const continuityStillSeparateWhenProactiveBindingPassesInjected = await withPatchedWrapperWorkspace({
|
||||
classification: 'long_task',
|
||||
silentCandidate: true,
|
||||
needsCheckpoint: true,
|
||||
needsSubagent: false,
|
||||
needsOwnerDecision: false,
|
||||
silentLaunchOk: true,
|
||||
requiredNextAction: 'dispatch_follow_up_subagent',
|
||||
autoChainDispatchEvidence: {
|
||||
action: 'dispatch_follow_up_subagent',
|
||||
dispatched: true,
|
||||
event: 'dispatch',
|
||||
},
|
||||
progressEvidence: { sessionKey: 'task-proactive-pass' },
|
||||
externalizedCheckpointPath: 'checkpoints/task-proactive-pass.json',
|
||||
firstReportTrigger: 'when delegated scan returns or at 10 minutes, whichever comes first',
|
||||
nextReportCondition: 'report again only after new verifier output or blocker-state change',
|
||||
fallbackState: 'blocked',
|
||||
reportMode: 'watchdog',
|
||||
ownerVisibleIfStalled: true,
|
||||
handoff: { mode: 'direct_reply' },
|
||||
}, async (workspaceDir) => runScenario(forceRecall, requestText, workspaceDir));
|
||||
assert.match(continuityStillSeparateWhenProactiveBindingPassesInjected, /\[PROACTIVE_REPORT_GATE\]/, 'pass regression should include proactive report gate block');
|
||||
assert.match(continuityStillSeparateWhenProactiveBindingPassesInjected, /reportBindingStatus=bound/, 'pass regression should show bound proactive report state');
|
||||
assert.match(continuityStillSeparateWhenProactiveBindingPassesInjected, /\[APPROVED_PLAN_CONTINUITY_GATE\]/, 'pass regression should still show continuity block separately');
|
||||
|
||||
const originalGateLock = await fs.readFile(gateLockPath, 'utf8');
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'force-recall-gate-lock-'));
|
||||
const backupPath = path.join(tempDir, path.basename(gateLockPath));
|
||||
|
||||
Reference in New Issue
Block a user