feat: package continuity plugin MVP docs and receipt store
This commit is contained in:
29
hooks/force-recall/HOOK.md
Normal file
29
hooks/force-recall/HOOK.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: force-recall
|
||||||
|
description: "Prepend mandatory RULEBOOK/SOUL recall block before the agent sees inbound messages"
|
||||||
|
homepage: https://docs.openclaw.ai/automation/hooks
|
||||||
|
metadata:
|
||||||
|
{ "openclaw": { "emoji": "🧠", "events": ["message:preprocessed"], "always": true } }
|
||||||
|
---
|
||||||
|
|
||||||
|
# Force Recall Hook (MVP)
|
||||||
|
|
||||||
|
This hook enforces a **recall gate** by prepending a short, high-salience block to every inbound message *after* media/link enrichment and *before* the agent sees it.
|
||||||
|
|
||||||
|
Goal: **Before any technical action/tooling**, the agent must recall key rules from `docs/RULEBOOK.md` + `SOUL.md`.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Listens on `message:preprocessed`
|
||||||
|
- Injects a `RECALL_GATE` prefix into `context.bodyForAgent`
|
||||||
|
- Optional debug: set `OPENCLAW_FORCE_RECALL_DEBUG=1` to append a one-line marker (visible in the agent prompt)
|
||||||
|
|
||||||
|
## Why this MVP
|
||||||
|
|
||||||
|
OpenClaw hooks currently provide reliable interception at the message boundary (`message:preprocessed`). This is the earliest stable point to force rules into the model's working context without patching core.
|
||||||
|
|
||||||
|
## Disable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw hooks disable force-recall
|
||||||
|
```
|
||||||
@@ -1,45 +1,167 @@
|
|||||||
# Continuity Plugin (MVP)
|
# Continuity Plugin (MVP)
|
||||||
|
|
||||||
This package is the skeleton for extracting the current approved-plan continuity hard gate into an installable plugin.
|
> 中文版:`README.zh-TW.md`
|
||||||
|
|
||||||
## MVP status
|
This package extracts the current approved-plan continuity hard gate into a small installable, testable OpenClaw plugin MVP.
|
||||||
|
|
||||||
- Task 2: package skeleton created
|
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.
|
||||||
- Task 3: config schema contract scaffolded
|
|
||||||
- Task 4: config validation tests added
|
|
||||||
- Task 5: minimal config validator implemented
|
|
||||||
- Task 6: receipt validator contract extracted
|
|
||||||
- Plugin evaluator / adapter logic intentionally not implemented yet
|
|
||||||
|
|
||||||
## Layout
|
## What this MVP currently provides
|
||||||
|
|
||||||
|
- continuity config validation
|
||||||
|
- dispatch receipt contract validation
|
||||||
|
- receipt file writing
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Install location
|
||||||
|
|
||||||
|
Recommended location inside an OpenClaw workspace:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
plugins/continuity/
|
<workspace>/plugins/continuity
|
||||||
README.md
|
```
|
||||||
|
|
||||||
|
With the current MVP integration, the related files normally look like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<workspace>/
|
||||||
|
hooks/
|
||||||
|
force-recall/
|
||||||
|
handler.ts
|
||||||
|
HOOK.md
|
||||||
|
plugins/
|
||||||
|
continuity/
|
||||||
README.zh-TW.md
|
README.zh-TW.md
|
||||||
|
README.md
|
||||||
HOOK.md
|
HOOK.md
|
||||||
package.json
|
package.json
|
||||||
examples/
|
examples/
|
||||||
src/
|
src/
|
||||||
test/
|
test/
|
||||||
|
scripts/
|
||||||
|
test_force_recall_long_task_preflight.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Planned public surface
|
## Directory structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
plugins/continuity/
|
||||||
|
README.zh-TW.md
|
||||||
|
README.md
|
||||||
|
HOOK.md
|
||||||
|
package.json
|
||||||
|
examples/
|
||||||
|
approved-plan-receipt.example.json
|
||||||
|
openclaw.continuity.example.json
|
||||||
|
src/
|
||||||
|
index.mjs
|
||||||
|
adapters/
|
||||||
|
force-recall.mjs
|
||||||
|
config/
|
||||||
|
defaults.mjs
|
||||||
|
schema.mjs
|
||||||
|
continuity/
|
||||||
|
evaluator.mjs
|
||||||
|
receipt-store.mjs
|
||||||
|
receipt-validator.mjs
|
||||||
|
types.md
|
||||||
|
test/
|
||||||
|
continuity.config.test.mjs
|
||||||
|
continuity.evaluator.test.mjs
|
||||||
|
continuity.plugin.test.mjs
|
||||||
|
continuity.receipt-store.test.mjs
|
||||||
|
continuity.receipt-validator.test.mjs
|
||||||
|
continuity.smoke.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public surface
|
||||||
|
|
||||||
- `src/config/schema.mjs`
|
- `src/config/schema.mjs`
|
||||||
- `src/config/defaults.mjs`
|
- `src/config/defaults.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/adapters/force-recall.mjs`
|
- `src/adapters/force-recall.mjs`
|
||||||
- `src/index.mjs`
|
- `src/index.mjs`
|
||||||
|
|
||||||
|
`src/index.mjs` currently re-exports:
|
||||||
|
|
||||||
|
- `defaultConfig`
|
||||||
|
- `cloneDefaultConfig()`
|
||||||
|
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
||||||
|
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
||||||
|
- `validateReceipt()` / `isValidReceipt()`
|
||||||
|
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
||||||
|
- `buildApprovedPlanContinuityInput()`
|
||||||
|
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
||||||
|
|
||||||
## Example config
|
## Example config
|
||||||
|
|
||||||
See `examples/openclaw.continuity.example.json`.
|
Start from `examples/openclaw.continuity.example.json`:
|
||||||
|
|
||||||
## Receipt validator contract
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"planMatchers": ["approved-plan"],
|
||||||
|
"legalTerminalStates": [
|
||||||
|
"waiting_user",
|
||||||
|
"blocked",
|
||||||
|
"pending_verification"
|
||||||
|
],
|
||||||
|
"receiptDir": "state/approved-plan-continuity",
|
||||||
|
"requireRealDispatchReceipt": true,
|
||||||
|
"allowReplyClosureWithoutDispatch": false,
|
||||||
|
"debug": false,
|
||||||
|
"adapter": {
|
||||||
|
"forceRecall": {
|
||||||
|
"enabled": true,
|
||||||
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The MVP receipt validator currently defines this minimum shape:
|
Defaults are defined in `src/config/defaults.mjs`.
|
||||||
|
|
||||||
|
## Hook integration
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runForceRecallContinuityAdapter({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
wrapperResult,
|
||||||
|
autoChainPlanResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (out?.block) {
|
||||||
|
context.bodyForAgent = `${out.block}\n${context.bodyForAgent}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want a custom injected block label, override `adapter.forceRecall.injectBlockLabel`.
|
||||||
|
|
||||||
|
## Receipt contract
|
||||||
|
|
||||||
|
Minimum receipt shape:
|
||||||
|
|
||||||
- `planId`
|
- `planId`
|
||||||
- `currentTask`
|
- `currentTask`
|
||||||
@@ -49,15 +171,84 @@ The MVP receipt validator currently defines this minimum shape:
|
|||||||
- `childSessionKey`
|
- `childSessionKey`
|
||||||
- `replyClosureState`
|
- `replyClosureState`
|
||||||
|
|
||||||
API surface:
|
Example from `examples/approved-plan-receipt.example.json`:
|
||||||
|
|
||||||
- `validateReceipt(receipt)`
|
```json
|
||||||
- `isValidReceipt(receipt)`
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
See `src/continuity/types.md` for the extracted contract note.
|
To persist a receipt:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { writeReceipt } from './src/index.mjs';
|
||||||
|
|
||||||
|
await writeReceipt({
|
||||||
|
receiptDir: 'state/approved-plan-continuity',
|
||||||
|
receipt,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Smoke test / verification
|
||||||
|
|
||||||
|
At minimum, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd plugins/continuity
|
||||||
|
npm test
|
||||||
|
|
||||||
|
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:
|
||||||
|
- `planMatchers`
|
||||||
|
- `legalTerminalStates`
|
||||||
|
- `receiptDir`
|
||||||
|
- `adapter.forceRecall.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.
|
||||||
|
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.
|
||||||
|
- 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
|
## Notes
|
||||||
|
|
||||||
- Current terminal states preserved by default: `waiting_user`, `blocked`, `pending_verification`
|
- Default legal terminal states are `waiting_user`, `blocked`, and `pending_verification`
|
||||||
- Default receipt directory target: `state/approved-plan-continuity`
|
- The evaluator preserves current behavior, including `missing_dispatch_receipt` and `missing_auto_next_dispatch`
|
||||||
- `npm test` is reserved for the full plugin test suite defined by the implementation plan
|
- The adapter mirrors the continuity input mapping used by `hooks/force-recall/handler.ts`
|
||||||
|
- `HOOK.md` describes the plugin/hook adapter contract boundary, not the full installation guide
|
||||||
|
|
||||||
|
## Chinese documentation
|
||||||
|
|
||||||
|
See `README.zh-TW.md` for the Traditional Chinese version.
|
||||||
|
|||||||
@@ -1,45 +1,167 @@
|
|||||||
# Continuity Plugin(MVP)
|
# Continuity Plugin(MVP)
|
||||||
|
|
||||||
這個套件目前是把既有 approved-plan continuity hard gate 抽離成可安裝 plugin 的骨架。
|
> English version: `README.md`
|
||||||
|
|
||||||
## MVP 狀態
|
這個套件把目前 approved-plan continuity hard gate 抽離成一個可安裝、可測試、可在 hook 內重用的 OpenClaw plugin MVP。
|
||||||
|
|
||||||
- Task 2:已建立 package skeleton
|
目標不是重新發明規則,而是把既有 continuity 判斷、receipt contract、force-recall adapter 收斂成一個可以被其他 OpenClaw workspace 直接帶走的最小可用包。
|
||||||
- Task 3:已先放入 config schema contract 骨架
|
|
||||||
- Task 4:已補 config validation 測試
|
|
||||||
- Task 5:已實作 minimal config validator
|
|
||||||
- Task 6:已抽出 receipt validator contract
|
|
||||||
- evaluator / adapter 邏輯目前仍未實作
|
|
||||||
|
|
||||||
## 目錄
|
## 目前能做什麼
|
||||||
|
|
||||||
|
- 驗證 continuity config
|
||||||
|
- 驗證 dispatch receipt contract
|
||||||
|
- 寫出 receipt 檔案
|
||||||
|
- 評估 approved-plan continuity gate
|
||||||
|
- 產生可注入 prompt 的 continuity gate block
|
||||||
|
- 透過 `force-recall` adapter,把 hook 端的 wrapper/planner 結果轉成 continuity input
|
||||||
|
|
||||||
|
## 安裝位置
|
||||||
|
|
||||||
|
建議直接放在 OpenClaw workspace 內:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
plugins/continuity/
|
<workspace>/plugins/continuity
|
||||||
README.md
|
```
|
||||||
|
|
||||||
|
以目前 MVP 慣例,相關檔案位置如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<workspace>/
|
||||||
|
hooks/
|
||||||
|
force-recall/
|
||||||
|
handler.ts
|
||||||
|
HOOK.md
|
||||||
|
plugins/
|
||||||
|
continuity/
|
||||||
README.zh-TW.md
|
README.zh-TW.md
|
||||||
|
README.md
|
||||||
HOOK.md
|
HOOK.md
|
||||||
package.json
|
package.json
|
||||||
examples/
|
examples/
|
||||||
src/
|
src/
|
||||||
test/
|
test/
|
||||||
|
scripts/
|
||||||
|
test_force_recall_long_task_preflight.mjs
|
||||||
```
|
```
|
||||||
|
|
||||||
## 預計公開介面
|
## 目錄結構
|
||||||
|
|
||||||
|
```text
|
||||||
|
plugins/continuity/
|
||||||
|
README.zh-TW.md
|
||||||
|
README.md
|
||||||
|
HOOK.md
|
||||||
|
package.json
|
||||||
|
examples/
|
||||||
|
approved-plan-receipt.example.json
|
||||||
|
openclaw.continuity.example.json
|
||||||
|
src/
|
||||||
|
index.mjs
|
||||||
|
adapters/
|
||||||
|
force-recall.mjs
|
||||||
|
config/
|
||||||
|
defaults.mjs
|
||||||
|
schema.mjs
|
||||||
|
continuity/
|
||||||
|
evaluator.mjs
|
||||||
|
receipt-store.mjs
|
||||||
|
receipt-validator.mjs
|
||||||
|
types.md
|
||||||
|
test/
|
||||||
|
continuity.config.test.mjs
|
||||||
|
continuity.evaluator.test.mjs
|
||||||
|
continuity.plugin.test.mjs
|
||||||
|
continuity.receipt-store.test.mjs
|
||||||
|
continuity.receipt-validator.test.mjs
|
||||||
|
continuity.smoke.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 公開介面
|
||||||
|
|
||||||
- `src/config/schema.mjs`
|
- `src/config/schema.mjs`
|
||||||
- `src/config/defaults.mjs`
|
- `src/config/defaults.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/adapters/force-recall.mjs`
|
- `src/adapters/force-recall.mjs`
|
||||||
- `src/index.mjs`
|
- `src/index.mjs`
|
||||||
|
|
||||||
## 範例設定
|
`src/index.mjs` 目前會 re-export:
|
||||||
|
|
||||||
請參考 `examples/openclaw.continuity.example.json`。
|
- `defaultConfig`
|
||||||
|
- `cloneDefaultConfig()`
|
||||||
|
- `validateContinuityConfig()` / `normalizeContinuityConfig()`
|
||||||
|
- `evaluateContinuity()` / `buildContinuityGateBlock()`
|
||||||
|
- `validateReceipt()` / `isValidReceipt()`
|
||||||
|
- `slugifyReceiptSegment()` / `buildReceiptFilename()` / `writeReceipt()`
|
||||||
|
- `buildApprovedPlanContinuityInput()`
|
||||||
|
- `createForceRecallContinuityAdapter()` / `runForceRecallContinuityAdapter()`
|
||||||
|
|
||||||
## Receipt validator contract
|
## Example config
|
||||||
|
|
||||||
目前 MVP receipt validator 最小欄位如下:
|
請從 `examples/openclaw.continuity.example.json` 開始:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"planMatchers": ["approved-plan"],
|
||||||
|
"legalTerminalStates": [
|
||||||
|
"waiting_user",
|
||||||
|
"blocked",
|
||||||
|
"pending_verification"
|
||||||
|
],
|
||||||
|
"receiptDir": "state/approved-plan-continuity",
|
||||||
|
"requireRealDispatchReceipt": true,
|
||||||
|
"allowReplyClosureWithoutDispatch": false,
|
||||||
|
"debug": false,
|
||||||
|
"adapter": {
|
||||||
|
"forceRecall": {
|
||||||
|
"enabled": true,
|
||||||
|
"injectBlockLabel": "APPROVED_PLAN_CONTINUITY_GATE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
預設值定義在 `src/config/defaults.mjs`。
|
||||||
|
|
||||||
|
## Hook 接法
|
||||||
|
|
||||||
|
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
|
||||||
|
import plugin from './plugins/continuity/src/index.mjs';
|
||||||
|
|
||||||
|
const out = plugin.runForceRecallContinuityAdapter({
|
||||||
|
config: plugin.defaultConfig,
|
||||||
|
wrapperResult,
|
||||||
|
autoChainPlanResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (out?.block) {
|
||||||
|
context.bodyForAgent = `${out.block}\n${context.bodyForAgent}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
若要覆蓋 block label,可改 `adapter.forceRecall.injectBlockLabel`。
|
||||||
|
|
||||||
|
## Receipt contract
|
||||||
|
|
||||||
|
最小 receipt shape 如下:
|
||||||
|
|
||||||
- `planId`
|
- `planId`
|
||||||
- `currentTask`
|
- `currentTask`
|
||||||
@@ -49,15 +171,84 @@ plugins/continuity/
|
|||||||
- `childSessionKey`
|
- `childSessionKey`
|
||||||
- `replyClosureState`
|
- `replyClosureState`
|
||||||
|
|
||||||
API 介面:
|
範例可參考 `examples/approved-plan-receipt.example.json`:
|
||||||
|
|
||||||
- `validateReceipt(receipt)`
|
```json
|
||||||
- `isValidReceipt(receipt)`
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
抽出的 contract 說明見 `src/continuity/types.md`。
|
若要把 receipt 落盤,可用:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { writeReceipt } from './src/index.mjs';
|
||||||
|
|
||||||
|
await writeReceipt({
|
||||||
|
receiptDir: 'state/approved-plan-continuity',
|
||||||
|
receipt,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Smoke test / 驗證
|
||||||
|
|
||||||
|
至少執行以下驗證:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd plugins/continuity
|
||||||
|
npm test
|
||||||
|
|
||||||
|
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`。
|
||||||
|
3. 視需要調整 continuity config,至少確認:
|
||||||
|
- `planMatchers`
|
||||||
|
- `legalTerminalStates`
|
||||||
|
- `receiptDir`
|
||||||
|
- `adapter.forceRecall.injectBlockLabel`
|
||||||
|
4. 若你的 dispatch 流程會產生 child run/session,請同步寫出 receipt。
|
||||||
|
5. 跑 smoke test 與 hook preflight 測試。
|
||||||
|
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 安裝器/註冊流程文件。
|
||||||
|
- receipt store 只負責寫檔,不含 retention、cleanup、indexing。
|
||||||
|
- receipt validator 目前只檢查最小 contract,不驗證每個 `nextDerivedAction` 子欄位語意。
|
||||||
|
- 文件描述的是「依現有 hook 整合」的安裝方式;若未採用 `force-recall` preflight 鏈,仍需自行補 glue code。
|
||||||
|
|
||||||
## 備註
|
## 備註
|
||||||
|
|
||||||
- 預設保留目前 terminal states:`waiting_user`、`blocked`、`pending_verification`
|
- 預設 legal terminal states:`waiting_user`、`blocked`、`pending_verification`
|
||||||
- 預設 receipt 目錄:`state/approved-plan-continuity`
|
- evaluator 保留既有行為,包括 `missing_dispatch_receipt` 與 `missing_auto_next_dispatch`
|
||||||
- `npm test` 先保留給後續依計畫補上的完整測試流程
|
- adapter 維持與 `hooks/force-recall/handler.ts` 的 continuity input mapping 一致
|
||||||
|
- `HOOK.md` 說明的是 plugin/hook adapter 契約定位,不是完整安裝說明
|
||||||
|
|
||||||
|
## 英文文件
|
||||||
|
|
||||||
|
英文版請見 `README.md`。
|
||||||
|
|||||||
44
plugins/continuity/src/continuity/receipt-store.mjs
Normal file
44
plugins/continuity/src/continuity/receipt-store.mjs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function isNonEmptyString(value) {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slugifyReceiptSegment(value) {
|
||||||
|
if (!isNonEmptyString(value)) return '';
|
||||||
|
return value
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9._-]+/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildReceiptFilename({ planId, dispatchRunId }) {
|
||||||
|
const planIdSafe = slugifyReceiptSegment(planId);
|
||||||
|
const dispatchRunIdSafe = slugifyReceiptSegment(dispatchRunId);
|
||||||
|
return {
|
||||||
|
planIdSafe,
|
||||||
|
dispatchRunIdSafe,
|
||||||
|
filename: (planIdSafe && dispatchRunIdSafe)
|
||||||
|
? `receipt-${planIdSafe}-${dispatchRunIdSafe}.json`
|
||||||
|
: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeReceipt({ receiptDir, receipt }) {
|
||||||
|
if (!isNonEmptyString(receiptDir)) {
|
||||||
|
throw new Error('receiptDir: expected non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filename, planIdSafe, dispatchRunIdSafe } = buildReceiptFilename(receipt ?? {});
|
||||||
|
if (!filename) {
|
||||||
|
throw new Error(`receipt filename segments invalid: planId=${JSON.stringify(planIdSafe)}, dispatchRunId=${JSON.stringify(dispatchRunIdSafe)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(receiptDir, { recursive: true });
|
||||||
|
const receiptPath = path.join(receiptDir, filename);
|
||||||
|
await fs.writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
||||||
|
return receiptPath;
|
||||||
|
}
|
||||||
38
plugins/continuity/test/continuity.receipt-store.test.mjs
Normal file
38
plugins/continuity/test/continuity.receipt-store.test.mjs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { buildReceiptFilename, slugifyReceiptSegment, writeReceipt } from '../src/continuity/receipt-store.mjs';
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(fn)
|
||||||
|
.then(() => console.log(`ok - ${name}`))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`not ok - ${name}`);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(slugifyReceiptSegment(' Plan ID / 01 '), 'plan-id-01');
|
||||||
|
const built = buildReceiptFilename({ planId: 'Plan ID', dispatchRunId: 'Run 01' });
|
||||||
|
assert.equal(built.filename, 'receipt-plan-id-run-01.json');
|
||||||
|
|
||||||
|
test('writes receipt using canonical filename', async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'continuity-receipt-store-'));
|
||||||
|
const receipt = {
|
||||||
|
planId: 'Plan ID',
|
||||||
|
currentTask: 'task-7',
|
||||||
|
nextDerivedAction: { type: 'message_subagent' },
|
||||||
|
dispatchedAt: '2026-04-24T09:00:00.000Z',
|
||||||
|
dispatchRunId: 'Run 01',
|
||||||
|
childSessionKey: 'child-1',
|
||||||
|
replyClosureState: 'completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
const receiptPath = await writeReceipt({ receiptDir: dir, receipt });
|
||||||
|
assert.match(receiptPath, /receipt-plan-id-run-01\.json$/);
|
||||||
|
assert.equal(fs.existsSync(receiptPath), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('continuity.receipt-store.test.mjs PASS');
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { isValidReceipt, validateReceipt } from '../src/continuity/receipt-validator.mjs';
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(`ok - ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`not ok - ${name}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('accepts full receipt contract', () => {
|
||||||
|
const receipt = {
|
||||||
|
planId: 'plan-1',
|
||||||
|
currentTask: 'task-7',
|
||||||
|
nextDerivedAction: { type: 'message_subagent' },
|
||||||
|
dispatchedAt: '2026-04-24T09:00:00.000Z',
|
||||||
|
dispatchRunId: 'run-1',
|
||||||
|
childSessionKey: 'child-1',
|
||||||
|
replyClosureState: 'completed',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = validateReceipt(receipt);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(isValidReceipt(receipt), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects non-object receipt', () => {
|
||||||
|
const result = validateReceipt(null);
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.errors.join('\n'), /expected object/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects missing required fields', () => {
|
||||||
|
const result = validateReceipt({ planId: 'plan-1' });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.errors.join('\n'), /currentTask/);
|
||||||
|
assert.match(result.errors.join('\n'), /nextDerivedAction/);
|
||||||
|
assert.match(result.errors.join('\n'), /replyClosureState/);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('continuity.receipt-validator.test.mjs PASS');
|
||||||
Reference in New Issue
Block a user