feat: route force-recall continuity via plugin adapter
This commit is contained in:
@@ -1,7 +1,63 @@
|
||||
export function createForceRecallContinuityAdapter() {
|
||||
throw new Error('Not implemented: force-recall continuity adapter contract placeholder');
|
||||
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
export function runForceRecallContinuityAdapter() {
|
||||
throw new Error('Not implemented: force-recall continuity adapter contract placeholder');
|
||||
export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult = null) {
|
||||
if (!wrapperResult || wrapperResult.classification !== 'long_task') return null;
|
||||
|
||||
const wrapperNextAction = wrapperResult?.nextDerivedAction ?? wrapperResult?.derivedAction ?? null;
|
||||
const plannerDerivedAction = autoChainPlanResult?.derivedAction && autoChainPlanResult.derivedAction !== 'none'
|
||||
? {
|
||||
type: autoChainPlanResult.dispatchMode ?? 'no_dispatch',
|
||||
action: autoChainPlanResult.derivedAction,
|
||||
}
|
||||
: null;
|
||||
const nextDerivedAction = wrapperNextAction ?? plannerDerivedAction;
|
||||
|
||||
if (nextDerivedAction == null) return null;
|
||||
|
||||
const replyClosureState = isNonEmptyString(wrapperResult?.replyClosureState)
|
||||
? wrapperResult.replyClosureState
|
||||
: (wrapperResult?.handoff?.mode === 'button_path' ? 'waiting_user' : 'completed');
|
||||
|
||||
const dispatchReceipt = wrapperResult?.dispatchReceipt ?? null;
|
||||
const nextTaskKnown = wrapperResult?.nextTaskKnown === true
|
||||
|| (plannerDerivedAction != null && isNonEmptyString(autoChainPlanResult?.derivedAction) && autoChainPlanResult.derivedAction !== 'none');
|
||||
const sameApprovedPlan = wrapperResult?.sameApprovedPlan === true || plannerDerivedAction != null;
|
||||
const taskBoundaryStop = wrapperResult?.taskBoundaryStop === true || replyClosureState === 'completed';
|
||||
const highRiskStop = wrapperResult?.highRiskStop === true;
|
||||
|
||||
return {
|
||||
planId: wrapperResult?.planId ?? 'hook-preflight-approved-plan',
|
||||
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? 'hook-preflight-task',
|
||||
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? 'complete' : null),
|
||||
nextDerivedAction,
|
||||
replyClosureState,
|
||||
dispatchReceipt,
|
||||
nextTaskKnown,
|
||||
sameApprovedPlan,
|
||||
taskBoundaryStop,
|
||||
highRiskStop,
|
||||
};
|
||||
}
|
||||
|
||||
export function createForceRecallContinuityAdapter(config = {}) {
|
||||
const legalTerminalStates = config?.legalTerminalStates;
|
||||
const label = config?.adapter?.forceRecall?.injectBlockLabel ?? 'APPROVED_PLAN_CONTINUITY_GATE';
|
||||
|
||||
return {
|
||||
evaluate({ wrapperResult, autoChainPlanResult = null }) {
|
||||
const input = buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult);
|
||||
if (!input) return { input: null, result: null, block: '' };
|
||||
const result = evaluateContinuity(input, { legalTerminalStates });
|
||||
const block = buildContinuityGateBlock(result, { legalTerminalStates, label });
|
||||
return { input, result, block };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function runForceRecallContinuityAdapter({ wrapperResult, autoChainPlanResult = null, config = {} } = {}) {
|
||||
return createForceRecallContinuityAdapter(config).evaluate({ wrapperResult, autoChainPlanResult });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,120 @@
|
||||
export function evaluateContinuity() {
|
||||
throw new Error('Not implemented: continuity evaluator contract placeholder');
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function buildContinuityGateBlock() {
|
||||
throw new Error('Not implemented: continuity gate block contract placeholder');
|
||||
function isNonEmptyString(value) {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function normalizeAction(action) {
|
||||
return JSON.stringify(action ?? null);
|
||||
}
|
||||
|
||||
export function hasValidDispatchReceipt(receipt) {
|
||||
if (!isPlainObject(receipt)) return false;
|
||||
if (!isNonEmptyString(receipt.planId)) return false;
|
||||
if (!isNonEmptyString(receipt.currentTask)) return false;
|
||||
if (!isPlainObject(receipt.nextDerivedAction)) return false;
|
||||
if (!isNonEmptyString(receipt.dispatchedAt)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export function evaluateContinuity(payload, options = {}) {
|
||||
const legalTerminalStates = new Set(options.legalTerminalStates ?? ['waiting_user', 'blocked', 'pending_verification']);
|
||||
const taskComplete = payload?.taskState === 'complete';
|
||||
const nextAction = payload?.nextDerivedAction ?? payload?.derivedAction ?? null;
|
||||
const nextActionKnown = nextAction != 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 = legalTerminalStates.has(closureState);
|
||||
const hasDispatchReceipt = receiptMatchesPayload(payload, payload?.dispatchReceipt ?? null);
|
||||
const autoNextObligatory = taskComplete
|
||||
&& explicitNextTaskKnown
|
||||
&& sameApprovedPlan
|
||||
&& taskBoundaryStop
|
||||
&& !isLegalTerminalState
|
||||
&& !highRiskStop;
|
||||
|
||||
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',
|
||||
verdict: 'continuity_failure',
|
||||
reason: 'missing_dispatch_receipt',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 'pass',
|
||||
verdict: 'pass',
|
||||
};
|
||||
}
|
||||
|
||||
export function buildContinuityGateBlock(result, options = {}) {
|
||||
if (!result) return '';
|
||||
const label = isNonEmptyString(options.label) ? options.label.trim() : 'APPROVED_PLAN_CONTINUITY_GATE';
|
||||
const terminalStates = options.legalTerminalStates ?? ['waiting_user', 'blocked', 'pending_verification'];
|
||||
const lines = [
|
||||
`[${label}]`,
|
||||
`status=${result.status}`,
|
||||
`verdict=${result.verdict}`,
|
||||
];
|
||||
|
||||
if (result.reason) lines.push(`reason=${result.reason}`);
|
||||
|
||||
if (result.ok === false) {
|
||||
lines.push('- HARD_GATE: Do not close out this reply as normal completion.');
|
||||
if (result.reason === 'missing_auto_next_dispatch') {
|
||||
lines.push('- HARD_GATE: Do not stop at this completed-task boundary.');
|
||||
lines.push(`- HARD_GATE: Auto-dispatch the next task in the same approved plan, unless ${terminalStates.join(', ')}, or high-risk stop applies.`);
|
||||
} else {
|
||||
lines.push(`- HARD_GATE: Route back to continuity failure until a real next dispatch receipt exists, unless closure state is ${terminalStates.join(', ')}.`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`[/${label}]`, '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -7,12 +7,20 @@ import {
|
||||
import {
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
hasValidDispatchReceipt,
|
||||
receiptMatchesPayload,
|
||||
} from './continuity/evaluator.mjs';
|
||||
import {
|
||||
validateReceipt,
|
||||
isValidReceipt,
|
||||
} from './continuity/receipt-validator.mjs';
|
||||
import {
|
||||
slugifyReceiptSegment,
|
||||
buildReceiptFilename,
|
||||
writeReceipt,
|
||||
} from './continuity/receipt-store.mjs';
|
||||
import {
|
||||
buildApprovedPlanContinuityInput,
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
} from './adapters/force-recall.mjs';
|
||||
@@ -25,8 +33,14 @@ export {
|
||||
normalizeContinuityConfig,
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
hasValidDispatchReceipt,
|
||||
receiptMatchesPayload,
|
||||
validateReceipt,
|
||||
isValidReceipt,
|
||||
slugifyReceiptSegment,
|
||||
buildReceiptFilename,
|
||||
writeReceipt,
|
||||
buildApprovedPlanContinuityInput,
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
};
|
||||
@@ -39,8 +53,14 @@ export default {
|
||||
normalizeContinuityConfig,
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
hasValidDispatchReceipt,
|
||||
receiptMatchesPayload,
|
||||
validateReceipt,
|
||||
isValidReceipt,
|
||||
slugifyReceiptSegment,
|
||||
buildReceiptFilename,
|
||||
writeReceipt,
|
||||
buildApprovedPlanContinuityInput,
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user