feat: sync auto-next obligation gate hardening

This commit is contained in:
2026-04-24 16:41:48 +08:00
parent 7c362dedf8
commit cb34935b28
9 changed files with 741 additions and 155 deletions

View File

@@ -11,6 +11,10 @@ function isObject(value) {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function normalizeAction(action) {
return JSON.stringify(action ?? null);
}
function hasValidDispatchReceipt(receipt) {
if (!isObject(receipt)) return false;
if (!isNonEmptyString(receipt.planId)) return false;
@@ -20,6 +24,27 @@ function hasValidDispatchReceipt(receipt) {
return true;
}
function receiptMatchesPayload(payload, receipt) {
if (!hasValidDispatchReceipt(receipt)) return false;
const expectedPlanId = payload?.planId;
if (isNonEmptyString(expectedPlanId) && receipt.planId !== expectedPlanId) return false;
const expectedCurrentTask = payload?.currentTask;
if (isNonEmptyString(expectedCurrentTask) && receipt.currentTask !== expectedCurrentTask) return false;
const expectedNextTask = payload?.nextTaskId ?? payload?.nextTaskKey ?? null;
const receiptNextTask = receipt?.nextTaskId ?? receipt?.nextTaskKey ?? null;
if (isNonEmptyString(expectedNextTask) && receiptNextTask !== expectedNextTask) return false;
const expectedNextAction = payload?.nextDerivedAction ?? payload?.derivedAction ?? null;
if (expectedNextAction != null && normalizeAction(receipt.nextDerivedAction) !== normalizeAction(expectedNextAction)) {
return false;
}
return true;
}
function parseArgs(argv) {
let inputPath = null;
let compact = false;
@@ -76,11 +101,39 @@ function evaluateContinuity(payload) {
const taskComplete = payload?.taskState === 'complete';
const nextAction = payload?.nextDerivedAction ?? payload?.derivedAction ?? null;
const nextActionKnown = nextAction != null;
const hasDispatchReceipt = hasValidDispatchReceipt(payload?.dispatchReceipt ?? null);
const explicitNextTaskKnown = payload?.nextTaskKnown === true;
const sameApprovedPlan = payload?.sameApprovedPlan === true;
const taskBoundaryStop = payload?.taskBoundaryStop === true;
const highRiskStop = payload?.highRiskStop === true;
const closureState = payload?.replyClosureState ?? null;
const isLegalTerminalState = LEGAL_TERMINAL_STATES.has(closureState);
const hasDispatchReceipt = receiptMatchesPayload(payload, payload?.dispatchReceipt ?? null);
const autoNextObligatory = taskComplete
&& explicitNextTaskKnown
&& sameApprovedPlan
&& taskBoundaryStop
&& !isLegalTerminalState
&& !highRiskStop;
if (taskComplete && nextActionKnown && !hasDispatchReceipt && !isLegalTerminalState) {
if (autoNextObligatory && !hasDispatchReceipt) {
return {
ok: false,
status: 'continuity_failure',
verdict: 'continuity_failure',
reason: 'missing_auto_next_dispatch',
};
}
if (taskComplete && nextActionKnown && !hasDispatchReceipt && !isLegalTerminalState && !highRiskStop && !('sameApprovedPlan' in (payload ?? {}))) {
return {
ok: false,
status: 'continuity_failure',
verdict: 'continuity_failure',
reason: 'missing_dispatch_receipt',
};
}
if (taskComplete && nextActionKnown && !hasDispatchReceipt && !isLegalTerminalState && !highRiskStop && sameApprovedPlan && !taskBoundaryStop && !explicitNextTaskKnown) {
return {
ok: false,
status: 'continuity_failure',
@@ -122,5 +175,4 @@ const response = {
},
};
process.stdout.write(`${JSON.stringify(response)}
`);
process.stdout.write(`${JSON.stringify(response)}\n`);