docs: clarify generic continuity adapter paths
This commit is contained in:
@@ -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 @@
|
||||
<workspace>/plugins/continuity
|
||||
```
|
||||
|
||||
以目前 MVP 的預期 layout:
|
||||
建議 layout(可依宿主調整):
|
||||
|
||||
```text
|
||||
<workspace>/
|
||||
@@ -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` 的完整語意。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
<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
|
||||
<workspace>/
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
<workspace>/plugins/continuity
|
||||
```
|
||||
|
||||
以目前 MVP 慣例,相關檔案位置如下:
|
||||
現在可支援兩類整合路徑:原本的 `force-recall` 路徑,以及較通用的 generic path。
|
||||
|
||||
```text
|
||||
<workspace>/
|
||||
@@ -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 契約定位,不是完整安裝說明
|
||||
|
||||
## 英文文件
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"forceRecall": {
|
||||
"enabled": true,
|
||||
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||
},
|
||||
"genericPreflight": {
|
||||
"enabled": true,
|
||||
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user