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 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_AUTO_CHAIN_PLANNER_TIMEOUT_MS = 8000;
|
||||
const APPROVED_PLAN_CONTINUITY_TIMEOUT_MS = 8000;
|
||||
@@ -20,6 +21,20 @@ type AutoChainPlanResult = {
|
||||
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 = {
|
||||
gateRequired: boolean;
|
||||
gateStatus: "not_applicable" | "pass" | "fail";
|
||||
@@ -130,6 +145,10 @@ async function runLongTaskWrapper(workspaceDir: string, ctx: any): Promise<any |
|
||||
checkpointTrigger: "",
|
||||
externalizedTrigger: "",
|
||||
triggerKind: "",
|
||||
firstReportTrigger: "",
|
||||
fallbackState: "pending_verification",
|
||||
reportMode: "watchdog",
|
||||
ownerVisibleIfStalled: true,
|
||||
};
|
||||
|
||||
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> {
|
||||
const gateLockPath = path.join(workspaceDir, "scripts", "long_task_gate_lock.mjs");
|
||||
const readableCheckpointArtifact = await getReadableCheckpointArtifact(workspaceDir, wrapperResult);
|
||||
@@ -568,6 +623,42 @@ function buildWrapperHardGate(wrapperResult: any): string[] {
|
||||
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 {
|
||||
if (!gateLockResult) {
|
||||
return [
|
||||
@@ -638,13 +729,14 @@ const forceRecall = async (event: any) => {
|
||||
safeReadText(soulPath),
|
||||
runLongTaskWrapper(workspaceDir, ctx),
|
||||
]);
|
||||
const proactiveReportGateResult = wrapperResult ? await runProactiveReportGateLock(workspaceDir, wrapperResult) : null;
|
||||
const gateLockResult = wrapperResult ? await runLongTaskGateLock(workspaceDir, wrapperResult) : null;
|
||||
const autoChainPlanResult = wrapperResult ? await runAutoChainPlanner(workspaceDir, gateLockResult, wrapperResult) : null;
|
||||
const approvedPlanContinuityResult = wrapperResult
|
||||
? await runApprovedPlanContinuityGate(workspaceDir, wrapperResult, autoChainPlanResult)
|
||||
: null;
|
||||
|
||||
if (!rulebook && !soul && !wrapperResult && !gateLockResult && !autoChainPlanResult && !approvedPlanContinuityResult) return;
|
||||
if (!rulebook && !soul && !wrapperResult && !proactiveReportGateResult && !gateLockResult && !autoChainPlanResult && !approvedPlanContinuityResult) return;
|
||||
|
||||
const wrapperBlock = wrapperResult
|
||||
? [
|
||||
@@ -668,6 +760,7 @@ const forceRecall = async (event: any) => {
|
||||
.join("\n")
|
||||
: "";
|
||||
|
||||
const proactiveReportGateBlock = buildProactiveReportGateBlock(proactiveReportGateResult);
|
||||
const gateLockBlock = buildGateLockBlock(gateLockResult);
|
||||
const autoChainPlanBlock = buildAutoChainPlanBlock(autoChainPlanResult);
|
||||
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.",
|
||||
"",
|
||||
wrapperBlock || null,
|
||||
proactiveReportGateBlock,
|
||||
gateLockBlock,
|
||||
autoChainPlanBlock,
|
||||
approvedPlanContinuityBlock || null,
|
||||
|
||||
Reference in New Issue
Block a user