#!/usr/bin/env node import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { stripTypeScriptTypes } from 'node:module'; 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 gateLockPath = path.join(repoRoot, 'scripts', 'long_task_gate_lock.mjs'); async function importTsModule(tsPath) { const source = await fs.readFile(tsPath, 'utf8'); const jsSource = stripTypeScriptTypes(source, { mode: 'strip' }); const dataUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(jsSource)}\n//# sourceURL=${encodeURIComponent(pathToFileURL(tsPath).href)}`; return import(dataUrl); } async function main() { await Promise.all([fs.access(wrapperPath), fs.access(gateLockPath)]); const { default: forceRecall } = await importTsModule(handlerPath); assert.equal(typeof forceRecall, 'function', 'force-recall handler should export default function'); const requestText = [ 'Please inspect the workspace files and verify the hook injection path.', 'I need you to review the behavior, choose the final accept/reject decision,', 'and continue in background with a follow-up later.', ].join(' '); const event = { type: 'message', action: 'preprocessed', context: { workspaceDir: repoRoot, body: requestText, bodyForAgent: requestText, }, }; await forceRecall(event); const injected = event.context?.bodyForAgent; assert.equal(typeof injected, 'string', 'event.context.bodyForAgent should be a string after handler runs'); const expectedSnippets = [ '[LONG_TASK_GOVERNOR_PREFLIGHT]', 'classification=long_task', 'silentLaunchOk=false', 'handoff.mode=button_path', '[LONG_TASK_GATE_LOCK]', 'gateStatus=fail', 'requiredEvidence=externalizedCheckpoint', 'requiredEvidence=concreteNextAction', 'requiredEvidence=buttonPathMode', 'reason=silent long-task cannot continue without externalized checkpoint path', 'reason=claimed execution requires evidence of a concrete next action', 'reason=owner decision flow must end in button-path, not plain text', 'ENFORCEMENT: Forbidden path: plain-text handoff that pretends the long task is already continuing without an externalized checkpoint.', 'ENFORCEMENT: Forbidden path: stating you have already entered the next task/step when the record only contains planning language and no concrete execution evidence.', 'HARD_GATE: Block any plain-text handoff or silent-continuation claim when externalized checkpoint evidence is missing.', 'HARD_GATE: Block any reply path that says you already moved into the next task or are advancing the next step without concrete execution evidence.', 'HARD_GATE: Do not say you are already on the next task, already dispatched follow-up work, or already progressing in background unless you can point to actual tool execution, file changes, emitted messages, or checkpoint records.', 'Do not claim you have progressed into the next task or are already pushing the next step unless you have concrete evidence such as actual dispatch, tool calls, file changes, or a persisted checkpoint artifact.', ]; for (const snippet of expectedSnippets) { assert.match(injected, new RegExp(snippet.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `missing snippet: ${snippet}`); } const summary = { ok: true, checked: expectedSnippets, bodyPreview: injected.split('\n').slice(0, 30), }; process.stdout.write(JSON.stringify(summary, null, 2) + '\n'); } main().catch((error) => { console.error(error); process.exitCode = 1; });