feat: enforce proactive report gate during force-recall preflight
This commit is contained in:
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
|
|||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const LONG_TASK_WRAPPER_TIMEOUT_MS = 8000;
|
const LONG_TASK_WRAPPER_TIMEOUT_MS = 8000;
|
||||||
|
const PROACTIVE_REPORT_GATE_LOCK_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;
|
const APPROVED_PLAN_CONTINUITY_TIMEOUT_MS = 8000;
|
||||||
@@ -20,6 +21,20 @@ type AutoChainPlanResult = {
|
|||||||
autoChainAllowed: boolean;
|
autoChainAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProactiveReportGateResult = {
|
||||||
|
gateRequired: boolean;
|
||||||
|
gateStatus: "not_applicable" | "pass" | "fail";
|
||||||
|
ok: boolean;
|
||||||
|
reasons?: string[];
|
||||||
|
requiredEvidence?: Array<{
|
||||||
|
evidenceKey?: string;
|
||||||
|
acceptedFields?: string[];
|
||||||
|
requiredValue?: string;
|
||||||
|
}>;
|
||||||
|
allowedResponseModes?: string[];
|
||||||
|
reportBindingStatus?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type GateLockResult = {
|
type GateLockResult = {
|
||||||
gateRequired: boolean;
|
gateRequired: boolean;
|
||||||
gateStatus: "not_applicable" | "pass" | "fail";
|
gateStatus: "not_applicable" | "pass" | "fail";
|
||||||
@@ -130,6 +145,10 @@ async function runLongTaskWrapper(workspaceDir: string, ctx: any): Promise<any |
|
|||||||
checkpointTrigger: "",
|
checkpointTrigger: "",
|
||||||
externalizedTrigger: "",
|
externalizedTrigger: "",
|
||||||
triggerKind: "",
|
triggerKind: "",
|
||||||
|
firstReportTrigger: "",
|
||||||
|
fallbackState: "pending_verification",
|
||||||
|
reportMode: "watchdog",
|
||||||
|
ownerVisibleIfStalled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return runJsonScript(wrapperPath, workspaceDir, input, LONG_TASK_WRAPPER_TIMEOUT_MS);
|
return runJsonScript(wrapperPath, workspaceDir, input, LONG_TASK_WRAPPER_TIMEOUT_MS);
|
||||||
@@ -282,6 +301,42 @@ function buildGateLockInput(wrapperResult: any, readableCheckpointArtifact: { re
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildProactiveReportGateInput(wrapperResult: any, readableCheckpointArtifact: { relativePath: string; absolutePath: string; content: string; } | null): Record<string, unknown> {
|
||||||
|
if (!wrapperResult || wrapperResult.classification !== "long_task") {
|
||||||
|
return { classification: wrapperResult?.classification ?? "general_chat" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsOwnerDecision = wrapperResult.needsOwnerDecision === true;
|
||||||
|
const silentCandidate = wrapperResult.silentCandidate === true;
|
||||||
|
const firstReportTrigger = typeof wrapperResult.firstReportTrigger === "string" ? wrapperResult.firstReportTrigger.trim() : "";
|
||||||
|
const nextReportCondition = typeof wrapperResult.nextReportCondition === "string" ? wrapperResult.nextReportCondition.trim() : "";
|
||||||
|
const fallbackState = typeof wrapperResult.fallbackState === "string" ? wrapperResult.fallbackState.trim() : "";
|
||||||
|
const reportMode = typeof wrapperResult.reportMode === "string" ? wrapperResult.reportMode.trim() : "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
classification: wrapperResult.classification,
|
||||||
|
silentCandidate,
|
||||||
|
needsSubagent: wrapperResult.needsSubagent === true,
|
||||||
|
needsWaiting: silentCandidate,
|
||||||
|
needsOwnerDecision,
|
||||||
|
firstReportTrigger,
|
||||||
|
nextReportCondition,
|
||||||
|
fallbackState,
|
||||||
|
reportMode,
|
||||||
|
ownerVisibleIfStalled: wrapperResult.ownerVisibleIfStalled === true,
|
||||||
|
handoffMode: typeof wrapperResult?.handoff?.mode === "string" ? wrapperResult.handoff.mode : "direct_reply",
|
||||||
|
externalizedCheckpointPath: readableCheckpointArtifact?.relativePath ?? (typeof wrapperResult.externalizedCheckpointPath === "string" ? wrapperResult.externalizedCheckpointPath : ""),
|
||||||
|
checkpointTrigger: typeof wrapperResult.checkpointTrigger === "string" ? wrapperResult.checkpointTrigger : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runProactiveReportGateLock(workspaceDir: string, wrapperResult: any): Promise<ProactiveReportGateResult | null> {
|
||||||
|
const gateLockPath = path.join(workspaceDir, "scripts", "proactive_report_gate_lock.mjs");
|
||||||
|
const readableCheckpointArtifact = await getReadableCheckpointArtifact(workspaceDir, wrapperResult);
|
||||||
|
const input = buildProactiveReportGateInput(wrapperResult, readableCheckpointArtifact);
|
||||||
|
return runJsonScript(gateLockPath, workspaceDir, input, PROACTIVE_REPORT_GATE_LOCK_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
async function runLongTaskGateLock(workspaceDir: string, wrapperResult: any): Promise<GateLockResult | null> {
|
async function runLongTaskGateLock(workspaceDir: string, wrapperResult: any): Promise<GateLockResult | null> {
|
||||||
const gateLockPath = path.join(workspaceDir, "scripts", "long_task_gate_lock.mjs");
|
const gateLockPath = path.join(workspaceDir, "scripts", "long_task_gate_lock.mjs");
|
||||||
const readableCheckpointArtifact = await getReadableCheckpointArtifact(workspaceDir, wrapperResult);
|
const readableCheckpointArtifact = await getReadableCheckpointArtifact(workspaceDir, wrapperResult);
|
||||||
@@ -568,6 +623,42 @@ function buildWrapperHardGate(wrapperResult: any): string[] {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildProactiveReportGateBlock(gateResult: ProactiveReportGateResult | null): string {
|
||||||
|
if (!gateResult) {
|
||||||
|
return [
|
||||||
|
"[PROACTIVE_REPORT_GATE]",
|
||||||
|
"gateStatus=degraded",
|
||||||
|
"gateRequired=unknown",
|
||||||
|
"- ENFORCEMENT: Proactive-report gate unavailable; do not treat this as permission to launch silent progression.",
|
||||||
|
"- HARD_GATE: Fall back to non-silent follow-up unless firstReportTrigger, nextReportCondition, and fallbackState are explicitly bound.",
|
||||||
|
"[/PROACTIVE_REPORT_GATE]",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
"[PROACTIVE_REPORT_GATE]",
|
||||||
|
`gateRequired=${gateResult.gateRequired}`,
|
||||||
|
`gateStatus=${gateResult.gateStatus}`,
|
||||||
|
`reportBindingStatus=${gateResult.reportBindingStatus ?? "unknown"}`,
|
||||||
|
...((gateResult.reasons ?? []).map((reason) => `reason=${reason}`)),
|
||||||
|
...((gateResult.requiredEvidence ?? []).map((requirement) => {
|
||||||
|
const fields = (requirement.acceptedFields ?? []).join(",");
|
||||||
|
return `requiredEvidence=${requirement.evidenceKey ?? "unknown"};fields=${fields};requiredValue=${requirement.requiredValue ?? "unknown"}`;
|
||||||
|
})),
|
||||||
|
...((gateResult.allowedResponseModes ?? []).map((mode) => `allowedResponseMode=${mode}`)),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (gateResult.gateStatus === "fail") {
|
||||||
|
lines.push("- ENFORCEMENT: Silent long-task cannot enter silent progression yet.");
|
||||||
|
lines.push("- ENFORCEMENT: Bind firstReportTrigger, nextReportCondition, and fallbackState before claiming background continuation.");
|
||||||
|
lines.push("- HARD_GATE: Downgrade to non-silent follow-up if proactive report binding remains incomplete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("[/PROACTIVE_REPORT_GATE]", "");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function buildGateLockBlock(gateLockResult: GateLockResult | null): string {
|
function buildGateLockBlock(gateLockResult: GateLockResult | null): string {
|
||||||
if (!gateLockResult) {
|
if (!gateLockResult) {
|
||||||
return [
|
return [
|
||||||
@@ -638,13 +729,14 @@ const forceRecall = async (event: any) => {
|
|||||||
safeReadText(soulPath),
|
safeReadText(soulPath),
|
||||||
runLongTaskWrapper(workspaceDir, ctx),
|
runLongTaskWrapper(workspaceDir, ctx),
|
||||||
]);
|
]);
|
||||||
|
const proactiveReportGateResult = wrapperResult ? await runProactiveReportGateLock(workspaceDir, wrapperResult) : null;
|
||||||
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
|
const approvedPlanContinuityResult = wrapperResult
|
||||||
? await runApprovedPlanContinuityGate(workspaceDir, wrapperResult, autoChainPlanResult)
|
? await runApprovedPlanContinuityGate(workspaceDir, wrapperResult, autoChainPlanResult)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!rulebook && !soul && !wrapperResult && !gateLockResult && !autoChainPlanResult && !approvedPlanContinuityResult) return;
|
if (!rulebook && !soul && !wrapperResult && !proactiveReportGateResult && !gateLockResult && !autoChainPlanResult && !approvedPlanContinuityResult) return;
|
||||||
|
|
||||||
const wrapperBlock = wrapperResult
|
const wrapperBlock = wrapperResult
|
||||||
? [
|
? [
|
||||||
@@ -668,6 +760,7 @@ const forceRecall = async (event: any) => {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const proactiveReportGateBlock = buildProactiveReportGateBlock(proactiveReportGateResult);
|
||||||
const gateLockBlock = buildGateLockBlock(gateLockResult);
|
const gateLockBlock = buildGateLockBlock(gateLockResult);
|
||||||
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
|
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
|
||||||
const approvedPlanContinuityBlock = await buildApprovedPlanContinuityBlock(workspaceDir, wrapperResult, autoChainPlanResult, approvedPlanContinuityResult);
|
const approvedPlanContinuityBlock = await buildApprovedPlanContinuityBlock(workspaceDir, wrapperResult, autoChainPlanResult, approvedPlanContinuityResult);
|
||||||
@@ -678,6 +771,7 @@ const forceRecall = async (event: any) => {
|
|||||||
"- If you are about to run tools, change configs, modify code, or delegate agents: restate the applicable rules first.",
|
"- If you are about to run tools, change configs, modify code, or delegate agents: restate the applicable rules first.",
|
||||||
"",
|
"",
|
||||||
wrapperBlock || null,
|
wrapperBlock || null,
|
||||||
|
proactiveReportGateBlock,
|
||||||
gateLockBlock,
|
gateLockBlock,
|
||||||
autoChainPlanBlock,
|
autoChainPlanBlock,
|
||||||
approvedPlanContinuityBlock || null,
|
approvedPlanContinuityBlock || null,
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ function normalizeRequest(raw) {
|
|||||||
checkpointTrigger: data.checkpointTrigger || '',
|
checkpointTrigger: data.checkpointTrigger || '',
|
||||||
externalizedTrigger: data.externalizedTrigger || '',
|
externalizedTrigger: data.externalizedTrigger || '',
|
||||||
triggerKind: data.triggerKind || '',
|
triggerKind: data.triggerKind || '',
|
||||||
|
firstReportTrigger: data.firstReportTrigger || '',
|
||||||
|
fallbackState: data.fallbackState || '',
|
||||||
|
reportMode: data.reportMode || '',
|
||||||
|
ownerVisibleIfStalled: data.ownerVisibleIfStalled === true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +80,9 @@ function inferFromRequestText(input) {
|
|||||||
if (!input.checkpointTrigger && inferred.needsSubagent) {
|
if (!input.checkpointTrigger && inferred.needsSubagent) {
|
||||||
inferred.checkpointTrigger = 'when delegated work returns or the next checkpoint fires';
|
inferred.checkpointTrigger = 'when delegated work returns or the next checkpoint fires';
|
||||||
}
|
}
|
||||||
|
if (!input.firstReportTrigger && inferred.checkpointTrigger) {
|
||||||
|
inferred.firstReportTrigger = inferred.checkpointTrigger;
|
||||||
|
}
|
||||||
if (!input.externalizedTrigger && inferred.needsSubagent) {
|
if (!input.externalizedTrigger && inferred.needsSubagent) {
|
||||||
inferred.externalizedTrigger = 'wrapper-derived checkpoint artifact';
|
inferred.externalizedTrigger = 'wrapper-derived checkpoint artifact';
|
||||||
}
|
}
|
||||||
@@ -114,6 +121,10 @@ function bootstrapTaskState(input, classificationResult) {
|
|||||||
waiting_on: input.waitingOn,
|
waiting_on: input.waitingOn,
|
||||||
blocker: input.blocker,
|
blocker: input.blocker,
|
||||||
silent: classificationResult.silentCandidate,
|
silent: classificationResult.silentCandidate,
|
||||||
|
first_report_trigger: input.firstReportTrigger,
|
||||||
|
fallback_state: input.fallbackState,
|
||||||
|
report_mode: input.reportMode,
|
||||||
|
owner_visible_if_stalled: input.ownerVisibleIfStalled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +255,11 @@ function main() {
|
|||||||
progressEvidence,
|
progressEvidence,
|
||||||
externalizedCheckpointPath,
|
externalizedCheckpointPath,
|
||||||
checkpointArtifact,
|
checkpointArtifact,
|
||||||
|
firstReportTrigger: input.firstReportTrigger || '',
|
||||||
|
nextReportCondition: input.nextReportCondition || '',
|
||||||
|
fallbackState: input.fallbackState || '',
|
||||||
|
reportMode: input.reportMode || '',
|
||||||
|
ownerVisibleIfStalled: input.ownerVisibleIfStalled === true,
|
||||||
silentLaunchOk: silentLaunch.ok,
|
silentLaunchOk: silentLaunch.ok,
|
||||||
silentLaunchReason: silentLaunch.reason,
|
silentLaunchReason: silentLaunch.reason,
|
||||||
recommendedFallback: silentLaunch.recommendedFallback,
|
recommendedFallback: silentLaunch.recommendedFallback,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|||||||
const repoRoot = path.resolve(__dirname, '..');
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
const handlerPath = path.join(repoRoot, 'hooks', 'force-recall', 'handler.ts');
|
const handlerPath = path.join(repoRoot, 'hooks', 'force-recall', 'handler.ts');
|
||||||
const wrapperPath = path.join(repoRoot, 'scripts', 'long_task_governor_wrapper.mjs');
|
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 gateLockPath = path.join(repoRoot, 'scripts', 'long_task_gate_lock.mjs');
|
||||||
const plannerPath = path.join(repoRoot, 'scripts', 'plan_long_task_auto_chain.mjs');
|
const plannerPath = path.join(repoRoot, 'scripts', 'plan_long_task_auto_chain.mjs');
|
||||||
const continuityGatePath = path.join(repoRoot, 'scripts', 'approved_plan_continuity_gate.mjs');
|
const continuityGatePath = path.join(repoRoot, 'scripts', 'approved_plan_continuity_gate.mjs');
|
||||||
@@ -57,12 +58,11 @@ async function prepareTempWorkspace() {
|
|||||||
|
|
||||||
const copies = [
|
const copies = [
|
||||||
[wrapperPath, path.join(tempWorkspace, 'scripts', 'long_task_governor_wrapper.mjs')],
|
[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')],
|
[gateLockPath, path.join(tempWorkspace, 'scripts', 'long_task_gate_lock.mjs')],
|
||||||
[plannerPath, path.join(tempWorkspace, 'scripts', 'plan_long_task_auto_chain.mjs')],
|
[plannerPath, path.join(tempWorkspace, 'scripts', 'plan_long_task_auto_chain.mjs')],
|
||||||
[continuityGatePath, path.join(tempWorkspace, 'scripts', 'approved_plan_continuity_gate.mjs')],
|
[continuityGatePath, path.join(tempWorkspace, 'scripts', 'approved_plan_continuity_gate.mjs')],
|
||||||
[handlerPath, path.join(tempWorkspace, 'hooks', 'force-recall', 'handler.ts')],
|
[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', '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', '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', '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.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;
|
return tempWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +177,9 @@ async function main() {
|
|||||||
} finally {
|
} finally {
|
||||||
await fs.rm(checkpointWorkspace, { recursive: true, force: true });
|
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, /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, /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');
|
assert.match(realWrapperInjected, /allowedResponseMode=silent_continuation/, 'real wrapper integration should preserve silent continuation allowance');
|
||||||
@@ -177,6 +191,10 @@ async function main() {
|
|||||||
|
|
||||||
const expectedSnippets = [
|
const expectedSnippets = [
|
||||||
'[LONG_TASK_GOVERNOR_PREFLIGHT]',
|
'[LONG_TASK_GOVERNOR_PREFLIGHT]',
|
||||||
|
'[PROACTIVE_REPORT_GATE]',
|
||||||
|
'gateStatus=fail',
|
||||||
|
'reason=missing first proactive report trigger',
|
||||||
|
'requiredEvidence=firstReportTrigger',
|
||||||
'classification=long_task',
|
'classification=long_task',
|
||||||
'silentLaunchOk=false',
|
'silentLaunchOk=false',
|
||||||
'handoff.mode=button_path',
|
'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, /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');
|
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 originalGateLock = await fs.readFile(gateLockPath, 'utf8');
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'force-recall-gate-lock-'));
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'force-recall-gate-lock-'));
|
||||||
const backupPath = path.join(tempDir, path.basename(gateLockPath));
|
const backupPath = path.join(tempDir, path.basename(gateLockPath));
|
||||||
|
|||||||
@@ -73,6 +73,49 @@ const fixtures = [
|
|||||||
assert.equal(JSON.stringify(output.progressEvidence).includes('Wait for delegated log survey'), false, 'subagent wait: progressEvidence must not derive from taskRecord.task_name');
|
assert.equal(JSON.stringify(output.progressEvidence).includes('Wait for delegated log survey'), false, 'subagent wait: progressEvidence must not derive from taskRecord.task_name');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'report binding fields preserved',
|
||||||
|
input: {
|
||||||
|
requestText: 'Dispatch a subagent to inspect logs and wait for the result.',
|
||||||
|
canReplyNow: false,
|
||||||
|
needsSubagent: true,
|
||||||
|
needsWaiting: true,
|
||||||
|
checkpointTrigger: 'when delegated work returns or the next checkpoint fires',
|
||||||
|
externalizedTrigger: 'wrapper-derived checkpoint artifact',
|
||||||
|
triggerKind: 'artifact',
|
||||||
|
firstReportTrigger: 'when delegated work 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,
|
||||||
|
},
|
||||||
|
assert(output) {
|
||||||
|
assert.equal(output.firstReportTrigger, 'when delegated work returns or at 10 minutes, whichever comes first');
|
||||||
|
assert.equal(output.nextReportCondition, 'report again only after new verifier output or blocker-state change');
|
||||||
|
assert.equal(output.fallbackState, 'blocked');
|
||||||
|
assert.equal(output.reportMode, 'watchdog');
|
||||||
|
assert.equal(output.ownerVisibleIfStalled, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wrapper does not fabricate hard report binding fields',
|
||||||
|
input: {
|
||||||
|
requestText: 'Dispatch a subagent to inspect logs and wait for the result.',
|
||||||
|
canReplyNow: false,
|
||||||
|
needsSubagent: true,
|
||||||
|
needsWaiting: true,
|
||||||
|
checkpointTrigger: 'when delegated work returns or the next checkpoint fires',
|
||||||
|
externalizedTrigger: 'wrapper-derived checkpoint artifact',
|
||||||
|
triggerKind: 'artifact',
|
||||||
|
},
|
||||||
|
assert(output) {
|
||||||
|
assert.equal(output.firstReportTrigger, 'when delegated work returns or the next checkpoint fires');
|
||||||
|
assert.equal(output.nextReportCondition, 'After next meaningful milestone');
|
||||||
|
assert.equal(output.fallbackState, '');
|
||||||
|
assert.equal(output.reportMode, '');
|
||||||
|
assert.equal(output.ownerVisibleIfStalled, false);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function runFixture(fixture) {
|
function runFixture(fixture) {
|
||||||
|
|||||||
Reference in New Issue
Block a user