@cleocode/playbooks 2026.4.88 → 2026.4.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -0
- package/package.json +3 -3
- package/dist/approval.d.ts +0 -113
- package/dist/approval.js +0 -244
- package/dist/index.d.ts +0 -29
- package/dist/index.js +0 -32
- package/dist/parser.d.ts +0 -60
- package/dist/parser.js +0 -509
- package/dist/policy.d.ts +0 -55
- package/dist/policy.js +0 -85
- package/dist/schema.d.ts +0 -374
- package/dist/schema.js +0 -34
- package/dist/state.d.ts +0 -96
- package/dist/state.js +0 -322
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @cleocode/playbooks
|
|
2
|
+
|
|
3
|
+
**Playbook DSL + runtime for CLEO — T889 Orchestration Coherence v3.**
|
|
4
|
+
|
|
5
|
+
Playbooks are `.cantbook` YAML documents that describe a multi-step agent workflow as a DAG of nodes (agentic, deterministic, and approval gates) connected by typed edges. This package parses them, persists execution state to `tasks.db`, evaluates HITL auto-policies, and manages HMAC-signed approval tokens for human-in-the-loop gates.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
| Wave | Concern | Status |
|
|
10
|
+
|------|---------|--------|
|
|
11
|
+
| W4-6 | Drizzle tables + types | shipped |
|
|
12
|
+
| W4-7 | `.cantbook` YAML parser | shipped |
|
|
13
|
+
| W4-8 | State layer CRUD | shipped |
|
|
14
|
+
| W4-9 | HITL auto-policy evaluator | shipped |
|
|
15
|
+
| W4-10 | State-machine runtime | pending |
|
|
16
|
+
| W4-16 | Approval resume tokens (HMAC) | shipped |
|
|
17
|
+
|
|
18
|
+
The runtime executor (`runtime.ts`) is the next ship — parser + state + policy + approval primitives are in place.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
This is an internal monorepo package. Consumers use it via workspace dependency:
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
// package.json
|
|
26
|
+
{
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@cleocode/playbooks": "workspace:*"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What a `.cantbook` looks like
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
version: "1.0"
|
|
37
|
+
name: release-cut
|
|
38
|
+
description: Cut a CalVer release and publish to npm
|
|
39
|
+
|
|
40
|
+
inputs:
|
|
41
|
+
- name: version
|
|
42
|
+
required: true
|
|
43
|
+
description: Target version (e.g. 2026.4.90)
|
|
44
|
+
|
|
45
|
+
nodes:
|
|
46
|
+
- id: build
|
|
47
|
+
type: deterministic
|
|
48
|
+
command: pnpm
|
|
49
|
+
args: [run, build]
|
|
50
|
+
|
|
51
|
+
- id: test
|
|
52
|
+
type: deterministic
|
|
53
|
+
command: pnpm
|
|
54
|
+
args: [run, test]
|
|
55
|
+
depends: [build]
|
|
56
|
+
|
|
57
|
+
- id: review
|
|
58
|
+
type: approval
|
|
59
|
+
prompt: "Build + tests green. Publish {{ version }} to npm?"
|
|
60
|
+
depends: [test]
|
|
61
|
+
|
|
62
|
+
- id: publish
|
|
63
|
+
type: agentic
|
|
64
|
+
skill: release-publisher
|
|
65
|
+
depends: [review]
|
|
66
|
+
|
|
67
|
+
edges:
|
|
68
|
+
- from: build
|
|
69
|
+
to: test
|
|
70
|
+
contract:
|
|
71
|
+
requires: [dist-exists]
|
|
72
|
+
ensures: [tests-passed]
|
|
73
|
+
- from: test
|
|
74
|
+
to: review
|
|
75
|
+
- from: review
|
|
76
|
+
to: publish
|
|
77
|
+
|
|
78
|
+
error_handlers:
|
|
79
|
+
- on: test-failed
|
|
80
|
+
action: abort
|
|
81
|
+
message: "Tests failed — aborting release."
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Validation rules enforced by the parser:
|
|
85
|
+
- `version` MUST be `"1.0"`.
|
|
86
|
+
- `name` MUST be non-empty.
|
|
87
|
+
- Node `id`s MUST be unique.
|
|
88
|
+
- Every `edges[].from` / `edges[].to` MUST reference a known node id.
|
|
89
|
+
- Nodes + edges MUST form a DAG (no cycles).
|
|
90
|
+
- `agentic` nodes MUST have `skill` OR `agent` (at least one).
|
|
91
|
+
- `deterministic` nodes MUST have `command` + `args`.
|
|
92
|
+
- `approval` nodes MUST have `prompt`.
|
|
93
|
+
- `depends[]` entries MUST be valid node ids.
|
|
94
|
+
- `iteration_cap` / `max_iterations` MUST be in `0..10` (hard limit).
|
|
95
|
+
|
|
96
|
+
## Quick API
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import {
|
|
100
|
+
parsePlaybook,
|
|
101
|
+
createPlaybookRun,
|
|
102
|
+
updatePlaybookRun,
|
|
103
|
+
evaluatePolicy,
|
|
104
|
+
createApprovalGate,
|
|
105
|
+
approveGate,
|
|
106
|
+
rejectGate,
|
|
107
|
+
DEFAULT_POLICY_RULES,
|
|
108
|
+
} from '@cleocode/playbooks';
|
|
109
|
+
|
|
110
|
+
// 1. Parse a .cantbook file
|
|
111
|
+
const { playbook, hash } = parsePlaybook(yamlSource);
|
|
112
|
+
|
|
113
|
+
// 2. Create a persisted run
|
|
114
|
+
const run = createPlaybookRun(tasksDb, {
|
|
115
|
+
runId: 'run_abc123',
|
|
116
|
+
playbookName: playbook.name,
|
|
117
|
+
playbookHash: hash,
|
|
118
|
+
bindings: { version: '2026.4.90' },
|
|
119
|
+
epicId: 'T889',
|
|
120
|
+
sessionId: 'ses_xyz',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 3. Evaluate auto-policy at an approval node
|
|
124
|
+
const { autoPassed, reason } = evaluatePolicy(node, DEFAULT_POLICY_RULES, context);
|
|
125
|
+
|
|
126
|
+
// 4. Create an approval gate (pending) or auto-pass it
|
|
127
|
+
const approval = createApprovalGate(tasksDb, {
|
|
128
|
+
runId: run.runId,
|
|
129
|
+
nodeId: 'review',
|
|
130
|
+
autoPassed,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 5. Resolve the gate (human approves via the resume token)
|
|
134
|
+
approveGate(tasksDb, approval.token, { approver: 'keaton', reason: 'LGTM' });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Database tables
|
|
138
|
+
|
|
139
|
+
Both tables live in `tasks.db`. Migration: `packages/core/migrations/drizzle-tasks/20260417220000_t889-playbook-tables/`.
|
|
140
|
+
|
|
141
|
+
### `playbook_runs`
|
|
142
|
+
|
|
143
|
+
| Column | Type | Notes |
|
|
144
|
+
|--------|------|-------|
|
|
145
|
+
| `run_id` | text PK | caller-supplied run id |
|
|
146
|
+
| `playbook_name` | text | from parsed playbook |
|
|
147
|
+
| `playbook_hash` | text | SHA-256 of source (parser computes) |
|
|
148
|
+
| `current_node` | text | id of the active node, `null` when done |
|
|
149
|
+
| `bindings` | json text | accumulated input + per-node output bindings |
|
|
150
|
+
| `error_context` | json text | populated when `status = 'failed'` |
|
|
151
|
+
| `status` | text | `running \| paused \| failed \| succeeded \| cancelled` |
|
|
152
|
+
| `iteration_counts` | json text | `{ nodeId: count }` — enforced against `iteration_cap` |
|
|
153
|
+
| `epic_id` | text | linked task epic (optional) |
|
|
154
|
+
| `session_id` | text | linked CLEO session (optional) |
|
|
155
|
+
| `started_at` | text | ISO-8601, defaults to `now()` |
|
|
156
|
+
| `completed_at` | text | ISO-8601 when terminal |
|
|
157
|
+
|
|
158
|
+
### `playbook_approvals`
|
|
159
|
+
|
|
160
|
+
| Column | Type | Notes |
|
|
161
|
+
|--------|------|-------|
|
|
162
|
+
| `approval_id` | text PK | |
|
|
163
|
+
| `run_id` | text | FK → `playbook_runs.run_id` |
|
|
164
|
+
| `node_id` | text | approval node id in the playbook |
|
|
165
|
+
| `token` | text UNIQUE | HMAC resume token |
|
|
166
|
+
| `requested_at` | text | ISO-8601 |
|
|
167
|
+
| `approved_at` | text | ISO-8601 on approve/reject |
|
|
168
|
+
| `approver` | text | who acted |
|
|
169
|
+
| `reason` | text | free-form rationale |
|
|
170
|
+
| `status` | text | `pending \| approved \| rejected` |
|
|
171
|
+
| `auto_passed` | int 0/1 | set by the policy evaluator |
|
|
172
|
+
|
|
173
|
+
## Approval tokens (HMAC)
|
|
174
|
+
|
|
175
|
+
Resume tokens are HMAC-SHA-256 signed with the secret resolved from `getPlaybookSecret(env)`:
|
|
176
|
+
|
|
177
|
+
1. `CLEO_PLAYBOOK_SECRET` (preferred)
|
|
178
|
+
2. `CLEO_SECRET` (fallback)
|
|
179
|
+
3. Hard error if neither is set — approval nodes cannot be created without a secret.
|
|
180
|
+
|
|
181
|
+
Tokens encode `{runId, nodeId, issuedAt}` and are verified before any `approveGate` / `rejectGate` call. Replay-resistant: an already-decided approval returns `E_APPROVAL_ALREADY_DECIDED`.
|
|
182
|
+
|
|
183
|
+
## Error codes
|
|
184
|
+
|
|
185
|
+
| Constant | Meaning |
|
|
186
|
+
|----------|---------|
|
|
187
|
+
| `E_APPROVAL_NOT_FOUND` | Token does not match any pending approval row |
|
|
188
|
+
| `E_APPROVAL_ALREADY_DECIDED` | Approval is already `approved` or `rejected` |
|
|
189
|
+
| `PlaybookParseError` | Thrown by `parsePlaybook` with a list of validation issues |
|
|
190
|
+
|
|
191
|
+
## Policy evaluator
|
|
192
|
+
|
|
193
|
+
`evaluatePolicy(node, rules, context)` matches an approval node against an ordered list of `PolicyRule`s. The first rule that matches decides: `autoPassed: true | false` plus a `reason`. Used to let low-risk approval gates auto-pass (e.g. "build dist unchanged from last green run") without bothering a human. `DEFAULT_POLICY_RULES` ships a safe conservative default set.
|
|
194
|
+
|
|
195
|
+
## Related packages
|
|
196
|
+
|
|
197
|
+
- **`@cleocode/contracts`** — `PlaybookDefinition`, `PlaybookRun`, `PlaybookApproval` type contracts consumed here.
|
|
198
|
+
- **`@cleocode/core`** — owns `tasks.db` lifecycle. Playbook migrations are applied by core's drizzle runner.
|
|
199
|
+
- **`@cleocode/cleo`** — CLI surface that will expose `cleo playbook run`, `cleo playbook approve <token>` once the runtime (W4-10) lands.
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pnpm --filter @cleocode/playbooks test
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Smoke, parser, schema, state, policy, and approval test suites cover the shipped surface.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/playbooks",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.92",
|
|
4
4
|
"description": "Playbook DSL + runtime for CLEO — T889 Orchestration Coherence v3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
|
20
20
|
"js-yaml": "^4.1.0",
|
|
21
|
-
"@cleocode/
|
|
22
|
-
"@cleocode/
|
|
21
|
+
"@cleocode/core": "2026.4.92",
|
|
22
|
+
"@cleocode/contracts": "2026.4.92"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/js-yaml": "^4.0.9",
|
package/dist/approval.d.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMAC-SHA256 resume tokens for HITL approval gates.
|
|
3
|
-
*
|
|
4
|
-
* Tokens bind `{runId, nodeId, bindings}` so they cannot be forged or replayed
|
|
5
|
-
* across different executions. The secret defaults to a well-known dev value —
|
|
6
|
-
* production deployments MUST set the `CLEO_PLAYBOOK_SECRET` env var to a
|
|
7
|
-
* high-entropy secret. If the secret rotates, existing tokens are invalidated
|
|
8
|
-
* because the HMAC output changes.
|
|
9
|
-
*
|
|
10
|
-
* Binding canonicalization uses sorted-keys JSON so that `{a:1,b:2}` and
|
|
11
|
-
* `{b:2,a:1}` produce the same token — semantically identical payloads
|
|
12
|
-
* should always yield the same gate identity.
|
|
13
|
-
*
|
|
14
|
-
* @task T889 / T908 / W4-16
|
|
15
|
-
*/
|
|
16
|
-
import type { DatabaseSync } from 'node:sqlite';
|
|
17
|
-
import type { PlaybookApproval } from '@cleocode/contracts';
|
|
18
|
-
/**
|
|
19
|
-
* Error code: approval token not found in the DB.
|
|
20
|
-
* Raised by {@link approveGate} / {@link rejectGate}.
|
|
21
|
-
*/
|
|
22
|
-
export declare const E_APPROVAL_NOT_FOUND: "E_APPROVAL_NOT_FOUND";
|
|
23
|
-
/**
|
|
24
|
-
* Error code: approval has already transitioned out of `pending`.
|
|
25
|
-
* Raised by {@link approveGate} / {@link rejectGate} to prevent re-decisions.
|
|
26
|
-
*/
|
|
27
|
-
export declare const E_APPROVAL_ALREADY_DECIDED: "E_APPROVAL_ALREADY_DECIDED";
|
|
28
|
-
/**
|
|
29
|
-
* Resolve the HMAC secret for resume-token generation.
|
|
30
|
-
*
|
|
31
|
-
* @param env - Override env source (defaults to `process.env`). Used in tests.
|
|
32
|
-
* @returns The configured secret, or a dev-only fallback if unset.
|
|
33
|
-
*/
|
|
34
|
-
export declare function getPlaybookSecret(env?: NodeJS.ProcessEnv): string;
|
|
35
|
-
/**
|
|
36
|
-
* Generate a deterministic 32-char hex HMAC-SHA256 resume token.
|
|
37
|
-
*
|
|
38
|
-
* The token is derived from `HMAC(secret, "runId:nodeId:canonicalBindings")`
|
|
39
|
-
* and truncated to 32 hex chars (128 bits). Determinism is an intentional
|
|
40
|
-
* design choice: the same (runId, nodeId, bindings, secret) tuple always
|
|
41
|
-
* produces the same token, preventing duplicate gates for the same step.
|
|
42
|
-
*
|
|
43
|
-
* @param runId - Playbook run identifier.
|
|
44
|
-
* @param nodeId - Node identifier within the run graph.
|
|
45
|
-
* @param bindings - Current runtime bindings (canonicalized via sorted-keys JSON).
|
|
46
|
-
* @param secret - HMAC secret (defaults to {@link getPlaybookSecret}).
|
|
47
|
-
* @returns A 32-char lowercase hex string.
|
|
48
|
-
*/
|
|
49
|
-
export declare function generateResumeToken(runId: string, nodeId: string, bindings: Record<string, unknown>, secret?: string): string;
|
|
50
|
-
/**
|
|
51
|
-
* Input for {@link createApprovalGate}.
|
|
52
|
-
*/
|
|
53
|
-
export interface CreateApprovalGateInput {
|
|
54
|
-
/** Run identifier (FK to `playbook_runs.run_id`). */
|
|
55
|
-
runId: string;
|
|
56
|
-
/** Node identifier within the run graph. */
|
|
57
|
-
nodeId: string;
|
|
58
|
-
/** Runtime bindings at gate creation time. */
|
|
59
|
-
bindings: Record<string, unknown>;
|
|
60
|
-
/** If true, gate is created pre-approved (policy auto-pass). Default false. */
|
|
61
|
-
autoPassed?: boolean;
|
|
62
|
-
/** Optional approver identity (required if `autoPassed=true` recorded by policy). */
|
|
63
|
-
approver?: string;
|
|
64
|
-
/** Optional human-readable reason (policy name, approval note, etc.). */
|
|
65
|
-
reason?: string;
|
|
66
|
-
/** Override secret for token generation. Defaults to env-resolved secret. */
|
|
67
|
-
secret?: string;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Create an HITL approval gate row in `playbook_approvals`.
|
|
71
|
-
*
|
|
72
|
-
* If `autoPassed` is true, the gate is written with `status='approved'`
|
|
73
|
-
* and `auto_passed=1` — used by the policy engine to short-circuit gates
|
|
74
|
-
* that match auto-pass rules. Otherwise status is `'pending'` and the
|
|
75
|
-
* runtime blocks until {@link approveGate} or {@link rejectGate} is called.
|
|
76
|
-
*
|
|
77
|
-
* @param db - Open `node:sqlite` handle with the T889 migration applied.
|
|
78
|
-
* @param input - Gate parameters.
|
|
79
|
-
* @returns The inserted {@link PlaybookApproval}, round-tripped from the DB.
|
|
80
|
-
*/
|
|
81
|
-
export declare function createApprovalGate(db: DatabaseSync, input: CreateApprovalGateInput): PlaybookApproval;
|
|
82
|
-
/**
|
|
83
|
-
* Transition an approval gate to `approved` state.
|
|
84
|
-
*
|
|
85
|
-
* @param db - Open sqlite handle.
|
|
86
|
-
* @param token - The resume token returned from {@link createApprovalGate}.
|
|
87
|
-
* @param approver - Identity of the approver (agent id, user email, etc.).
|
|
88
|
-
* @param reason - Optional justification note.
|
|
89
|
-
* @returns The updated {@link PlaybookApproval} record.
|
|
90
|
-
* @throws Error with `E_APPROVAL_NOT_FOUND` code if no gate matches the token.
|
|
91
|
-
* @throws Error with `E_APPROVAL_ALREADY_DECIDED` code if the gate is not pending.
|
|
92
|
-
*/
|
|
93
|
-
export declare function approveGate(db: DatabaseSync, token: string, approver: string, reason?: string): PlaybookApproval;
|
|
94
|
-
/**
|
|
95
|
-
* Transition an approval gate to `rejected` state. Same semantics as
|
|
96
|
-
* {@link approveGate} but records a rejection — runtime will halt the run.
|
|
97
|
-
*
|
|
98
|
-
* @param db - Open sqlite handle.
|
|
99
|
-
* @param token - The resume token.
|
|
100
|
-
* @param approver - Identity of the rejector.
|
|
101
|
-
* @param reason - Optional justification.
|
|
102
|
-
* @returns The updated {@link PlaybookApproval} record.
|
|
103
|
-
* @throws Error with `E_APPROVAL_NOT_FOUND` if the token is unknown.
|
|
104
|
-
* @throws Error with `E_APPROVAL_ALREADY_DECIDED` if the gate is not pending.
|
|
105
|
-
*/
|
|
106
|
-
export declare function rejectGate(db: DatabaseSync, token: string, approver: string, reason?: string): PlaybookApproval;
|
|
107
|
-
/**
|
|
108
|
-
* List all gates that are still awaiting a decision, oldest first.
|
|
109
|
-
*
|
|
110
|
-
* @param db - Open sqlite handle.
|
|
111
|
-
* @returns Pending {@link PlaybookApproval} records ordered by `requested_at`.
|
|
112
|
-
*/
|
|
113
|
-
export declare function getPendingApprovals(db: DatabaseSync): PlaybookApproval[];
|
package/dist/approval.js
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMAC-SHA256 resume tokens for HITL approval gates.
|
|
3
|
-
*
|
|
4
|
-
* Tokens bind `{runId, nodeId, bindings}` so they cannot be forged or replayed
|
|
5
|
-
* across different executions. The secret defaults to a well-known dev value —
|
|
6
|
-
* production deployments MUST set the `CLEO_PLAYBOOK_SECRET` env var to a
|
|
7
|
-
* high-entropy secret. If the secret rotates, existing tokens are invalidated
|
|
8
|
-
* because the HMAC output changes.
|
|
9
|
-
*
|
|
10
|
-
* Binding canonicalization uses sorted-keys JSON so that `{a:1,b:2}` and
|
|
11
|
-
* `{b:2,a:1}` produce the same token — semantically identical payloads
|
|
12
|
-
* should always yield the same gate identity.
|
|
13
|
-
*
|
|
14
|
-
* @task T889 / T908 / W4-16
|
|
15
|
-
*/
|
|
16
|
-
import { createHmac, randomUUID } from 'node:crypto';
|
|
17
|
-
/**
|
|
18
|
-
* Dev-only fallback secret. Surfaced through {@link getPlaybookSecret} so
|
|
19
|
-
* production code paths can override via `CLEO_PLAYBOOK_SECRET`.
|
|
20
|
-
*/
|
|
21
|
-
const DEFAULT_SECRET = 'cleo-playbook-dev-secret-do-not-use-in-production';
|
|
22
|
-
/**
|
|
23
|
-
* Token length (hex chars). 32 hex chars = 128 bits of HMAC output — enough
|
|
24
|
-
* for collision resistance while keeping tokens URL-safe and log-friendly.
|
|
25
|
-
*/
|
|
26
|
-
const TOKEN_LENGTH = 32;
|
|
27
|
-
/**
|
|
28
|
-
* Error code: approval token not found in the DB.
|
|
29
|
-
* Raised by {@link approveGate} / {@link rejectGate}.
|
|
30
|
-
*/
|
|
31
|
-
export const E_APPROVAL_NOT_FOUND = 'E_APPROVAL_NOT_FOUND';
|
|
32
|
-
/**
|
|
33
|
-
* Error code: approval has already transitioned out of `pending`.
|
|
34
|
-
* Raised by {@link approveGate} / {@link rejectGate} to prevent re-decisions.
|
|
35
|
-
*/
|
|
36
|
-
export const E_APPROVAL_ALREADY_DECIDED = 'E_APPROVAL_ALREADY_DECIDED';
|
|
37
|
-
/**
|
|
38
|
-
* Resolve the HMAC secret for resume-token generation.
|
|
39
|
-
*
|
|
40
|
-
* @param env - Override env source (defaults to `process.env`). Used in tests.
|
|
41
|
-
* @returns The configured secret, or a dev-only fallback if unset.
|
|
42
|
-
*/
|
|
43
|
-
export function getPlaybookSecret(env = process.env) {
|
|
44
|
-
return env['CLEO_PLAYBOOK_SECRET'] ?? DEFAULT_SECRET;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Generate a deterministic 32-char hex HMAC-SHA256 resume token.
|
|
48
|
-
*
|
|
49
|
-
* The token is derived from `HMAC(secret, "runId:nodeId:canonicalBindings")`
|
|
50
|
-
* and truncated to 32 hex chars (128 bits). Determinism is an intentional
|
|
51
|
-
* design choice: the same (runId, nodeId, bindings, secret) tuple always
|
|
52
|
-
* produces the same token, preventing duplicate gates for the same step.
|
|
53
|
-
*
|
|
54
|
-
* @param runId - Playbook run identifier.
|
|
55
|
-
* @param nodeId - Node identifier within the run graph.
|
|
56
|
-
* @param bindings - Current runtime bindings (canonicalized via sorted-keys JSON).
|
|
57
|
-
* @param secret - HMAC secret (defaults to {@link getPlaybookSecret}).
|
|
58
|
-
* @returns A 32-char lowercase hex string.
|
|
59
|
-
*/
|
|
60
|
-
export function generateResumeToken(runId, nodeId, bindings, secret = getPlaybookSecret()) {
|
|
61
|
-
// Canonicalize bindings via sorted-keys JSON for determinism.
|
|
62
|
-
const canonical = JSON.stringify(bindings, Object.keys(bindings).sort());
|
|
63
|
-
const payload = `${runId}:${nodeId}:${canonical}`;
|
|
64
|
-
return createHmac('sha256', secret).update(payload).digest('hex').slice(0, TOKEN_LENGTH);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Narrow a raw status string to {@link PlaybookApprovalStatus}, guarding
|
|
68
|
-
* against unexpected DB values that would otherwise poison downstream types.
|
|
69
|
-
*
|
|
70
|
-
* @internal
|
|
71
|
-
*/
|
|
72
|
-
function narrowStatus(s) {
|
|
73
|
-
if (s === 'pending' || s === 'approved' || s === 'rejected')
|
|
74
|
-
return s;
|
|
75
|
-
throw new Error(`invariant: unknown playbook_approvals.status '${s}'`);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Read a required string column from a raw sqlite row.
|
|
79
|
-
*
|
|
80
|
-
* @internal
|
|
81
|
-
*/
|
|
82
|
-
function readString(row, key) {
|
|
83
|
-
const v = row[key];
|
|
84
|
-
if (typeof v !== 'string') {
|
|
85
|
-
throw new Error(`invariant: expected string for column ${key}, got ${typeof v}`);
|
|
86
|
-
}
|
|
87
|
-
return v;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Read a required integer column from a raw sqlite row.
|
|
91
|
-
*
|
|
92
|
-
* @internal
|
|
93
|
-
*/
|
|
94
|
-
function readInt(row, key) {
|
|
95
|
-
const v = row[key];
|
|
96
|
-
if (typeof v === 'number')
|
|
97
|
-
return v;
|
|
98
|
-
if (typeof v === 'bigint')
|
|
99
|
-
return Number(v);
|
|
100
|
-
throw new Error(`invariant: expected integer for column ${key}, got ${typeof v}`);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Read an optional string column. Returns `undefined` for both `null`
|
|
104
|
-
* (SQL NULL) and missing keys.
|
|
105
|
-
*
|
|
106
|
-
* @internal
|
|
107
|
-
*/
|
|
108
|
-
function readOptionalString(row, key) {
|
|
109
|
-
const v = row[key];
|
|
110
|
-
if (v === null || v === undefined)
|
|
111
|
-
return undefined;
|
|
112
|
-
if (typeof v !== 'string') {
|
|
113
|
-
throw new Error(`invariant: expected string|null for column ${key}, got ${typeof v}`);
|
|
114
|
-
}
|
|
115
|
-
return v;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Map a raw `playbook_approvals` row to the camelCase {@link PlaybookApproval}
|
|
119
|
-
* contract shape. Validates types, converts the `auto_passed` 0/1 integer to
|
|
120
|
-
* a boolean, and strips nullable fields rather than emitting `null`.
|
|
121
|
-
*
|
|
122
|
-
* @internal
|
|
123
|
-
*/
|
|
124
|
-
function rowToApproval(row) {
|
|
125
|
-
const approval = {
|
|
126
|
-
approvalId: readString(row, 'approval_id'),
|
|
127
|
-
runId: readString(row, 'run_id'),
|
|
128
|
-
nodeId: readString(row, 'node_id'),
|
|
129
|
-
token: readString(row, 'token'),
|
|
130
|
-
requestedAt: readString(row, 'requested_at'),
|
|
131
|
-
status: narrowStatus(readString(row, 'status')),
|
|
132
|
-
autoPassed: readInt(row, 'auto_passed') === 1,
|
|
133
|
-
};
|
|
134
|
-
const approvedAt = readOptionalString(row, 'approved_at');
|
|
135
|
-
const approver = readOptionalString(row, 'approver');
|
|
136
|
-
const reason = readOptionalString(row, 'reason');
|
|
137
|
-
if (approvedAt !== undefined)
|
|
138
|
-
approval.approvedAt = approvedAt;
|
|
139
|
-
if (approver !== undefined)
|
|
140
|
-
approval.approver = approver;
|
|
141
|
-
if (reason !== undefined)
|
|
142
|
-
approval.reason = reason;
|
|
143
|
-
return approval;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Create an HITL approval gate row in `playbook_approvals`.
|
|
147
|
-
*
|
|
148
|
-
* If `autoPassed` is true, the gate is written with `status='approved'`
|
|
149
|
-
* and `auto_passed=1` — used by the policy engine to short-circuit gates
|
|
150
|
-
* that match auto-pass rules. Otherwise status is `'pending'` and the
|
|
151
|
-
* runtime blocks until {@link approveGate} or {@link rejectGate} is called.
|
|
152
|
-
*
|
|
153
|
-
* @param db - Open `node:sqlite` handle with the T889 migration applied.
|
|
154
|
-
* @param input - Gate parameters.
|
|
155
|
-
* @returns The inserted {@link PlaybookApproval}, round-tripped from the DB.
|
|
156
|
-
*/
|
|
157
|
-
export function createApprovalGate(db, input) {
|
|
158
|
-
const token = generateResumeToken(input.runId, input.nodeId, input.bindings, input.secret);
|
|
159
|
-
const approvalId = randomUUID();
|
|
160
|
-
const autoPassed = input.autoPassed ?? false;
|
|
161
|
-
const status = autoPassed ? 'approved' : 'pending';
|
|
162
|
-
const stmt = db.prepare(`
|
|
163
|
-
INSERT INTO playbook_approvals
|
|
164
|
-
(approval_id, run_id, node_id, token, status, auto_passed, approver, reason)
|
|
165
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
166
|
-
`);
|
|
167
|
-
stmt.run(approvalId, input.runId, input.nodeId, token, status, autoPassed ? 1 : 0, input.approver ?? null, input.reason ?? null);
|
|
168
|
-
const row = db
|
|
169
|
-
.prepare('SELECT * FROM playbook_approvals WHERE approval_id = ?')
|
|
170
|
-
.get(approvalId);
|
|
171
|
-
if (row === undefined) {
|
|
172
|
-
throw new Error(`${E_APPROVAL_NOT_FOUND}: insert did not round-trip (approval_id=${approvalId})`);
|
|
173
|
-
}
|
|
174
|
-
return rowToApproval(row);
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Transition an approval gate to `approved` state.
|
|
178
|
-
*
|
|
179
|
-
* @param db - Open sqlite handle.
|
|
180
|
-
* @param token - The resume token returned from {@link createApprovalGate}.
|
|
181
|
-
* @param approver - Identity of the approver (agent id, user email, etc.).
|
|
182
|
-
* @param reason - Optional justification note.
|
|
183
|
-
* @returns The updated {@link PlaybookApproval} record.
|
|
184
|
-
* @throws Error with `E_APPROVAL_NOT_FOUND` code if no gate matches the token.
|
|
185
|
-
* @throws Error with `E_APPROVAL_ALREADY_DECIDED` code if the gate is not pending.
|
|
186
|
-
*/
|
|
187
|
-
export function approveGate(db, token, approver, reason) {
|
|
188
|
-
return transitionGate(db, token, 'approved', approver, reason);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Transition an approval gate to `rejected` state. Same semantics as
|
|
192
|
-
* {@link approveGate} but records a rejection — runtime will halt the run.
|
|
193
|
-
*
|
|
194
|
-
* @param db - Open sqlite handle.
|
|
195
|
-
* @param token - The resume token.
|
|
196
|
-
* @param approver - Identity of the rejector.
|
|
197
|
-
* @param reason - Optional justification.
|
|
198
|
-
* @returns The updated {@link PlaybookApproval} record.
|
|
199
|
-
* @throws Error with `E_APPROVAL_NOT_FOUND` if the token is unknown.
|
|
200
|
-
* @throws Error with `E_APPROVAL_ALREADY_DECIDED` if the gate is not pending.
|
|
201
|
-
*/
|
|
202
|
-
export function rejectGate(db, token, approver, reason) {
|
|
203
|
-
return transitionGate(db, token, 'rejected', approver, reason);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Internal shared transition logic for approve/reject. Performs row lookup,
|
|
207
|
-
* state validation, update, and round-trip fetch in a single atomic flow.
|
|
208
|
-
*
|
|
209
|
-
* @internal
|
|
210
|
-
*/
|
|
211
|
-
function transitionGate(db, token, next, approver, reason) {
|
|
212
|
-
const existing = db.prepare('SELECT * FROM playbook_approvals WHERE token = ?').get(token);
|
|
213
|
-
if (existing === undefined) {
|
|
214
|
-
throw new Error(`${E_APPROVAL_NOT_FOUND}: no approval gate for token`);
|
|
215
|
-
}
|
|
216
|
-
const existingStatus = narrowStatus(readString(existing, 'status'));
|
|
217
|
-
if (existingStatus !== 'pending') {
|
|
218
|
-
const approvalId = readString(existing, 'approval_id');
|
|
219
|
-
throw new Error(`${E_APPROVAL_ALREADY_DECIDED}: gate ${approvalId} is already ${existingStatus}`);
|
|
220
|
-
}
|
|
221
|
-
db.prepare(`UPDATE playbook_approvals
|
|
222
|
-
SET status = ?, approved_at = datetime('now'), approver = ?, reason = ?
|
|
223
|
-
WHERE token = ?`).run(next, approver, reason ?? null, token);
|
|
224
|
-
const row = db.prepare('SELECT * FROM playbook_approvals WHERE token = ?').get(token);
|
|
225
|
-
if (row === undefined) {
|
|
226
|
-
// Unreachable: UPDATE just succeeded on this token.
|
|
227
|
-
throw new Error(`${E_APPROVAL_NOT_FOUND}: row vanished after update (token=${token})`);
|
|
228
|
-
}
|
|
229
|
-
return rowToApproval(row);
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* List all gates that are still awaiting a decision, oldest first.
|
|
233
|
-
*
|
|
234
|
-
* @param db - Open sqlite handle.
|
|
235
|
-
* @returns Pending {@link PlaybookApproval} records ordered by `requested_at`.
|
|
236
|
-
*/
|
|
237
|
-
export function getPendingApprovals(db) {
|
|
238
|
-
const rows = db
|
|
239
|
-
.prepare(`SELECT * FROM playbook_approvals
|
|
240
|
-
WHERE status = 'pending'
|
|
241
|
-
ORDER BY requested_at ASC, approval_id ASC`)
|
|
242
|
-
.all();
|
|
243
|
-
return rows.map(rowToApproval);
|
|
244
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cleocode/playbooks — Playbook DSL + runtime for T889 Orchestration Coherence v3.
|
|
3
|
-
*
|
|
4
|
-
* This package is scaffolded in Wave 0. Subsequent waves will populate:
|
|
5
|
-
* - `schema.ts` (W4-6) — types + Drizzle table defs
|
|
6
|
-
* - `parser.ts` (W4-7) — .cantbook YAML parser
|
|
7
|
-
* - `state.ts` (W4-8) — DB CRUD for playbook_runs + playbook_approvals
|
|
8
|
-
* - `policy.ts` (W4-9) — HITL auto-policy rules
|
|
9
|
-
* - `runtime.ts` (W4-10) — state machine executor
|
|
10
|
-
* - `approval.ts` (W4-16) — resume token generation + approval ops
|
|
11
|
-
* - `skill-composer.ts` (W4-2..5) — three-source skill bundle composer
|
|
12
|
-
*
|
|
13
|
-
* @remarks
|
|
14
|
-
* Only the {@link PLAYBOOKS_PACKAGE_VERSION} constant is exported from the
|
|
15
|
-
* Wave 0 scaffold. Each follow-up wave adds a named barrel export here.
|
|
16
|
-
*
|
|
17
|
-
* @task T889 Orchestration Coherence v3 — Wave 0 scaffold
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Package version string matching the monorepo's CalVer cadence.
|
|
21
|
-
*
|
|
22
|
-
* Consumers can use this to assert dependency alignment at runtime
|
|
23
|
-
* (e.g. ensuring the `@cleocode/playbooks` runtime matches CLEO core).
|
|
24
|
-
*/
|
|
25
|
-
export declare const PLAYBOOKS_PACKAGE_VERSION: string;
|
|
26
|
-
export { approveGate, type CreateApprovalGateInput, createApprovalGate, E_APPROVAL_ALREADY_DECIDED, E_APPROVAL_NOT_FOUND, generateResumeToken, getPendingApprovals, getPlaybookSecret, rejectGate, } from './approval.js';
|
|
27
|
-
export { type ParsePlaybookResult, PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
28
|
-
export { DEFAULT_POLICY_RULES, type EvaluatePolicyResult, evaluatePolicy, type PolicyRule, } from './policy.js';
|
|
29
|
-
export { type CreatePlaybookApprovalInput, type CreatePlaybookRunInput, createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, type ListPlaybookRunsOptions, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|
package/dist/index.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cleocode/playbooks — Playbook DSL + runtime for T889 Orchestration Coherence v3.
|
|
3
|
-
*
|
|
4
|
-
* This package is scaffolded in Wave 0. Subsequent waves will populate:
|
|
5
|
-
* - `schema.ts` (W4-6) — types + Drizzle table defs
|
|
6
|
-
* - `parser.ts` (W4-7) — .cantbook YAML parser
|
|
7
|
-
* - `state.ts` (W4-8) — DB CRUD for playbook_runs + playbook_approvals
|
|
8
|
-
* - `policy.ts` (W4-9) — HITL auto-policy rules
|
|
9
|
-
* - `runtime.ts` (W4-10) — state machine executor
|
|
10
|
-
* - `approval.ts` (W4-16) — resume token generation + approval ops
|
|
11
|
-
* - `skill-composer.ts` (W4-2..5) — three-source skill bundle composer
|
|
12
|
-
*
|
|
13
|
-
* @remarks
|
|
14
|
-
* Only the {@link PLAYBOOKS_PACKAGE_VERSION} constant is exported from the
|
|
15
|
-
* Wave 0 scaffold. Each follow-up wave adds a named barrel export here.
|
|
16
|
-
*
|
|
17
|
-
* @task T889 Orchestration Coherence v3 — Wave 0 scaffold
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Package version string matching the monorepo's CalVer cadence.
|
|
21
|
-
*
|
|
22
|
-
* Consumers can use this to assert dependency alignment at runtime
|
|
23
|
-
* (e.g. ensuring the `@cleocode/playbooks` runtime matches CLEO core).
|
|
24
|
-
*/
|
|
25
|
-
export const PLAYBOOKS_PACKAGE_VERSION = '2026.4.85';
|
|
26
|
-
export { approveGate, createApprovalGate, E_APPROVAL_ALREADY_DECIDED, E_APPROVAL_NOT_FOUND, generateResumeToken, getPendingApprovals, getPlaybookSecret, rejectGate, } from './approval.js';
|
|
27
|
-
// W4-7: .cantbook YAML parser → PlaybookDefinition
|
|
28
|
-
export { PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
29
|
-
// W4-9: HITL auto-policy evaluator
|
|
30
|
-
export { DEFAULT_POLICY_RULES, evaluatePolicy, } from './policy.js';
|
|
31
|
-
// W4-8: state layer CRUD for playbook_runs + playbook_approvals
|
|
32
|
-
export { createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|