generalize continuity plugin engine and generic adapter
This commit is contained in:
@@ -394,14 +394,37 @@ const continuityAdapterModuleCache = new Map<string, Promise<ForceRecallContinui
|
|||||||
|
|
||||||
async function loadForceRecallContinuityAdapterModule(workspaceDir: string): Promise<ForceRecallContinuityAdapterModule | null> {
|
async function loadForceRecallContinuityAdapterModule(workspaceDir: string): Promise<ForceRecallContinuityAdapterModule | null> {
|
||||||
const adapterPath = path.join(workspaceDir, "plugins", "continuity", "src", "index.mjs");
|
const adapterPath = path.join(workspaceDir, "plugins", "continuity", "src", "index.mjs");
|
||||||
let modulePromise = continuityAdapterModuleCache.get(adapterPath);
|
|
||||||
|
|
||||||
if (!modulePromise) {
|
try {
|
||||||
modulePromise = import(pathToFileURL(adapterPath).href).catch(() => null);
|
const stat = await fs.stat(adapterPath);
|
||||||
continuityAdapterModuleCache.set(adapterPath, modulePromise);
|
const cacheKey = `${adapterPath}?mtimeMs=${stat.mtimeMs}`;
|
||||||
|
let modulePromise = continuityAdapterModuleCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (!modulePromise) {
|
||||||
|
modulePromise = import(pathToFileURL(adapterPath).href + `?mtimeMs=${stat.mtimeMs}`).catch(() => null);
|
||||||
|
continuityAdapterModuleCache.set(cacheKey, modulePromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modulePromise;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return modulePromise;
|
async function readContinuityPluginConfigOverrides(workspaceDir: string): Promise<Record<string, unknown>> {
|
||||||
|
const defaultsPath = path.join(workspaceDir, "plugins", "continuity", "src", "config", "defaults.mjs");
|
||||||
|
const source = await safeReadText(defaultsPath);
|
||||||
|
if (!source) return {};
|
||||||
|
|
||||||
|
const forceRecallLabel = source.match(/forceRecall:\s*\{[\s\S]*?injectBlockLabel:\s*['"]([^'"]+)['"]/);
|
||||||
|
const genericPreflightLabel = source.match(/genericPreflight:\s*\{[\s\S]*?injectBlockLabel:\s*['"]([^'"]+)['"]/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
adapter: {
|
||||||
|
forceRecall: forceRecallLabel ? { injectBlockLabel: forceRecallLabel[1] } : {},
|
||||||
|
genericPreflight: genericPreflightLabel ? { injectBlockLabel: genericPreflightLabel[1] } : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluateApprovedPlanContinuityViaPlugin(workspaceDir: string, wrapperResult: any, autoChainPlanResult: AutoChainPlanResult | null): Promise<{ input: Record<string, unknown> | null; result: ApprovedPlanContinuityResult | null; block: string; } | null> {
|
async function evaluateApprovedPlanContinuityViaPlugin(workspaceDir: string, wrapperResult: any, autoChainPlanResult: AutoChainPlanResult | null): Promise<{ input: Record<string, unknown> | null; result: ApprovedPlanContinuityResult | null; block: string; } | null> {
|
||||||
@@ -409,10 +432,27 @@ async function evaluateApprovedPlanContinuityViaPlugin(workspaceDir: string, wra
|
|||||||
const runAdapter = adapterModule?.runForceRecallContinuityAdapter;
|
const runAdapter = adapterModule?.runForceRecallContinuityAdapter;
|
||||||
if (typeof runAdapter !== "function") return null;
|
if (typeof runAdapter !== "function") return null;
|
||||||
|
|
||||||
|
const configOverrides = await readContinuityPluginConfigOverrides(workspaceDir);
|
||||||
|
|
||||||
return runAdapter({
|
return runAdapter({
|
||||||
wrapperResult,
|
wrapperResult,
|
||||||
autoChainPlanResult,
|
autoChainPlanResult,
|
||||||
config: adapterModule?.defaultConfig ?? {},
|
config: {
|
||||||
|
...(adapterModule?.defaultConfig ?? {}),
|
||||||
|
...configOverrides,
|
||||||
|
adapter: {
|
||||||
|
...(((adapterModule?.defaultConfig ?? {}) as any)?.adapter ?? {}),
|
||||||
|
...((configOverrides as any)?.adapter ?? {}),
|
||||||
|
forceRecall: {
|
||||||
|
...((((adapterModule?.defaultConfig ?? {}) as any)?.adapter?.forceRecall) ?? {}),
|
||||||
|
...(((configOverrides as any)?.adapter?.forceRecall) ?? {}),
|
||||||
|
},
|
||||||
|
genericPreflight: {
|
||||||
|
...((((adapterModule?.defaultConfig ?? {}) as any)?.adapter?.genericPreflight) ?? {}),
|
||||||
|
...(((configOverrides as any)?.adapter?.genericPreflight) ?? {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
# Continuity Plugin (MVP)
|
# Continuity Plugin (MVP → generalized checkpoint)
|
||||||
|
|
||||||
> 中文版:`README.zh-TW.md`
|
> 中文版:`README.zh-TW.md`
|
||||||
|
|
||||||
This package extracts the current approved-plan continuity hard gate into a small installable, testable OpenClaw plugin MVP.
|
This package extracts the current approved-plan continuity hard gate into a small installable, testable OpenClaw plugin.
|
||||||
|
|
||||||
The goal is not to reinvent workflow policy. The goal is to package the existing continuity evaluator, receipt contract, and force-recall adapter so other OpenClaw workspaces can reuse the same minimum integration.
|
The package still preserves the current approved-plan behavior, but it now moves one step closer to a more general **engine + adapter** structure:
|
||||||
|
|
||||||
## What this MVP currently provides
|
- a host-agnostic continuity engine input/output contract
|
||||||
|
- the existing `force-recall` parity adapter
|
||||||
|
- a new `generic-preflight` adapter and manual runner for non-`force-recall` integration
|
||||||
|
|
||||||
|
## What this package currently provides
|
||||||
|
|
||||||
- continuity config validation
|
- continuity config validation
|
||||||
- dispatch receipt contract validation
|
- dispatch receipt contract validation
|
||||||
@@ -14,6 +18,8 @@ The goal is not to reinvent workflow policy. The goal is to package the existing
|
|||||||
- approved-plan continuity gate evaluation
|
- approved-plan continuity gate evaluation
|
||||||
- prompt block generation for the continuity gate
|
- prompt block generation for the continuity gate
|
||||||
- a `force-recall` adapter that maps hook wrapper/planner output into continuity input
|
- a `force-recall` adapter that maps hook wrapper/planner output into continuity input
|
||||||
|
- a `generic-preflight` adapter that accepts host-agnostic continuity input directly
|
||||||
|
- a manual preflight runner for workspaces that do not use `force-recall`
|
||||||
|
|
||||||
## Install location
|
## Install location
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ Recommended location inside an OpenClaw workspace:
|
|||||||
<workspace>/plugins/continuity
|
<workspace>/plugins/continuity
|
||||||
```
|
```
|
||||||
|
|
||||||
With the current MVP integration, the related files normally look like this:
|
Possible integration layouts now include both the original `force-recall` path and a more generic path:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
<workspace>/
|
<workspace>/
|
||||||
@@ -59,10 +65,12 @@ plugins/continuity/
|
|||||||
index.mjs
|
index.mjs
|
||||||
adapters/
|
adapters/
|
||||||
force-recall.mjs
|
force-recall.mjs
|
||||||
|
generic-preflight.mjs
|
||||||
config/
|
config/
|
||||||
defaults.mjs
|
defaults.mjs
|
||||||
schema.mjs
|
schema.mjs
|
||||||
continuity/
|
continuity/
|
||||||
|
engine.mjs
|
||||||
evaluator.mjs
|
evaluator.mjs
|
||||||
receipt-store.mjs
|
receipt-store.mjs
|
||||||
receipt-validator.mjs
|
receipt-validator.mjs
|
||||||
@@ -80,10 +88,12 @@ plugins/continuity/
|
|||||||
|
|
||||||
- `src/config/schema.mjs`
|
- `src/config/schema.mjs`
|
||||||
- `src/config/defaults.mjs`
|
- `src/config/defaults.mjs`
|
||||||
|
- `src/continuity/engine.mjs`
|
||||||
- `src/continuity/evaluator.mjs`
|
- `src/continuity/evaluator.mjs`
|
||||||
- `src/continuity/receipt-validator.mjs`
|
- `src/continuity/receipt-validator.mjs`
|
||||||
- `src/continuity/receipt-store.mjs`
|
- `src/continuity/receipt-store.mjs`
|
||||||
- `src/adapters/force-recall.mjs`
|
- `src/adapters/force-recall.mjs`
|
||||||
|
- `src/adapters/generic-preflight.mjs`
|
||||||
- `src/index.mjs`
|
- `src/index.mjs`
|
||||||
|
|
||||||
`src/index.mjs` currently re-exports:
|
`src/index.mjs` currently re-exports:
|
||||||
@@ -91,11 +101,42 @@ plugins/continuity/
|
|||||||
- `defaultConfig`
|
- `defaultConfig`
|
||||||
- `cloneDefaultConfig()`
|
- `cloneDefaultConfig()`
|
||||||
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
||||||
|
- `normalizeContinuityEngineInput()`
|
||||||
|
- `createContinuityEngineResult()` / `createContinuityEngineContract()`
|
||||||
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
||||||
- `validateReceipt()` / `isValidReceipt()`
|
- `validateReceipt()` / `isValidReceipt()`
|
||||||
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
||||||
- `buildApprovedPlanContinuityInput()`
|
- `buildApprovedPlanContinuityInput()`
|
||||||
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
||||||
|
- `buildGenericContinuityInput()`
|
||||||
|
- `createGenericPreflightContinuityAdapter()` / `runGenericPreflightContinuityAdapter()`
|
||||||
|
- `runManualContinuityPreflight()`
|
||||||
|
|
||||||
|
## Host-agnostic engine contract
|
||||||
|
|
||||||
|
The generalized engine accepts a normalized continuity input with fields such as:
|
||||||
|
|
||||||
|
- `planId`
|
||||||
|
- `currentTask`
|
||||||
|
- `taskState`
|
||||||
|
- `nextDerivedAction`
|
||||||
|
- `replyClosureState`
|
||||||
|
- `dispatchReceipt`
|
||||||
|
- `nextTaskKnown`
|
||||||
|
- `sameApprovedPlan`
|
||||||
|
- `taskBoundaryStop`
|
||||||
|
- `highRiskStop`
|
||||||
|
|
||||||
|
Generalized adapters return a common contract:
|
||||||
|
|
||||||
|
- `input`
|
||||||
|
- `result`
|
||||||
|
- `evaluation`
|
||||||
|
- `block`
|
||||||
|
- `meta.adapterName`
|
||||||
|
- `meta.hostAgnostic`
|
||||||
|
|
||||||
|
See `src/continuity/types.md` for the concise contract notes.
|
||||||
|
|
||||||
## Example config
|
## Example config
|
||||||
|
|
||||||
@@ -118,6 +159,10 @@ Start from `examples/openclaw.continuity.example.json`:
|
|||||||
"forceRecall": {
|
"forceRecall": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
|
},
|
||||||
|
"genericPreflight": {
|
||||||
|
"enabled": true,
|
||||||
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,23 +170,9 @@ Start from `examples/openclaw.continuity.example.json`:
|
|||||||
|
|
||||||
Defaults are defined in `src/config/defaults.mjs`.
|
Defaults are defined in `src/config/defaults.mjs`.
|
||||||
|
|
||||||
## Hook integration
|
## Integration path A: `force-recall`
|
||||||
|
|
||||||
The primary MVP integration point is `hooks/force-recall/handler.ts`.
|
The original MVP integration point remains `hooks/force-recall/handler.ts`.
|
||||||
|
|
||||||
The current hook path is:
|
|
||||||
|
|
||||||
1. run long-task preflight / gate lock / auto-chain planner
|
|
||||||
2. dynamically load `plugins/continuity/src/index.mjs`
|
|
||||||
3. call `runForceRecallContinuityAdapter({ wrapperResult, autoChainPlanResult, config })`
|
|
||||||
4. prepend the returned block into `bodyForAgent`
|
|
||||||
|
|
||||||
The handler already contains the plugin path integration points. The key symbols are:
|
|
||||||
|
|
||||||
- `runForceRecallContinuityAdapter`
|
|
||||||
- `[APPROVED_PLAN_CONTINUITY_GATE]`
|
|
||||||
|
|
||||||
Minimal integration example:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import plugin from './plugins/continuity/src/index.mjs';
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
@@ -157,7 +188,48 @@ if (out?.block) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want a custom injected block label, override `adapter.forceRecall.injectBlockLabel`.
|
## Integration path B: generic/manual preflight
|
||||||
|
|
||||||
|
If your workspace does **not** use `force-recall`, you can still install and use the plugin by calling the generalized adapter or the manual runner directly.
|
||||||
|
|
||||||
|
### Generic preflight adapter
|
||||||
|
|
||||||
|
```js
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runGenericPreflightContinuityAdapter({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
source: {
|
||||||
|
planId: 'approved-plan-1',
|
||||||
|
currentTask: 'task-3',
|
||||||
|
taskState: 'complete',
|
||||||
|
nextTaskKnown: true,
|
||||||
|
sameApprovedPlan: true,
|
||||||
|
taskBoundaryStop: true,
|
||||||
|
nextTaskId: 'task-4',
|
||||||
|
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||||
|
replyClosureState: 'completed',
|
||||||
|
dispatchReceipt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual runner
|
||||||
|
|
||||||
|
```js
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runManualContinuityPreflight({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
planId: 'approved-plan-1',
|
||||||
|
currentTask: 'task-3',
|
||||||
|
taskState: 'complete',
|
||||||
|
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||||
|
replyClosureState: 'waiting_user',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If `out.block` is non-empty, prepend it into the prompt/body seen by the agent.
|
||||||
|
|
||||||
## Receipt contract
|
## Receipt contract
|
||||||
|
|
||||||
@@ -171,24 +243,6 @@ Minimum receipt shape:
|
|||||||
- `childSessionKey`
|
- `childSessionKey`
|
||||||
- `replyClosureState`
|
- `replyClosureState`
|
||||||
|
|
||||||
Example from `examples/approved-plan-receipt.example.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"planId": "example-plan",
|
|
||||||
"currentTask": "task-01",
|
|
||||||
"nextDerivedAction": {
|
|
||||||
"kind": "delegate",
|
|
||||||
"target": "subagent",
|
|
||||||
"task": "placeholder"
|
|
||||||
},
|
|
||||||
"dispatchedAt": "2026-04-24T16:43:00+08:00",
|
|
||||||
"dispatchRunId": "example-run",
|
|
||||||
"childSessionKey": "session-placeholder",
|
|
||||||
"replyClosureState": "pending_verification"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To persist a receipt:
|
To persist a receipt:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -202,51 +256,51 @@ await writeReceipt({
|
|||||||
|
|
||||||
## Smoke test / verification
|
## Smoke test / verification
|
||||||
|
|
||||||
At minimum, run:
|
Required plugin verification:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd plugins/continuity
|
cd plugins/continuity
|
||||||
npm test
|
npm test
|
||||||
|
node test/continuity.smoke.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
If your workspace uses `force-recall`, also run:
|
||||||
|
|
||||||
|
```bash
|
||||||
cd /path/to/workspace
|
cd /path/to/workspace
|
||||||
node scripts/test_force_recall_long_task_preflight.mjs
|
node scripts/test_force_recall_long_task_preflight.mjs
|
||||||
node --check hooks/force-recall/handler.ts
|
node --check hooks/force-recall/handler.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
For a minimal plugin-only check, you can also run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd plugins/continuity
|
|
||||||
node test/continuity.smoke.test.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install and apply steps for another OpenClaw workspace
|
## Install and apply steps for another OpenClaw workspace
|
||||||
|
|
||||||
1. Copy `plugins/continuity` into your workspace.
|
1. Copy `plugins/continuity` into your workspace.
|
||||||
2. Ensure `hooks/force-recall/handler.ts` loads `plugins/continuity/src/index.mjs`.
|
2. Choose one integration path:
|
||||||
3. Adjust the continuity config as needed, especially:
|
- `force-recall`: load `runForceRecallContinuityAdapter(...)`
|
||||||
|
- no `force-recall`: call `runGenericPreflightContinuityAdapter(...)` or `runManualContinuityPreflight(...)`
|
||||||
|
3. Adjust config as needed, especially:
|
||||||
- `planMatchers`
|
- `planMatchers`
|
||||||
- `legalTerminalStates`
|
- `legalTerminalStates`
|
||||||
- `receiptDir`
|
- `receiptDir`
|
||||||
- `adapter.forceRecall.injectBlockLabel`
|
- `adapter.forceRecall.injectBlockLabel`
|
||||||
|
- `adapter.genericPreflight.injectBlockLabel`
|
||||||
4. If your dispatch flow creates child runs/sessions, persist a real receipt.
|
4. If your dispatch flow creates child runs/sessions, persist a real receipt.
|
||||||
5. Run the smoke test and the force-recall preflight test.
|
5. Run plugin tests and the relevant workspace smoke path.
|
||||||
6. Confirm the agent prompt contains the continuity gate block and that dry-run dispatch alone does not pass the gate.
|
6. Confirm the agent prompt contains the continuity gate block and that dry-run dispatch alone does not pass the gate.
|
||||||
|
|
||||||
## Current limitations
|
## Current limitations
|
||||||
|
|
||||||
- This is an MVP extraction of the **approved-plan continuity hard gate**, not a general workflow engine.
|
- This is still centered on the approved-plan continuity hard gate, not a full general workflow engine.
|
||||||
- The main adapter is `force-recall`; the package is not yet generalized into a multi-hook / multi-event integration layer.
|
- The generalized engine contract is intentionally minimal and conservative.
|
||||||
- Config is still passed as module defaults plus caller input; there is not yet a full OpenClaw plugin installer/registration guide.
|
- `force-recall` remains the most battle-tested adapter.
|
||||||
- The receipt store only writes files; it does not manage retention, cleanup, or indexing.
|
- The receipt store only writes files; it does not manage retention, cleanup, or indexing.
|
||||||
- The receipt validator checks the minimum contract only; it does not deeply validate every `nextDerivedAction` subtype.
|
- The receipt validator checks the minimum contract only; it does not deeply validate every `nextDerivedAction` subtype.
|
||||||
- The documented install path assumes the existing `force-recall` preflight chain; if your workspace does not use that chain, you still need your own glue code.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Default legal terminal states are `waiting_user`, `blocked`, and `pending_verification`
|
- Default legal terminal states are `waiting_user`, `blocked`, and `pending_verification`
|
||||||
- The evaluator preserves current behavior, including `missing_dispatch_receipt` and `missing_auto_next_dispatch`
|
- The evaluator preserves current behavior, including `missing_dispatch_receipt` and `missing_auto_next_dispatch`
|
||||||
- The adapter mirrors the continuity input mapping used by `hooks/force-recall/handler.ts`
|
- The new generic path makes the plugin more reusable even without the `force-recall` hook
|
||||||
- `HOOK.md` describes the plugin/hook adapter contract boundary, not the full installation guide
|
- `HOOK.md` describes the plugin/hook adapter contract boundary, not the full installation guide
|
||||||
|
|
||||||
## Chinese documentation
|
## Chinese documentation
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# Continuity Plugin(MVP)
|
# Continuity Plugin(MVP → generalized checkpoint)
|
||||||
|
|
||||||
> English version: `README.md`
|
> English version: `README.md`
|
||||||
|
|
||||||
這個套件把目前 approved-plan continuity hard gate 抽離成一個可安裝、可測試、可在 hook 內重用的 OpenClaw plugin MVP。
|
這個套件把目前 approved-plan continuity hard gate 抽離成一個可安裝、可測試、可在 hook 內重用的 OpenClaw plugin。
|
||||||
|
|
||||||
目標不是重新發明規則,而是把既有 continuity 判斷、receipt contract、force-recall adapter 收斂成一個可以被其他 OpenClaw workspace 直接帶走的最小可用包。
|
它仍保留既有 approved-plan 行為,但現在往比較通用的 **engine + adapter** 結構前進了一步:
|
||||||
|
|
||||||
|
- 有 host-agnostic 的 continuity engine input/output contract
|
||||||
|
- 保留既有 `force-recall` parity adapter
|
||||||
|
- 新增 `generic-preflight` adapter 與 manual runner,讓未使用 `force-recall` 的 workspace 也能接
|
||||||
|
|
||||||
## 目前能做什麼
|
## 目前能做什麼
|
||||||
|
|
||||||
@@ -14,6 +18,8 @@
|
|||||||
- 評估 approved-plan continuity gate
|
- 評估 approved-plan continuity gate
|
||||||
- 產生可注入 prompt 的 continuity gate block
|
- 產生可注入 prompt 的 continuity gate block
|
||||||
- 透過 `force-recall` adapter,把 hook 端的 wrapper/planner 結果轉成 continuity input
|
- 透過 `force-recall` adapter,把 hook 端的 wrapper/planner 結果轉成 continuity input
|
||||||
|
- 透過 `generic-preflight` adapter,直接吃 host-agnostic continuity input
|
||||||
|
- 透過 manual preflight runner,在沒有 `force-recall` 的情況下也能直接呼叫
|
||||||
|
|
||||||
## 安裝位置
|
## 安裝位置
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@
|
|||||||
<workspace>/plugins/continuity
|
<workspace>/plugins/continuity
|
||||||
```
|
```
|
||||||
|
|
||||||
以目前 MVP 慣例,相關檔案位置如下:
|
現在可支援兩類整合路徑:原本的 `force-recall` 路徑,以及較通用的 generic path。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
<workspace>/
|
<workspace>/
|
||||||
@@ -59,10 +65,12 @@ plugins/continuity/
|
|||||||
index.mjs
|
index.mjs
|
||||||
adapters/
|
adapters/
|
||||||
force-recall.mjs
|
force-recall.mjs
|
||||||
|
generic-preflight.mjs
|
||||||
config/
|
config/
|
||||||
defaults.mjs
|
defaults.mjs
|
||||||
schema.mjs
|
schema.mjs
|
||||||
continuity/
|
continuity/
|
||||||
|
engine.mjs
|
||||||
evaluator.mjs
|
evaluator.mjs
|
||||||
receipt-store.mjs
|
receipt-store.mjs
|
||||||
receipt-validator.mjs
|
receipt-validator.mjs
|
||||||
@@ -80,10 +88,12 @@ plugins/continuity/
|
|||||||
|
|
||||||
- `src/config/schema.mjs`
|
- `src/config/schema.mjs`
|
||||||
- `src/config/defaults.mjs`
|
- `src/config/defaults.mjs`
|
||||||
|
- `src/continuity/engine.mjs`
|
||||||
- `src/continuity/evaluator.mjs`
|
- `src/continuity/evaluator.mjs`
|
||||||
- `src/continuity/receipt-validator.mjs`
|
- `src/continuity/receipt-validator.mjs`
|
||||||
- `src/continuity/receipt-store.mjs`
|
- `src/continuity/receipt-store.mjs`
|
||||||
- `src/adapters/force-recall.mjs`
|
- `src/adapters/force-recall.mjs`
|
||||||
|
- `src/adapters/generic-preflight.mjs`
|
||||||
- `src/index.mjs`
|
- `src/index.mjs`
|
||||||
|
|
||||||
`src/index.mjs` 目前會 re-export:
|
`src/index.mjs` 目前會 re-export:
|
||||||
@@ -91,11 +101,42 @@ plugins/continuity/
|
|||||||
- `defaultConfig`
|
- `defaultConfig`
|
||||||
- `cloneDefaultConfig()`
|
- `cloneDefaultConfig()`
|
||||||
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
||||||
|
- `normalizeContinuityEngineInput()`
|
||||||
|
- `createContinuityEngineResult()` / `createContinuityEngineContract()`
|
||||||
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
||||||
- `validateReceipt()` / `isValidReceipt()`
|
- `validateReceipt()` / `isValidReceipt()`
|
||||||
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
||||||
- `buildApprovedPlanContinuityInput()`
|
- `buildApprovedPlanContinuityInput()`
|
||||||
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
||||||
|
- `buildGenericContinuityInput()`
|
||||||
|
- `createGenericPreflightContinuityAdapter()` / `runGenericPreflightContinuityAdapter()`
|
||||||
|
- `runManualContinuityPreflight()`
|
||||||
|
|
||||||
|
## Host-agnostic engine contract
|
||||||
|
|
||||||
|
generalized engine 會吃一個正規化後的 continuity input,常用欄位包括:
|
||||||
|
|
||||||
|
- `planId`
|
||||||
|
- `currentTask`
|
||||||
|
- `taskState`
|
||||||
|
- `nextDerivedAction`
|
||||||
|
- `replyClosureState`
|
||||||
|
- `dispatchReceipt`
|
||||||
|
- `nextTaskKnown`
|
||||||
|
- `sameApprovedPlan`
|
||||||
|
- `taskBoundaryStop`
|
||||||
|
- `highRiskStop`
|
||||||
|
|
||||||
|
generalized adapter 會回傳共同 contract:
|
||||||
|
|
||||||
|
- `input`
|
||||||
|
- `result`
|
||||||
|
- `evaluation`
|
||||||
|
- `block`
|
||||||
|
- `meta.adapterName`
|
||||||
|
- `meta.hostAgnostic`
|
||||||
|
|
||||||
|
精簡契約請見 `src/continuity/types.md`。
|
||||||
|
|
||||||
## Example config
|
## Example config
|
||||||
|
|
||||||
@@ -118,6 +159,10 @@ plugins/continuity/
|
|||||||
"forceRecall": {
|
"forceRecall": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
|
},
|
||||||
|
"genericPreflight": {
|
||||||
|
"enabled": true,
|
||||||
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,23 +170,9 @@ plugins/continuity/
|
|||||||
|
|
||||||
預設值定義在 `src/config/defaults.mjs`。
|
預設值定義在 `src/config/defaults.mjs`。
|
||||||
|
|
||||||
## Hook 接法
|
## 整合路徑 A:`force-recall`
|
||||||
|
|
||||||
MVP 的主要整合點是 `hooks/force-recall/handler.ts`。
|
原本的 MVP 整合點仍是 `hooks/force-recall/handler.ts`。
|
||||||
|
|
||||||
目前 hook 端做法是:
|
|
||||||
|
|
||||||
1. 先完成 long-task preflight / gate lock / auto-chain planner
|
|
||||||
2. 再動態載入 `plugins/continuity/src/index.mjs`
|
|
||||||
3. 呼叫 `runForceRecallContinuityAdapter({ wrapperResult, autoChainPlanResult, config })`
|
|
||||||
4. 把 adapter 產出的 block 注入 `bodyForAgent`
|
|
||||||
|
|
||||||
`handler.ts` 內已有 plugin 路徑接點,關鍵符號是:
|
|
||||||
|
|
||||||
- `runForceRecallContinuityAdapter`
|
|
||||||
- `[APPROVED_PLAN_CONTINUITY_GATE]`
|
|
||||||
|
|
||||||
最小接法示意:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import plugin from './plugins/continuity/src/index.mjs';
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
@@ -157,7 +188,48 @@ if (out?.block) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
若要覆蓋 block label,可改 `adapter.forceRecall.injectBlockLabel`。
|
## 整合路徑 B:generic / manual preflight
|
||||||
|
|
||||||
|
如果你的 workspace **沒有** 使用 `force-recall`,現在也可以安裝這個 plugin,直接呼叫 generalized adapter 或 manual runner。
|
||||||
|
|
||||||
|
### Generic preflight adapter
|
||||||
|
|
||||||
|
```js
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runGenericPreflightContinuityAdapter({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
source: {
|
||||||
|
planId: 'approved-plan-1',
|
||||||
|
currentTask: 'task-3',
|
||||||
|
taskState: 'complete',
|
||||||
|
nextTaskKnown: true,
|
||||||
|
sameApprovedPlan: true,
|
||||||
|
taskBoundaryStop: true,
|
||||||
|
nextTaskId: 'task-4',
|
||||||
|
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||||
|
replyClosureState: 'completed',
|
||||||
|
dispatchReceipt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual runner
|
||||||
|
|
||||||
|
```js
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runManualContinuityPreflight({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
planId: 'approved-plan-1',
|
||||||
|
currentTask: 'task-3',
|
||||||
|
taskState: 'complete',
|
||||||
|
nextDerivedAction: { type: 'message_subagent', task: 'continue' },
|
||||||
|
replyClosureState: 'waiting_user',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
若 `out.block` 非空,就把它 prepend 到 agent 會看到的 prompt/body。
|
||||||
|
|
||||||
## Receipt contract
|
## Receipt contract
|
||||||
|
|
||||||
@@ -171,24 +243,6 @@ if (out?.block) {
|
|||||||
- `childSessionKey`
|
- `childSessionKey`
|
||||||
- `replyClosureState`
|
- `replyClosureState`
|
||||||
|
|
||||||
範例可參考 `examples/approved-plan-receipt.example.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"planId": "example-plan",
|
|
||||||
"currentTask": "task-01",
|
|
||||||
"nextDerivedAction": {
|
|
||||||
"kind": "delegate",
|
|
||||||
"target": "subagent",
|
|
||||||
"task": "placeholder"
|
|
||||||
},
|
|
||||||
"dispatchedAt": "2026-04-24T16:43:00+08:00",
|
|
||||||
"dispatchRunId": "example-run",
|
|
||||||
"childSessionKey": "session-placeholder",
|
|
||||||
"replyClosureState": "pending_verification"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
若要把 receipt 落盤,可用:
|
若要把 receipt 落盤,可用:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -202,51 +256,51 @@ await writeReceipt({
|
|||||||
|
|
||||||
## Smoke test / 驗證
|
## Smoke test / 驗證
|
||||||
|
|
||||||
至少執行以下驗證:
|
plugin 本體必要驗證:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd plugins/continuity
|
cd plugins/continuity
|
||||||
npm test
|
npm test
|
||||||
|
node test/continuity.smoke.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
若你的 workspace 有使用 `force-recall`,再補跑:
|
||||||
|
|
||||||
|
```bash
|
||||||
cd /path/to/workspace
|
cd /path/to/workspace
|
||||||
node scripts/test_force_recall_long_task_preflight.mjs
|
node scripts/test_force_recall_long_task_preflight.mjs
|
||||||
node --check hooks/force-recall/handler.ts
|
node --check hooks/force-recall/handler.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
若只想先做 plugin 本體最小檢查,也可以:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd plugins/continuity
|
|
||||||
node test/continuity.smoke.test.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安裝與套用步驟(給其他 OpenClaw 使用者)
|
## 安裝與套用步驟(給其他 OpenClaw 使用者)
|
||||||
|
|
||||||
1. 把 `plugins/continuity` 複製到你的 workspace。
|
1. 把 `plugins/continuity` 複製到你的 workspace。
|
||||||
2. 確認 `hooks/force-recall/handler.ts` 會載入 `plugins/continuity/src/index.mjs`。
|
2. 選一條整合路徑:
|
||||||
|
- `force-recall`:載入 `runForceRecallContinuityAdapter(...)`
|
||||||
|
- 沒有 `force-recall`:呼叫 `runGenericPreflightContinuityAdapter(...)` 或 `runManualContinuityPreflight(...)`
|
||||||
3. 視需要調整 continuity config,至少確認:
|
3. 視需要調整 continuity config,至少確認:
|
||||||
- `planMatchers`
|
- `planMatchers`
|
||||||
- `legalTerminalStates`
|
- `legalTerminalStates`
|
||||||
- `receiptDir`
|
- `receiptDir`
|
||||||
- `adapter.forceRecall.injectBlockLabel`
|
- `adapter.forceRecall.injectBlockLabel`
|
||||||
|
- `adapter.genericPreflight.injectBlockLabel`
|
||||||
4. 若你的 dispatch 流程會產生 child run/session,請同步寫出 receipt。
|
4. 若你的 dispatch 流程會產生 child run/session,請同步寫出 receipt。
|
||||||
5. 跑 smoke test 與 hook preflight 測試。
|
5. 跑 plugin 測試與對應 workspace smoke path。
|
||||||
6. 確認 agent prompt 內可見 continuity gate block,且 dry-run dispatch 不會被誤判為 pass。
|
6. 確認 agent prompt 內可見 continuity gate block,且 dry-run dispatch 不會被誤判為 pass。
|
||||||
|
|
||||||
## 目前限制
|
## 目前限制
|
||||||
|
|
||||||
- 目前是 **approved-plan continuity hard gate** 的 MVP 抽離,不是通用 workflow engine。
|
- 它仍以 approved-plan continuity hard gate 為中心,不是完整通用 workflow engine。
|
||||||
- 主要 adapter 只有 `force-recall`,尚未抽象成多 hook / 多事件通用介面。
|
- generalized engine contract 目前刻意保持最小且保守。
|
||||||
- config 目前是模組內預設 + 呼叫端傳入,還沒有完整的 OpenClaw plugin 安裝器/註冊流程文件。
|
- `force-recall` 仍是目前最成熟的 adapter。
|
||||||
- receipt store 只負責寫檔,不含 retention、cleanup、indexing。
|
- receipt store 只負責寫檔,不含 retention、cleanup、indexing。
|
||||||
- receipt validator 目前只檢查最小 contract,不驗證每個 `nextDerivedAction` 子欄位語意。
|
- receipt validator 目前只檢查最小 contract,不驗證每個 `nextDerivedAction` 子欄位語意。
|
||||||
- 文件描述的是「依現有 hook 整合」的安裝方式;若未採用 `force-recall` preflight 鏈,仍需自行補 glue code。
|
|
||||||
|
|
||||||
## 備註
|
## 備註
|
||||||
|
|
||||||
- 預設 legal terminal states:`waiting_user`、`blocked`、`pending_verification`
|
- 預設 legal terminal states:`waiting_user`、`blocked`、`pending_verification`
|
||||||
- evaluator 保留既有行為,包括 `missing_dispatch_receipt` 與 `missing_auto_next_dispatch`
|
- evaluator 保留既有行為,包括 `missing_dispatch_receipt` 與 `missing_auto_next_dispatch`
|
||||||
- adapter 維持與 `hooks/force-recall/handler.ts` 的 continuity input mapping 一致
|
- 新增 generic path 後,即使沒有 `force-recall` hook,也比較能重用這個 plugin
|
||||||
- `HOOK.md` 說明的是 plugin/hook adapter 契約定位,不是完整安裝說明
|
- `HOOK.md` 說明的是 plugin/hook adapter 契約定位,不是完整安裝說明
|
||||||
|
|
||||||
## 英文文件
|
## 英文文件
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineContract,
|
||||||
|
} from '../continuity/engine.mjs';
|
||||||
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
|
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
|
||||||
|
|
||||||
function isNonEmptyString(value) {
|
function isNonEmptyString(value) {
|
||||||
@@ -29,7 +33,7 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
|
|||||||
const taskBoundaryStop = wrapperResult?.taskBoundaryStop === true || replyClosureState === 'completed';
|
const taskBoundaryStop = wrapperResult?.taskBoundaryStop === true || replyClosureState === 'completed';
|
||||||
const highRiskStop = wrapperResult?.highRiskStop === true;
|
const highRiskStop = wrapperResult?.highRiskStop === true;
|
||||||
|
|
||||||
return {
|
return normalizeContinuityEngineInput({
|
||||||
planId: wrapperResult?.planId ?? 'hook-preflight-approved-plan',
|
planId: wrapperResult?.planId ?? 'hook-preflight-approved-plan',
|
||||||
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? 'hook-preflight-task',
|
currentTask: wrapperResult?.currentTask ?? wrapperResult?.requiredNextAction ?? 'hook-preflight-task',
|
||||||
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? 'complete' : null),
|
taskState: wrapperResult?.taskState ?? (plannerDerivedAction ? 'complete' : null),
|
||||||
@@ -40,7 +44,11 @@ export function buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanRes
|
|||||||
sameApprovedPlan,
|
sameApprovedPlan,
|
||||||
taskBoundaryStop,
|
taskBoundaryStop,
|
||||||
highRiskStop,
|
highRiskStop,
|
||||||
};
|
metadata: {
|
||||||
|
adapterSource: 'force-recall',
|
||||||
|
classification: wrapperResult?.classification ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createForceRecallContinuityAdapter(config = {}) {
|
export function createForceRecallContinuityAdapter(config = {}) {
|
||||||
@@ -50,10 +58,22 @@ export function createForceRecallContinuityAdapter(config = {}) {
|
|||||||
return {
|
return {
|
||||||
evaluate({ wrapperResult, autoChainPlanResult = null }) {
|
evaluate({ wrapperResult, autoChainPlanResult = null }) {
|
||||||
const input = buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult);
|
const input = buildApprovedPlanContinuityInput(wrapperResult, autoChainPlanResult);
|
||||||
if (!input) return { input: null, result: null, block: '' };
|
if (!input) {
|
||||||
const result = evaluateContinuity(input, { legalTerminalStates });
|
return createContinuityEngineContract({
|
||||||
const block = buildContinuityGateBlock(result, { legalTerminalStates, label });
|
input: null,
|
||||||
return { input, result, block };
|
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 },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
98
plugins/continuity/src/adapters/generic-preflight.mjs
Normal file
98
plugins/continuity/src/adapters/generic-preflight.mjs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineContract,
|
||||||
|
} from '../continuity/engine.mjs';
|
||||||
|
import { evaluateContinuity, buildContinuityGateBlock } from '../continuity/evaluator.mjs';
|
||||||
|
|
||||||
|
function isNonEmptyString(value) {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGenericContinuityInput(source = {}) {
|
||||||
|
const normalized = normalizeContinuityEngineInput(source);
|
||||||
|
|
||||||
|
if (!normalized.planId || !normalized.currentTask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGenericPreflightContinuityAdapter(config = {}) {
|
||||||
|
const legalTerminalStates = config?.legalTerminalStates;
|
||||||
|
const label = config?.adapter?.genericPreflight?.injectBlockLabel
|
||||||
|
?? config?.adapter?.forceRecall?.injectBlockLabel
|
||||||
|
?? 'APPROVED_PLAN_CONTINUITY_GATE';
|
||||||
|
|
||||||
|
return {
|
||||||
|
evaluate(source = {}) {
|
||||||
|
const input = buildGenericContinuityInput(source);
|
||||||
|
if (!input) {
|
||||||
|
return createContinuityEngineContract({
|
||||||
|
input: null,
|
||||||
|
evaluation: null,
|
||||||
|
block: '',
|
||||||
|
options: { adapterName: 'generic-preflight', label },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluation = evaluateContinuity(input, { legalTerminalStates });
|
||||||
|
const block = buildContinuityGateBlock(evaluation, { legalTerminalStates, label });
|
||||||
|
return createContinuityEngineContract({
|
||||||
|
input,
|
||||||
|
evaluation,
|
||||||
|
block,
|
||||||
|
options: { adapterName: 'generic-preflight', label },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runGenericPreflightContinuityAdapter({ source = {}, config = {} } = {}) {
|
||||||
|
return createGenericPreflightContinuityAdapter(config).evaluate(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runManualContinuityPreflight({
|
||||||
|
config = {},
|
||||||
|
planId,
|
||||||
|
currentTask,
|
||||||
|
taskState = null,
|
||||||
|
nextDerivedAction = null,
|
||||||
|
derivedAction = null,
|
||||||
|
replyClosureState = null,
|
||||||
|
dispatchReceipt = null,
|
||||||
|
nextTaskKnown = false,
|
||||||
|
sameApprovedPlan = false,
|
||||||
|
taskBoundaryStop = false,
|
||||||
|
highRiskStop = false,
|
||||||
|
nextTaskId = null,
|
||||||
|
nextTaskKey = null,
|
||||||
|
metadata = {},
|
||||||
|
} = {}) {
|
||||||
|
return runGenericPreflightContinuityAdapter({
|
||||||
|
config,
|
||||||
|
source: {
|
||||||
|
planId,
|
||||||
|
currentTask,
|
||||||
|
taskState,
|
||||||
|
nextDerivedAction: nextDerivedAction ?? derivedAction,
|
||||||
|
derivedAction: derivedAction ?? nextDerivedAction,
|
||||||
|
replyClosureState,
|
||||||
|
dispatchReceipt,
|
||||||
|
nextTaskKnown,
|
||||||
|
sameApprovedPlan,
|
||||||
|
taskBoundaryStop,
|
||||||
|
highRiskStop,
|
||||||
|
nextTaskId,
|
||||||
|
nextTaskKey,
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
buildGenericContinuityInput,
|
||||||
|
createGenericPreflightContinuityAdapter,
|
||||||
|
runGenericPreflightContinuityAdapter,
|
||||||
|
runManualContinuityPreflight,
|
||||||
|
};
|
||||||
@@ -11,6 +11,10 @@ export const defaultConfig = Object.freeze({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
|
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
|
||||||
},
|
},
|
||||||
|
genericPreflight: {
|
||||||
|
enabled: true,
|
||||||
|
injectBlockLabel: 'APPROVED_PLAN_CONTINUITY_GATE',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export const continuityConfigSchema = Object.freeze({
|
|||||||
enabled: 'boolean',
|
enabled: 'boolean',
|
||||||
injectBlockLabel: 'string',
|
injectBlockLabel: 'string',
|
||||||
},
|
},
|
||||||
|
genericPreflight: {
|
||||||
|
enabled: 'boolean',
|
||||||
|
injectBlockLabel: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,8 +31,8 @@ const TOP_LEVEL_KEYS = new Set([
|
|||||||
'adapter',
|
'adapter',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ADAPTER_KEYS = new Set(['forceRecall']);
|
const ADAPTER_KEYS = new Set(['forceRecall', 'genericPreflight']);
|
||||||
const FORCE_RECALL_KEYS = new Set(['enabled', 'injectBlockLabel']);
|
const ADAPTER_CONFIG_KEYS = new Set(['enabled', 'injectBlockLabel']);
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(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 = {}) {
|
export function normalizeContinuityConfig(input = {}) {
|
||||||
const base = cloneDefaultConfig();
|
const base = cloneDefaultConfig();
|
||||||
|
|
||||||
@@ -79,17 +112,11 @@ export function normalizeContinuityConfig(input = {}) {
|
|||||||
adapter: {
|
adapter: {
|
||||||
...base.adapter,
|
...base.adapter,
|
||||||
...(isPlainObject(input.adapter) ? input.adapter : {}),
|
...(isPlainObject(input.adapter) ? input.adapter : {}),
|
||||||
forceRecall: {
|
forceRecall: normalizeAdapterConfig(base.adapter.forceRecall, input.adapter?.forceRecall),
|
||||||
...base.adapter.forceRecall,
|
genericPreflight: normalizeAdapterConfig(base.adapter.genericPreflight, input.adapter?.genericPreflight),
|
||||||
...(isPlainObject(input.adapter?.forceRecall) ? input.adapter.forceRecall : {}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof normalized.adapter.forceRecall.injectBlockLabel === 'string') {
|
|
||||||
normalized.adapter.forceRecall.injectBlockLabel = normalized.adapter.forceRecall.injectBlockLabel.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,25 +167,8 @@ export function validateContinuityConfig(input = {}) {
|
|||||||
errors.push('adapter: expected object');
|
errors.push('adapter: expected object');
|
||||||
} else {
|
} else {
|
||||||
pushUnknownKeyErrors(errors, input.adapter, ADAPTER_KEYS, 'adapter.');
|
pushUnknownKeyErrors(errors, input.adapter, ADAPTER_KEYS, 'adapter.');
|
||||||
|
validateNamedAdapter(errors, input.adapter, 'forceRecall');
|
||||||
if ('forceRecall' in input.adapter) {
|
validateNamedAdapter(errors, input.adapter, 'genericPreflight');
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
plugins/continuity/src/continuity/engine.mjs
Normal file
66
plugins/continuity/src/continuity/engine.mjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNonEmptyString(value) {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeContinuityEngineInput(input = {}) {
|
||||||
|
if (!isPlainObject(input)) return {};
|
||||||
|
|
||||||
|
const normalized = {
|
||||||
|
planId: isNonEmptyString(input.planId) ? input.planId.trim() : input.planId ?? null,
|
||||||
|
currentTask: isNonEmptyString(input.currentTask) ? input.currentTask.trim() : input.currentTask ?? null,
|
||||||
|
taskState: isNonEmptyString(input.taskState) ? input.taskState.trim() : input.taskState ?? null,
|
||||||
|
nextTaskId: isNonEmptyString(input.nextTaskId) ? input.nextTaskId.trim() : input.nextTaskId ?? null,
|
||||||
|
nextTaskKey: isNonEmptyString(input.nextTaskKey) ? input.nextTaskKey.trim() : input.nextTaskKey ?? null,
|
||||||
|
nextDerivedAction: input.nextDerivedAction ?? input.derivedAction ?? null,
|
||||||
|
derivedAction: input.derivedAction ?? input.nextDerivedAction ?? null,
|
||||||
|
replyClosureState: isNonEmptyString(input.replyClosureState) ? input.replyClosureState.trim() : input.replyClosureState ?? null,
|
||||||
|
dispatchReceipt: input.dispatchReceipt ?? null,
|
||||||
|
nextTaskKnown: input.nextTaskKnown === true,
|
||||||
|
sameApprovedPlan: input.sameApprovedPlan === true,
|
||||||
|
taskBoundaryStop: input.taskBoundaryStop === true,
|
||||||
|
highRiskStop: input.highRiskStop === true,
|
||||||
|
metadata: isPlainObject(input.metadata) ? { ...input.metadata } : {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContinuityEngineResult(input, evaluation, options = {}) {
|
||||||
|
const label = isNonEmptyString(options.label) ? options.label.trim() : 'APPROVED_PLAN_CONTINUITY_GATE';
|
||||||
|
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
evaluation,
|
||||||
|
label,
|
||||||
|
ok: evaluation?.ok === true,
|
||||||
|
status: evaluation?.status ?? 'pass',
|
||||||
|
verdict: evaluation?.verdict ?? 'pass',
|
||||||
|
reason: evaluation?.reason ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createContinuityEngineContract({ input, evaluation, block, options = {} }) {
|
||||||
|
const result = createContinuityEngineResult(input, evaluation, options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
result,
|
||||||
|
evaluation,
|
||||||
|
block: typeof block === 'string' ? block : '',
|
||||||
|
meta: {
|
||||||
|
adapterName: options.adapterName ?? 'unknown',
|
||||||
|
label: result.label,
|
||||||
|
hostAgnostic: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineResult,
|
||||||
|
createContinuityEngineContract,
|
||||||
|
};
|
||||||
@@ -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
|
## Receipt contract
|
||||||
|
|
||||||
@@ -20,4 +57,6 @@ The MVP receipt validator contract uses this minimum shape:
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- This contract is intentionally minimal and keeps file I/O separate.
|
- 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,
|
validateContinuityConfig,
|
||||||
normalizeContinuityConfig,
|
normalizeContinuityConfig,
|
||||||
} from './config/schema.mjs';
|
} from './config/schema.mjs';
|
||||||
|
import {
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineResult,
|
||||||
|
createContinuityEngineContract,
|
||||||
|
} from './continuity/engine.mjs';
|
||||||
import {
|
import {
|
||||||
evaluateContinuity,
|
evaluateContinuity,
|
||||||
buildContinuityGateBlock,
|
buildContinuityGateBlock,
|
||||||
@@ -24,6 +29,12 @@ import {
|
|||||||
createForceRecallContinuityAdapter,
|
createForceRecallContinuityAdapter,
|
||||||
runForceRecallContinuityAdapter,
|
runForceRecallContinuityAdapter,
|
||||||
} from './adapters/force-recall.mjs';
|
} from './adapters/force-recall.mjs';
|
||||||
|
import {
|
||||||
|
buildGenericContinuityInput,
|
||||||
|
createGenericPreflightContinuityAdapter,
|
||||||
|
runGenericPreflightContinuityAdapter,
|
||||||
|
runManualContinuityPreflight,
|
||||||
|
} from './adapters/generic-preflight.mjs';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
@@ -31,6 +42,9 @@ export {
|
|||||||
continuityConfigSchema,
|
continuityConfigSchema,
|
||||||
validateContinuityConfig,
|
validateContinuityConfig,
|
||||||
normalizeContinuityConfig,
|
normalizeContinuityConfig,
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineResult,
|
||||||
|
createContinuityEngineContract,
|
||||||
evaluateContinuity,
|
evaluateContinuity,
|
||||||
buildContinuityGateBlock,
|
buildContinuityGateBlock,
|
||||||
hasValidDispatchReceipt,
|
hasValidDispatchReceipt,
|
||||||
@@ -43,6 +57,10 @@ export {
|
|||||||
buildApprovedPlanContinuityInput,
|
buildApprovedPlanContinuityInput,
|
||||||
createForceRecallContinuityAdapter,
|
createForceRecallContinuityAdapter,
|
||||||
runForceRecallContinuityAdapter,
|
runForceRecallContinuityAdapter,
|
||||||
|
buildGenericContinuityInput,
|
||||||
|
createGenericPreflightContinuityAdapter,
|
||||||
|
runGenericPreflightContinuityAdapter,
|
||||||
|
runManualContinuityPreflight,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -51,6 +69,9 @@ export default {
|
|||||||
continuityConfigSchema,
|
continuityConfigSchema,
|
||||||
validateContinuityConfig,
|
validateContinuityConfig,
|
||||||
normalizeContinuityConfig,
|
normalizeContinuityConfig,
|
||||||
|
normalizeContinuityEngineInput,
|
||||||
|
createContinuityEngineResult,
|
||||||
|
createContinuityEngineContract,
|
||||||
evaluateContinuity,
|
evaluateContinuity,
|
||||||
buildContinuityGateBlock,
|
buildContinuityGateBlock,
|
||||||
hasValidDispatchReceipt,
|
hasValidDispatchReceipt,
|
||||||
@@ -63,4 +84,8 @@ export default {
|
|||||||
buildApprovedPlanContinuityInput,
|
buildApprovedPlanContinuityInput,
|
||||||
createForceRecallContinuityAdapter,
|
createForceRecallContinuityAdapter,
|
||||||
runForceRecallContinuityAdapter,
|
runForceRecallContinuityAdapter,
|
||||||
|
buildGenericContinuityInput,
|
||||||
|
createGenericPreflightContinuityAdapter,
|
||||||
|
runGenericPreflightContinuityAdapter,
|
||||||
|
runManualContinuityPreflight,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ test('normalizes missing fields from defaults', () => {
|
|||||||
assert.equal(normalized.receiptDir, defaultConfig.receiptDir);
|
assert.equal(normalized.receiptDir, defaultConfig.receiptDir);
|
||||||
assert.deepEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
|
assert.deepEqual(normalized.legalTerminalStates, defaultConfig.legalTerminalStates);
|
||||||
assert.notEqual(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', () => {
|
test('rejects non-array legalTerminalStates', () => {
|
||||||
@@ -94,6 +107,17 @@ test('rejects malformed adapter.forceRecall shape', () => {
|
|||||||
assert.match(result.errors.join('\n'), /adapter\.forceRecall/);
|
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', () => {
|
test('rejects malformed adapter.forceRecall.enabled type', () => {
|
||||||
const result = validateContinuityConfig({
|
const result = validateContinuityConfig({
|
||||||
adapter: {
|
adapter: {
|
||||||
@@ -120,6 +144,32 @@ test('rejects malformed adapter.forceRecall.injectBlockLabel type', () => {
|
|||||||
assert.match(result.errors.join('\n'), /injectBlockLabel/);
|
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', () => {
|
test('rejects unknown top-level key', () => {
|
||||||
const result = validateContinuityConfig({
|
const result = validateContinuityConfig({
|
||||||
unexpected: true,
|
unexpected: true,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import plugin, {
|
import plugin, {
|
||||||
createForceRecallContinuityAdapter,
|
createForceRecallContinuityAdapter,
|
||||||
|
createGenericPreflightContinuityAdapter,
|
||||||
defaultConfig,
|
defaultConfig,
|
||||||
evaluateContinuity,
|
evaluateContinuity,
|
||||||
|
runManualContinuityPreflight,
|
||||||
} from '../src/index.mjs';
|
} from '../src/index.mjs';
|
||||||
|
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
@@ -19,6 +21,8 @@ test('index exports plugin surface', () => {
|
|||||||
assert.equal(plugin.name, '@openclaw/plugin-continuity');
|
assert.equal(plugin.name, '@openclaw/plugin-continuity');
|
||||||
assert.equal(typeof evaluateContinuity, 'function');
|
assert.equal(typeof evaluateContinuity, 'function');
|
||||||
assert.equal(defaultConfig.adapter.forceRecall.enabled, true);
|
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', () => {
|
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.result.ok, true);
|
||||||
|
assert.equal(out.meta.adapterName, 'force-recall');
|
||||||
|
assert.equal(out.meta.hostAgnostic, true);
|
||||||
assert.match(out.block, /status=pass/);
|
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/);
|
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');
|
console.log('continuity.plugin.test.mjs PASS');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import plugin, {
|
import plugin, {
|
||||||
runForceRecallContinuityAdapter,
|
runForceRecallContinuityAdapter,
|
||||||
|
runGenericPreflightContinuityAdapter,
|
||||||
validateContinuityConfig,
|
validateContinuityConfig,
|
||||||
} from '../src/index.mjs';
|
} from '../src/index.mjs';
|
||||||
|
|
||||||
@@ -25,4 +26,26 @@ const smoke = runForceRecallContinuityAdapter({
|
|||||||
assert.equal(smoke.result.ok, false);
|
assert.equal(smoke.result.ok, false);
|
||||||
assert.equal(smoke.result.reason, 'missing_auto_next_dispatch');
|
assert.equal(smoke.result.reason, 'missing_auto_next_dispatch');
|
||||||
assert.match(smoke.block, /APPROVED_PLAN_CONTINUITY_GATE/);
|
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');
|
console.log('continuity.smoke.test.mjs PASS');
|
||||||
|
|||||||
Reference in New Issue
Block a user