fix continuity clean-room install verification
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
normalizeContinuityEngineInput,
|
||||
createContinuityEngineContract,
|
||||
} from '../continuity/engine.mjs';
|
||||
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
|
||||
|
||||
function isNonEmptyString(value) {
|
||||
@@ -29,7 +33,7 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
|
||||
const taskBoundaryStop = wrapperResult?.taskBoundaryStop === true || replyClosureState === 'completed';
|
||||
const highRiskStop = wrapperResult?.highRiskStop === true;
|
||||
|
||||
return {
|
||||
return normalizeContinuityEngineInput({
|
||||
planId: wrapperResult?.planId ?? 'hook-preflight-approved-plan',
|
||||
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? 'hook-preflight-task',
|
||||
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? 'complete' : null),
|
||||
@@ -40,7 +44,11 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
|
||||
sameApprovedPlan,
|
||||
taskBoundaryStop,
|
||||
highRiskStop,
|
||||
};
|
||||
metadata: {
|
||||
adapterSource: 'force-recall',
|
||||
classification: wrapperResult?.classification ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createForceRecallContinuityAdapter(config = {}) {
|
||||
@@ -50,10 +58,22 @@ export function createForceRecallContinuityAdapter(config = {}) {
|
||||
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 };
|
||||
if (!input) {
|
||||
return createContinuityEngineContract({
|
||||
input: null,
|
||||
evaluation: null,
|
||||
block: '',
|
||||
options: { adapterName: 'force-recall', label },
|
||||
});
|
||||
}
|
||||
const evaluation = evaluateContinuity(input, { legalTerminalStates });
|
||||
const block = buildContinuityGateBlock(evaluation, { legalTerminalStates, label });
|
||||
return createContinuityEngineContract({
|
||||
input,
|
||||
evaluation,
|
||||
block,
|
||||
options: { adapterName: 'force-recall', label },
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ export const defaultConfig = Object.freeze({
|
||||
enabled: true,
|
||||
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
|
||||
},
|
||||
genericPreflight: {
|
||||
enabled: true,
|
||||
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ export const continuityConfigSchema = Object.freeze({
|
||||
enabled: 'boolean',
|
||||
injectBlockLabel: 'string',
|
||||
},
|
||||
genericPreflight: {
|
||||
enabled: 'boolean',
|
||||
injectBlockLabel: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,8 +31,8 @@ const TOP_LEVEL_KEYS = new Set([
|
||||
'adapter',
|
||||
]);
|
||||
|
||||
const ADAPTER_KEYS = new Set(['forceRecall']);
|
||||
const FORCE_RECALL_KEYS = new Set(['enabled', 'injectBlockLabel']);
|
||||
const ADAPTER_KEYS = new Set(['forceRecall', 'genericPreflight']);
|
||||
const ADAPTER_CONFIG_KEYS = new Set(['enabled', 'injectBlockLabel']);
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
@@ -59,6 +63,35 @@ function validateStringArray(errors, value, fieldName) {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeAdapterConfig(baseAdapterConfig, inputAdapterConfig) {
|
||||
return {
|
||||
...baseAdapterConfig,
|
||||
...(isPlainObject(inputAdapterConfig) ? inputAdapterConfig : {}),
|
||||
injectBlockLabel: typeof inputAdapterConfig?.injectBlockLabel === 'string'
|
||||
? inputAdapterConfig.injectBlockLabel.trim()
|
||||
: baseAdapterConfig.injectBlockLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function validateNamedAdapter(errors, adapterInput, adapterKey) {
|
||||
if (!(adapterKey in adapterInput)) return;
|
||||
|
||||
if (!isPlainObject(adapterInput[adapterKey])) {
|
||||
errors.push(`adapter.${adapterKey}: expected object`);
|
||||
return;
|
||||
}
|
||||
|
||||
pushUnknownKeyErrors(errors, adapterInput[adapterKey], ADAPTER_CONFIG_KEYS, `adapter.${adapterKey}.`);
|
||||
|
||||
if ('enabled' in adapterInput[adapterKey] && typeof adapterInput[adapterKey].enabled !== 'boolean') {
|
||||
errors.push(`adapter.${adapterKey}.enabled: expected boolean`);
|
||||
}
|
||||
|
||||
if ('injectBlockLabel' in adapterInput[adapterKey] && !isNonEmptyString(adapterInput[adapterKey].injectBlockLabel)) {
|
||||
errors.push(`adapter.${adapterKey}.injectBlockLabel: expected non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeContinuityConfig(input = {}) {
|
||||
const base = cloneDefaultConfig();
|
||||
|
||||
@@ -79,17 +112,11 @@ export function normalizeContinuityConfig(input = {}) {
|
||||
adapter: {
|
||||
...base.adapter,
|
||||
...(isPlainObject(input.adapter) ? input.adapter : {}),
|
||||
forceRecall: {
|
||||
...base.adapter.forceRecall,
|
||||
...(isPlainObject(input.adapter?.forceRecall) ? input.adapter.forceRecall : {}),
|
||||
},
|
||||
forceRecall: normalizeAdapterConfig(base.adapter.forceRecall, input.adapter?.forceRecall),
|
||||
genericPreflight: normalizeAdapterConfig(base.adapter.genericPreflight, input.adapter?.genericPreflight),
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof normalized.adapter.forceRecall.injectBlockLabel === 'string') {
|
||||
normalized.adapter.forceRecall.injectBlockLabel = normalized.adapter.forceRecall.injectBlockLabel.trim();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -140,25 +167,8 @@ export function validateContinuityConfig(input = {}) {
|
||||
errors.push('adapter: expected object');
|
||||
} else {
|
||||
pushUnknownKeyErrors(errors, input.adapter, ADAPTER_KEYS, 'adapter.');
|
||||
|
||||
if ('forceRecall' in input.adapter) {
|
||||
if (!isPlainObject(input.adapter.forceRecall)) {
|
||||
errors.push('adapter.forceRecall: expected object');
|
||||
} else {
|
||||
pushUnknownKeyErrors(errors, input.adapter.forceRecall, FORCE_RECALL_KEYS, 'adapter.forceRecall.');
|
||||
|
||||
if ('enabled' in input.adapter.forceRecall && typeof input.adapter.forceRecall.enabled !== 'boolean') {
|
||||
errors.push('adapter.forceRecall.enabled: expected boolean');
|
||||
}
|
||||
|
||||
if (
|
||||
'injectBlockLabel' in input.adapter.forceRecall
|
||||
&& !isNonEmptyString(input.adapter.forceRecall.injectBlockLabel)
|
||||
) {
|
||||
errors.push('adapter.forceRecall.injectBlockLabel: expected non-empty string');
|
||||
}
|
||||
}
|
||||
}
|
||||
validateNamedAdapter(errors, input.adapter, 'forceRecall');
|
||||
validateNamedAdapter(errors, input.adapter, 'genericPreflight');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ export function buildContinuityGateBlock(result, options = {}) {
|
||||
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.`);
|
||||
lines.push('- HARD_GATE: Do not hand control back to the user with an ordinary progress update while auto-next is still obligatory.');
|
||||
lines.push('- HARD_GATE: If you cannot prove the next dispatch, convert this into an explicit continuity failure instead of a normal status report.');
|
||||
} else {
|
||||
lines.push(`- HARD_GATE: Route back to continuity failure until a real next dispatch receipt exists, unless closure state is ${terminalStates.join(', ')}.`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,41 @@
|
||||
# Continuity Types (MVP)
|
||||
# Continuity Types
|
||||
|
||||
## Host-agnostic continuity engine input
|
||||
|
||||
The generalized engine operates on a host-agnostic input object. Adapters are responsible for mapping host or hook specific context into this shape.
|
||||
|
||||
Minimum practical fields:
|
||||
|
||||
- `planId`: string
|
||||
- `currentTask`: string
|
||||
- `taskState`: string | null
|
||||
- `nextDerivedAction`: object | null
|
||||
- `replyClosureState`: string | null
|
||||
- `dispatchReceipt`: object | null
|
||||
- `nextTaskKnown`: boolean
|
||||
- `sameApprovedPlan`: boolean
|
||||
- `taskBoundaryStop`: boolean
|
||||
- `highRiskStop`: boolean
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `nextTaskId`: string | null
|
||||
- `nextTaskKey`: string | null
|
||||
- `derivedAction`: object | null
|
||||
- `metadata`: object
|
||||
|
||||
Normalization entrypoint:
|
||||
|
||||
- `normalizeContinuityEngineInput(input)`
|
||||
|
||||
Engine contract returned by generalized adapters:
|
||||
|
||||
- `input`: normalized engine input or `null`
|
||||
- `result`: summarized engine result object
|
||||
- `evaluation`: raw evaluator result or `null`
|
||||
- `block`: injected prompt block string
|
||||
- `meta.adapterName`: adapter identifier
|
||||
- `meta.hostAgnostic`: always `true`
|
||||
|
||||
## Receipt contract
|
||||
|
||||
@@ -20,4 +57,6 @@ The MVP receipt validator contract uses this minimum shape:
|
||||
## Notes
|
||||
|
||||
- This contract is intentionally minimal and keeps file I/O separate.
|
||||
- It mirrors the current approved-plan dispatch receipt fields used by the existing continuity scripts.
|
||||
- The engine is host-agnostic; host-specific behavior belongs in adapters.
|
||||
- `force-recall` remains the parity adapter for the current hook path.
|
||||
- `generic-preflight` is the minimal generalized adapter/runner for non-`force-recall` integration.
|
||||
|
||||
@@ -4,6 +4,11 @@ import {
|
||||
validateContinuityConfig,
|
||||
normalizeContinuityConfig,
|
||||
} from './config/schema.mjs';
|
||||
import {
|
||||
normalizeContinuityEngineInput,
|
||||
createContinuityEngineResult,
|
||||
createContinuityEngineContract,
|
||||
} from './continuity/engine.mjs';
|
||||
import {
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
@@ -24,6 +29,12 @@ import {
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
} from './adapters/force-recall.mjs';
|
||||
import {
|
||||
buildGenericContinuityInput,
|
||||
createGenericPreflightContinuityAdapter,
|
||||
runGenericPreflightContinuityAdapter,
|
||||
runManualContinuityPreflight,
|
||||
} from './adapters/generic-preflight.mjs';
|
||||
|
||||
export {
|
||||
defaultConfig,
|
||||
@@ -31,6 +42,9 @@ export {
|
||||
continuityConfigSchema,
|
||||
validateContinuityConfig,
|
||||
normalizeContinuityConfig,
|
||||
normalizeContinuityEngineInput,
|
||||
createContinuityEngineResult,
|
||||
createContinuityEngineContract,
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
hasValidDispatchReceipt,
|
||||
@@ -43,6 +57,10 @@ export {
|
||||
buildApprovedPlanContinuityInput,
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
buildGenericContinuityInput,
|
||||
createGenericPreflightContinuityAdapter,
|
||||
runGenericPreflightContinuityAdapter,
|
||||
runManualContinuityPreflight,
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -51,6 +69,9 @@ export default {
|
||||
continuityConfigSchema,
|
||||
validateContinuityConfig,
|
||||
normalizeContinuityConfig,
|
||||
normalizeContinuityEngineInput,
|
||||
createContinuityEngineResult,
|
||||
createContinuityEngineContract,
|
||||
evaluateContinuity,
|
||||
buildContinuityGateBlock,
|
||||
hasValidDispatchReceipt,
|
||||
@@ -63,4 +84,8 @@ export default {
|
||||
buildApprovedPlanContinuityInput,
|
||||
createForceRecallContinuityAdapter,
|
||||
runForceRecallContinuityAdapter,
|
||||
buildGenericContinuityInput,
|
||||
createGenericPreflightContinuityAdapter,
|
||||
runGenericPreflightContinuityAdapter,
|
||||
runManualContinuityPreflight,
|
||||
};
|
||||
|
||||
@@ -52,6 +52,19 @@ test('normalizes missing fields from defaults', () => {
|
||||
assert.equal(normalized.receiptDir, defaultConfig.receiptDir);
|
||||
assert.deepEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
|
||||
assert.notEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
|
||||
assert.equal(normalized.adapter.genericPreflight.enabled, true);
|
||||
});
|
||||
|
||||
test('normalizes generic preflight adapter block label', () => {
|
||||
const normalized = normalizeContinuityConfig({
|
||||
adapter: {
|
||||
genericPreflight: {
|
||||
injectBlockLabel: ' CUSTOM_GENERIC_GATE ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(normalized.adapter.genericPreflight.injectBlockLabel, 'CUSTOM_GENERIC_GATE');
|
||||
});
|
||||
|
||||
test('rejects non-array legalTerminalStates', () => {
|
||||
@@ -94,6 +107,17 @@ test('rejects malformed adapter.forceRecall shape', () => {
|
||||
assert.match(result.errors.join('\n'), /adapter\.forceRecall/);
|
||||
});
|
||||
|
||||
test('rejects malformed adapter.genericPreflight shape', () => {
|
||||
const result = validateContinuityConfig({
|
||||
adapter: {
|
||||
genericPreflight: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.errors.join('\n'), /adapter\.genericPreflight/);
|
||||
});
|
||||
|
||||
test('rejects malformed adapter.forceRecall.enabled type', () => {
|
||||
const result = validateContinuityConfig({
|
||||
adapter: {
|
||||
@@ -120,6 +144,32 @@ test('rejects malformed adapter.forceRecall.injectBlockLabel type', () => {
|
||||
assert.match(result.errors.join('\n'), /injectBlockLabel/);
|
||||
});
|
||||
|
||||
test('rejects malformed adapter.genericPreflight.enabled type', () => {
|
||||
const result = validateContinuityConfig({
|
||||
adapter: {
|
||||
genericPreflight: {
|
||||
enabled: 'yes',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.errors.join('\n'), /adapter\.genericPreflight\.enabled/);
|
||||
});
|
||||
|
||||
test('rejects malformed adapter.genericPreflight.injectBlockLabel type', () => {
|
||||
const result = validateContinuityConfig({
|
||||
adapter: {
|
||||
genericPreflight: {
|
||||
injectBlockLabel: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.errors.join('\n'), /adapter\.genericPreflight\.injectBlockLabel/);
|
||||
});
|
||||
|
||||
test('rejects unknown top-level key', () => {
|
||||
const result = validateContinuityConfig({
|
||||
unexpected: true,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import plugin, {
|
||||
createForceRecallContinuityAdapter,
|
||||
createGenericPreflightContinuityAdapter,
|
||||
defaultConfig,
|
||||
evaluateContinuity,
|
||||
runManualContinuityPreflight,
|
||||
} from '../src/index.mjs';
|
||||
|
||||
function test(name, fn) {
|
||||
@@ -19,6 +21,8 @@ test('index exports plugin surface', () => {
|
||||
assert.equal(plugin.name, '@openclaw/plugin-continuity');
|
||||
assert.equal(typeof evaluateContinuity, 'function');
|
||||
assert.equal(defaultConfig.adapter.forceRecall.enabled, true);
|
||||
assert.equal(defaultConfig.adapter.genericPreflight.enabled, true);
|
||||
assert.equal(typeof plugin.runGenericPreflightContinuityAdapter, 'function');
|
||||
});
|
||||
|
||||
test('adapter preserves current hook parity for plain wrapper next-action mapping', () => {
|
||||
@@ -36,6 +40,8 @@ test('adapter preserves current hook parity for plain wrapper next-action mappin
|
||||
});
|
||||
|
||||
assert.equal(out.result.ok, true);
|
||||
assert.equal(out.meta.adapterName, 'force-recall');
|
||||
assert.equal(out.meta.hostAgnostic, true);
|
||||
assert.match(out.block, /status=pass/);
|
||||
});
|
||||
|
||||
@@ -60,4 +66,41 @@ test('adapter fails when planner-derived auto-next boundary exists without dispa
|
||||
assert.match(out.block, /continuity_failure/);
|
||||
});
|
||||
|
||||
test('generic preflight adapter evaluates host-agnostic source payload', () => {
|
||||
const adapter = createGenericPreflightContinuityAdapter(defaultConfig);
|
||||
const out = adapter.evaluate({
|
||||
planId: 'plan-generic',
|
||||
currentTask: 'task-generic',
|
||||
taskState: 'complete',
|
||||
nextTaskKnown: true,
|
||||
sameApprovedPlan: true,
|
||||
taskBoundaryStop: true,
|
||||
nextTaskId: 'task-next',
|
||||
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||
replyClosureState: 'completed',
|
||||
dispatchReceipt: null,
|
||||
});
|
||||
|
||||
assert.equal(out.result.ok, false);
|
||||
assert.equal(out.result.reason, 'missing_auto_next_dispatch');
|
||||
assert.equal(out.meta.adapterName, 'generic-preflight');
|
||||
assert.equal(out.meta.hostAgnostic, true);
|
||||
assert.equal(out.input.planId, 'plan-generic');
|
||||
});
|
||||
|
||||
test('manual continuity preflight runner works without force-recall hook', () => {
|
||||
const out = runManualContinuityPreflight({
|
||||
config: defaultConfig,
|
||||
planId: 'plan-manual',
|
||||
currentTask: 'task-manual',
|
||||
taskState: 'complete',
|
||||
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||
replyClosureState: 'waiting_user',
|
||||
});
|
||||
|
||||
assert.equal(out.result.ok, true);
|
||||
assert.match(out.block, /APPROVED_PLAN_CONTINUITY_GATE/);
|
||||
assert.equal(out.meta.adapterName, 'generic-preflight');
|
||||
});
|
||||
|
||||
console.log('continuity.plugin.test.mjs PASS');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import plugin, {
|
||||
runForceRecallContinuityAdapter,
|
||||
runGenericPreflightContinuityAdapter,
|
||||
validateContinuityConfig,
|
||||
} from '../src/index.mjs';
|
||||
|
||||
@@ -25,4 +26,26 @@ const smoke = runForceRecallContinuityAdapter({
|
||||
assert.equal(smoke.result.ok, false);
|
||||
assert.equal(smoke.result.reason, 'missing_auto_next_dispatch');
|
||||
assert.match(smoke.block, /APPROVED_PLAN_CONTINUITY_GATE/);
|
||||
assert.equal(smoke.meta.adapterName, 'force-recall');
|
||||
|
||||
const genericSmoke = runGenericPreflightContinuityAdapter({
|
||||
config: plugin.defaultConfig,
|
||||
source: {
|
||||
planId: 'plan-generic-smoke',
|
||||
currentTask: 'task-9',
|
||||
taskState: 'complete',
|
||||
nextTaskKnown: true,
|
||||
sameApprovedPlan: true,
|
||||
taskBoundaryStop: true,
|
||||
nextTaskId: 'task-10',
|
||||
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||
replyClosureState: 'completed',
|
||||
dispatchReceipt: null,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(genericSmoke.result.ok, false);
|
||||
assert.equal(genericSmoke.result.reason, 'missing_auto_next_dispatch');
|
||||
assert.match(genericSmoke.block, /APPROVED_PLAN_CONTINUITY_GATE/);
|
||||
assert.equal(genericSmoke.meta.adapterName, 'generic-preflight');
|
||||
console.log('continuity.smoke.test.mjs PASS');
|
||||
|
||||
Reference in New Issue
Block a user