From d35fe70c21d341ec270e3c20b5e9d8684fc21761 Mon Sep 17 00:00:00 2001 From: "openclaw@cowbay.org" Date: Fri, 24 Apr 2026 19:49:45 +0800 Subject: [PATCH] docs: clarify generic continuity adapter paths --- plugins/continuity/AGENT_GUIDE.zh-TW.md | 76 ++++++-- plugins/continuity/HOOK.md | 33 +++- plugins/continuity/README.md | 166 ++++++++++++------ plugins/continuity/README.zh-TW.md | 162 +++++++++++------ .../examples/openclaw.continuity.example.json | 4 + plugins/continuity/package.json | 2 +- .../src/adapters/generic-preflight.mjs | 98 +++++++++++ plugins/continuity/src/continuity/engine.mjs | 66 +++++++ 8 files changed, 469 insertions(+), 138 deletions(-) create mode 100644 plugins/continuity/src/adapters/generic-preflight.mjs create mode 100644 plugins/continuity/src/continuity/engine.mjs diff --git a/plugins/continuity/AGENT_GUIDE.zh-TW.md b/plugins/continuity/AGENT_GUIDE.zh-TW.md index 3e7de8a..2628fb8 100644 --- a/plugins/continuity/AGENT_GUIDE.zh-TW.md +++ b/plugins/continuity/AGENT_GUIDE.zh-TW.md @@ -16,6 +16,7 @@ - 你需要 agent 在同一個 approved plan 內,判斷是否還有 **下一個要派發的 task**。 - 你需要用 **dispatch receipt** 證明「真的有派出下一步」,而不是只在回覆裡口頭說會繼續。 - 你已經在用 `hooks/force-recall/handler.ts`,希望把 approved-plan continuity gate 注入 agent prompt。 +- 你沒有 `force-recall` hook,但有自己的 preflight / planner / orchestrator,想直接呼叫通用 continuity adapter。 ### 可先不裝的情況 @@ -23,7 +24,7 @@ - 你的 workspace 沒有 approved-plan / auto-chain / dispatch 這類流程。 - 你只需要一般單回合聊天,不需要 continuity hard gate。 -- 你沒有 `force-recall` hook,也暫時不打算自己補 glue code。 +- 你沒有 `force-recall` hook,且也不打算在自己的 host / orchestrator 補 generic glue code。 --- @@ -31,11 +32,11 @@ 安裝完成後,應達到這個最小結果: -- `hooks/force-recall/handler.ts` 能載入 `plugins/continuity/src/index.mjs` -- hook 在 preflight 時會呼叫 `runForceRecallContinuityAdapter(...)` +- 你的 host(例如 `force-recall` hook 或自家 preflight)能載入 `plugins/continuity/src/index.mjs` +- host 在 preflight 時會呼叫 `runForceRecallContinuityAdapter(...)`、`runGenericPreflightContinuityAdapter(...)` 或 `runManualContinuityPreflight(...)` 其中之一 - agent prompt 內可看到 `[APPROVED_PLAN_CONTINUITY_GATE] ... [/APPROVED_PLAN_CONTINUITY_GATE]` - 當 approved plan 任務完成,但沒有真實下一步 dispatch receipt 時,gate 會擋下錯誤結案 -- plugin 自身測試與 hook smoke test 可通過 +- plugin 自身測試與對應 host smoke test 可通過 --- @@ -49,7 +50,7 @@ /plugins/continuity ``` -以目前 MVP 的預期 layout: +建議 layout(可依宿主調整): ```text / @@ -86,7 +87,7 @@ - [ ] 你的 workspace 有 `hooks/force-recall/handler.ts` - [ ] 你的 workspace 有 `scripts/test_force_recall_long_task_preflight.mjs` - [ ] 你的 workspace 願意採用 approved-plan continuity hard gate -- [ ] 你接受 MVP 目前只優先支援 `force-recall` adapter +- [ ] 你接受目前 `force-recall` 是最成熟 adapter,但不是唯一宿主 - [ ] 你知道 receipt 只是「最小 contract」,不是完整 workflow engine 可先用以下命令確認: @@ -275,11 +276,11 @@ mkdir -p state/approved-plan-continuity --- -## 7. 如何接 `force-recall` hook +## 7. 如何接 host / hook(`force-recall` 只是其中一種) ## 7.1 你要確認的整合點 -MVP 的主要整合點是: +目前最成熟的整合點仍是: ```text hooks/force-recall/handler.ts @@ -309,9 +310,9 @@ grep -n "APPROVED_PLAN_CONTINUITY_GATE" hooks/force-recall/handler.ts grep -n "plugins\", \"continuity\", \"src\", \"index.mjs" hooks/force-recall/handler.ts ``` -## 7.2 最小接法範例 +## 7.2 最小接法範例(force-recall) -如果你要自己補接,最小概念如下: +如果你走既有 `force-recall` 路徑,最小概念如下: ```js import plugin from './plugins/continuity/src/index.mjs'; @@ -327,7 +328,44 @@ if (out?.block) { } ``` -## 7.3 實作者要檢查的實際條件 +## 7.3 Generic/manual 最小接法範例 + +如果你沒有 `force-recall`,也可直接把自己的 preflight 結果餵給 generic 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, + }, +}); +``` + +若你只是手動做一次 continuity preflight,也可直接呼叫: + +```js +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', +}); +``` + +## 7.4 實作者要檢查的實際條件 - [ ] hook 能動態 import `plugins/continuity/src/index.mjs` - [ ] `runForceRecallContinuityAdapter` 是 function @@ -337,7 +375,7 @@ if (out?.block) { - [ ] `out.block` 非空時有 prepend 到 `bodyForAgent` - [ ] agent prompt 最終可見 continuity gate block -## 7.4 receipt 與 continuity input 的關係 +## 7.5 receipt 與 continuity input 的關係 如果你的流程真的有派發下一步,請不要只在字串裡描述「已派發」,而要產出真實 `dispatchReceipt`。 @@ -360,14 +398,14 @@ if (out?.block) { ### Checklist:第一次把 plugin 接進現有 workspace - [ ] 建立 / 複製 `plugins/continuity` -- [ ] 確認 `src/index.mjs` 可被 hook 載入 +- [ ] 確認 `src/index.mjs` 可被 host 載入 - [ ] 確認 continuity example config 可讀 - [ ] 建立 `state/approved-plan-continuity/` -- [ ] 確認 `hooks/force-recall/handler.ts` 有 continuity adapter 接點 -- [ ] 確認有 prepend continuity block 到 `bodyForAgent` +- [ ] 確認你的 host(例如 `hooks/force-recall/handler.ts` 或自家 preflight)有 continuity adapter 接點 +- [ ] 確認有 prepend continuity block 到 `bodyForAgent` 或對應 prompt/body - [ ] 若 dispatch 真的發生,補上 receipt 寫檔 - [ ] 跑 plugin test -- [ ] 跑 force-recall smoke test +- [ ] 跑對應 host 的 smoke test - [ ] 檢查 injected prompt block 是否存在 ### Checklist:第一次加 receipt 落盤 @@ -553,11 +591,11 @@ find state/approved-plan-continuity -maxdepth 2 -type f | sort ## 13. 目前限制 -這個 MVP 目前有以下限制,安裝前要先接受: +這個 plugin 目前有以下限制,安裝前要先接受: - 它是 **approved-plan continuity hard gate** 的抽離版,不是通用 workflow engine。 -- 主要 adapter 目前只有 `force-recall`。 -- 文件描述的是「接在既有 force-recall preflight 鏈上」的路徑;若你的 workspace 沒這條鏈,仍要自己補 glue code。 +- `force-recall` 是目前最成熟的 adapter,但不是唯一 adapter。 +- 若你的 workspace 沒有 `force-recall`,仍需自己把 host 資料接到 generic/manual path。 - config 目前是模組預設值 + 呼叫端傳入,不是完整 plugin installer / registry 流程。 - receipt store 目前只負責寫檔,不管 retention、cleanup、indexing。 - receipt validator 目前只驗證最小 contract,不會深入驗證每種 `nextDerivedAction` 的完整語意。 diff --git a/plugins/continuity/HOOK.md b/plugins/continuity/HOOK.md index b0646db..85b81f7 100644 --- a/plugins/continuity/HOOK.md +++ b/plugins/continuity/HOOK.md @@ -1,20 +1,37 @@ # HOOK.md -This document reserves the hook adapter contract for the continuity plugin MVP. +This document defines the hook/host adapter boundary for the continuity plugin. -## Target adapter +## Target adapters -Primary MVP integration target: +Current supported adapter paths: - `force-recall` +- `generic-preflight` -## Planned responsibilities +`force-recall` is the current primary OpenClaw integration path, but it is not the only intended host. -- derive continuity input from hook context -- invoke the plugin evaluator +## Adapter responsibilities + +Adapters should: + +- derive or normalize continuity input from host context +- invoke the shared continuity evaluator/engine - return a prompt block / gate result without duplicating continuity rules +- keep host-specific parsing outside the core evaluator + +## Contract boundary + +The host-facing contract lives in the plugin engine and adapter exports: + +- `normalizeContinuityEngineInput()` +- `createContinuityEngineContract()` +- `runForceRecallContinuityAdapter()` +- `runGenericPreflightContinuityAdapter()` +- `runManualContinuityPreflight()` ## Current status -- contract placeholder only -- implementation deferred to later plan tasks +- `force-recall` adapter: implemented and parity-oriented +- `generic-preflight` adapter: implemented for host-agnostic/manual preflight integration +- full installer/registry integration: intentionally out of scope for this package diff --git a/plugins/continuity/README.md b/plugins/continuity/README.md index 2b99d68..ee1fcd5 100644 --- a/plugins/continuity/README.md +++ b/plugins/continuity/README.md @@ -1,12 +1,16 @@ -# Continuity Plugin (MVP) +# Continuity Plugin (MVP → generalized checkpoint) > 中文版:`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 - 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 - prompt block generation for the continuity gate - 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 @@ -23,7 +29,7 @@ Recommended location inside an OpenClaw 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 / @@ -59,10 +65,12 @@ plugins/continuity/ index.mjs adapters/ force-recall.mjs + generic-preflight.mjs config/ defaults.mjs schema.mjs continuity/ + engine.mjs evaluator.mjs receipt-store.mjs receipt-validator.mjs @@ -80,10 +88,12 @@ plugins/continuity/ - `src/config/schema.mjs` - `src/config/defaults.mjs` +- `src/continuity/engine.mjs` - `src/continuity/evaluator.mjs` - `src/continuity/receipt-validator.mjs` - `src/continuity/receipt-store.mjs` - `src/adapters/force-recall.mjs` +- `src/adapters/generic-preflight.mjs` - `src/index.mjs` `src/index.mjs` currently re-exports: @@ -91,11 +101,42 @@ plugins/continuity/ - `defaultConfig` - `cloneDefaultConfig()` - `validateContinuityConfig()` / `normalizeContinuityConfig()` +- `normalizeContinuityEngineInput()` +- `createContinuityEngineResult()` / `createContinuityEngineContract()` - `evaluateContinuity()` / `buildContinuityGateBlock()` - `validateReceipt()` / `isValidReceipt()` - `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()` - `buildApprovedPlanContinuityInput()` - `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 @@ -118,6 +159,10 @@ Start from `examples/openclaw.continuity.example.json`: "forceRecall": { "enabled": true, "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`. -## Hook integration +## Integration path A: `force-recall` -The primary MVP integration point is `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: +The original MVP integration point remains `hooks/force-recall/handler.ts`. ```js 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 @@ -171,24 +243,6 @@ Minimum receipt shape: - `childSessionKey` - `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: ```js @@ -202,51 +256,51 @@ await writeReceipt({ ## Smoke test / verification -At minimum, run: +Required plugin verification: ```bash cd plugins/continuity npm test +node test/continuity.smoke.test.mjs +``` +If your workspace uses `force-recall`, also run: + +```bash cd /path/to/workspace node scripts/test_force_recall_long_task_preflight.mjs 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 1. Copy `plugins/continuity` into your workspace. -2. Ensure `hooks/force-recall/handler.ts` loads `plugins/continuity/src/index.mjs`. -3. Adjust the continuity config as needed, especially: +2. Choose one integration path: + - `force-recall`: load `runForceRecallContinuityAdapter(...)` + - no `force-recall`: call `runGenericPreflightContinuityAdapter(...)` or `runManualContinuityPreflight(...)` +3. Adjust config as needed, especially: - `planMatchers` - `legalTerminalStates` - `receiptDir` - `adapter.forceRecall.injectBlockLabel` + - `adapter.genericPreflight.injectBlockLabel` 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. ## Current limitations -- This is an MVP extraction of the **approved-plan continuity hard gate**, not a general workflow engine. -- The main adapter is `force-recall`; the package is not yet generalized into a multi-hook / multi-event integration layer. -- Config is still passed as module defaults plus caller input; there is not yet a full OpenClaw plugin installer/registration guide. +- This is still centered on the approved-plan continuity hard gate, not a full general workflow engine. +- The generalized engine contract is intentionally minimal and conservative. +- `force-recall` remains the most battle-tested adapter. - 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 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 - 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 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 ## Chinese documentation diff --git a/plugins/continuity/README.zh-TW.md b/plugins/continuity/README.zh-TW.md index 6ffeb5a..0e3026b 100644 --- a/plugins/continuity/README.zh-TW.md +++ b/plugins/continuity/README.zh-TW.md @@ -1,10 +1,14 @@ -# Continuity Plugin(MVP) +# Continuity Plugin(MVP → generalized checkpoint) > 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 - 產生可注入 prompt 的 continuity gate block - 透過 `force-recall` adapter,把 hook 端的 wrapper/planner 結果轉成 continuity input +- 透過 `generic-preflight` adapter,直接吃 host-agnostic continuity input +- 透過 manual preflight runner,在沒有 `force-recall` 的情況下也能直接呼叫 ## 安裝位置 @@ -23,7 +29,7 @@ /plugins/continuity ``` -以目前 MVP 慣例,相關檔案位置如下: +現在可支援兩類整合路徑:原本的 `force-recall` 路徑,以及較通用的 generic path。 ```text / @@ -59,10 +65,12 @@ plugins/continuity/ index.mjs adapters/ force-recall.mjs + generic-preflight.mjs config/ defaults.mjs schema.mjs continuity/ + engine.mjs evaluator.mjs receipt-store.mjs receipt-validator.mjs @@ -80,10 +88,12 @@ plugins/continuity/ - `src/config/schema.mjs` - `src/config/defaults.mjs` +- `src/continuity/engine.mjs` - `src/continuity/evaluator.mjs` - `src/continuity/receipt-validator.mjs` - `src/continuity/receipt-store.mjs` - `src/adapters/force-recall.mjs` +- `src/adapters/generic-preflight.mjs` - `src/index.mjs` `src/index.mjs` 目前會 re-export: @@ -91,11 +101,42 @@ plugins/continuity/ - `defaultConfig` - `cloneDefaultConfig()` - `validateContinuityConfig()` / `normalizeContinuityConfig()` +- `normalizeContinuityEngineInput()` +- `createContinuityEngineResult()` / `createContinuityEngineContract()` - `evaluateContinuity()` / `buildContinuityGateBlock()` - `validateReceipt()` / `isValidReceipt()` - `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()` - `buildApprovedPlanContinuityInput()` - `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 @@ -118,6 +159,10 @@ plugins/continuity/ "forceRecall": { "enabled": true, "injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE" + }, + "genericPreflight": { + "enabled": true, + "injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE" } } } @@ -125,23 +170,9 @@ plugins/continuity/ 預設值定義在 `src/config/defaults.mjs`。 -## Hook 接法 +## 整合路徑 A:`force-recall` -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]` - -最小接法示意: +原本的 MVP 整合點仍是 `hooks/force-recall/handler.ts`。 ```js 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 @@ -171,24 +243,6 @@ if (out?.block) { - `childSessionKey` - `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 落盤,可用: ```js @@ -202,51 +256,51 @@ await writeReceipt({ ## Smoke test / 驗證 -至少執行以下驗證: +plugin 本體必要驗證: ```bash cd plugins/continuity npm test +node test/continuity.smoke.test.mjs +``` +若你的 workspace 有使用 `force-recall`,再補跑: + +```bash cd /path/to/workspace node scripts/test_force_recall_long_task_preflight.mjs node --check hooks/force-recall/handler.ts ``` -若只想先做 plugin 本體最小檢查,也可以: - -```bash -cd plugins/continuity -node test/continuity.smoke.test.mjs -``` - ## 安裝與套用步驟(給其他 OpenClaw 使用者) 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,至少確認: - `planMatchers` - `legalTerminalStates` - `receiptDir` - `adapter.forceRecall.injectBlockLabel` + - `adapter.genericPreflight.injectBlockLabel` 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。 ## 目前限制 -- 目前是 **approved-plan continuity hard gate** 的 MVP 抽離,不是通用 workflow engine。 -- 主要 adapter 只有 `force-recall`,尚未抽象成多 hook / 多事件通用介面。 -- config 目前是模組內預設 + 呼叫端傳入,還沒有完整的 OpenClaw plugin 安裝器/註冊流程文件。 +- 它仍以 approved-plan continuity hard gate 為中心,不是完整通用 workflow engine。 +- generalized engine contract 目前刻意保持最小且保守。 +- `force-recall` 仍是目前最成熟的 adapter。 - receipt store 只負責寫檔,不含 retention、cleanup、indexing。 - receipt validator 目前只檢查最小 contract,不驗證每個 `nextDerivedAction` 子欄位語意。 -- 文件描述的是「依現有 hook 整合」的安裝方式;若未採用 `force-recall` preflight 鏈,仍需自行補 glue code。 ## 備註 - 預設 legal terminal states:`waiting_user`、`blocked`、`pending_verification` - 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 契約定位,不是完整安裝說明 ## 英文文件 diff --git a/plugins/continuity/examples/openclaw.continuity.example.json b/plugins/continuity/examples/openclaw.continuity.example.json index 4ab8db8..40db2fc 100644 --- a/plugins/continuity/examples/openclaw.continuity.example.json +++ b/plugins/continuity/examples/openclaw.continuity.example.json @@ -16,6 +16,10 @@ "forceRecall": { "enabled": true, "injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE" + }, + "genericPreflight": { + "enabled": true, + "injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE" } } } diff --git a/plugins/continuity/package.json b/plugins/continuity/package.json index e9a0e01..b6a0656 100644 --- a/plugins/continuity/package.json +++ b/plugins/continuity/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "type": "module", - "description": "Continuity plugin MVP skeleton for approved-plan dispatch gating.", + "description": "Continuity plugin for approved-plan dispatch gating with a minimal host-agnostic engine and adapters.", "exports": { ".": "./src/index.mjs" }, diff --git a/plugins/continuity/src/adapters/generic-preflight.mjs b/plugins/continuity/src/adapters/generic-preflight.mjs new file mode 100644 index 0000000..837830f --- /dev/null +++ b/plugins/continuity/src/adapters/generic-preflight.mjs @@ -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, +}; diff --git a/plugins/continuity/src/continuity/engine.mjs b/plugins/continuity/src/continuity/engine.mjs new file mode 100644 index 0000000..4ec1167 --- /dev/null +++ b/plugins/continuity/src/continuity/engine.mjs @@ -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, +};