@cleocode/playbooks 2026.10.11 → 2026.10.12
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/dist/state.js +10 -0
- package/package.json +3 -3
- package/src/__tests__/state.test.ts +23 -0
- package/src/state.ts +12 -0
package/dist/state.js
CHANGED
|
@@ -272,6 +272,16 @@ export function updatePlaybookApproval(db, approvalId, patch) {
|
|
|
272
272
|
values.push(patch.reason ?? null);
|
|
273
273
|
}
|
|
274
274
|
if ('status' in patch && patch.status !== undefined) {
|
|
275
|
+
// A decided gate never changes its decision (playbook.approval.once, the
|
|
276
|
+
// same guard transitionGate applies): a peer would refuse the move, so
|
|
277
|
+
// the origin refuses it too (T12944).
|
|
278
|
+
const current = db
|
|
279
|
+
.prepare('SELECT status FROM playbook_approvals WHERE approval_id = ?')
|
|
280
|
+
.get(approvalId);
|
|
281
|
+
if (current && current.status !== 'pending' && current.status !== patch.status) {
|
|
282
|
+
// @sync-invariant playbook.approval.once a decided gate keeps its decision locally, as every peer's merge keeps it
|
|
283
|
+
throw new Error(`E_APPROVAL_ALREADY_DECIDED: gate ${approvalId} is already ${current.status}`);
|
|
284
|
+
}
|
|
275
285
|
sets.push('status = ?');
|
|
276
286
|
values.push(patch.status);
|
|
277
287
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/playbooks",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.12",
|
|
4
4
|
"description": "Playbook DSL + runtime for CLEO — T889 Orchestration Coherence v3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"drizzle-orm": "1.0.0-rc.4",
|
|
21
21
|
"js-yaml": "^4.1.0",
|
|
22
|
-
"@cleocode/
|
|
23
|
-
"@cleocode/
|
|
22
|
+
"@cleocode/contracts": "2026.10.12",
|
|
23
|
+
"@cleocode/core": "2026.10.12"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -218,6 +218,29 @@ describe('W4-8: playbook state CRUD', () => {
|
|
|
218
218
|
expect(updated.reason).toBe('explicit human approval');
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
it('updatePlaybookApproval refuses to change a decided gate, as transitionGate does (T12944)', () => {
|
|
222
|
+
const run = createPlaybookRun(db, { playbookName: 'p', playbookHash: 'h' });
|
|
223
|
+
const approval = createPlaybookApproval(db, {
|
|
224
|
+
runId: run.runId,
|
|
225
|
+
nodeId: 'approval-decided',
|
|
226
|
+
token: 'tok-decided-guard-00000000000000',
|
|
227
|
+
});
|
|
228
|
+
updatePlaybookApproval(db, approval.approvalId, { status: 'approved', approver: 'a' });
|
|
229
|
+
for (const status of ['pending', 'rejected'] as const) {
|
|
230
|
+
expect(() => updatePlaybookApproval(db, approval.approvalId, { status })).toThrow(
|
|
231
|
+
/E_APPROVAL_ALREADY_DECIDED/,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
// The same decision and non-status fields still patch.
|
|
235
|
+
expect(
|
|
236
|
+
updatePlaybookApproval(db, approval.approvalId, { status: 'approved', reason: 'noted' })
|
|
237
|
+
.reason,
|
|
238
|
+
).toBe('noted');
|
|
239
|
+
expect(getPlaybookApprovalByToken(db, 'tok-decided-guard-00000000000000')?.status).toBe(
|
|
240
|
+
'approved',
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
221
244
|
it('listPlaybookApprovals returns rows in requestedAt order with stable secondary key', () => {
|
|
222
245
|
const run = createPlaybookRun(db, {
|
|
223
246
|
playbookName: 'p',
|
package/src/state.ts
CHANGED
|
@@ -414,6 +414,18 @@ export function updatePlaybookApproval(
|
|
|
414
414
|
values.push(patch.reason ?? null);
|
|
415
415
|
}
|
|
416
416
|
if ('status' in patch && patch.status !== undefined) {
|
|
417
|
+
// A decided gate never changes its decision (playbook.approval.once, the
|
|
418
|
+
// same guard transitionGate applies): a peer would refuse the move, so
|
|
419
|
+
// the origin refuses it too (T12944).
|
|
420
|
+
const current = db
|
|
421
|
+
.prepare('SELECT status FROM playbook_approvals WHERE approval_id = ?')
|
|
422
|
+
.get(approvalId) as { status: string } | undefined;
|
|
423
|
+
if (current && current.status !== 'pending' && current.status !== patch.status) {
|
|
424
|
+
// @sync-invariant playbook.approval.once a decided gate keeps its decision locally, as every peer's merge keeps it
|
|
425
|
+
throw new Error(
|
|
426
|
+
`E_APPROVAL_ALREADY_DECIDED: gate ${approvalId} is already ${current.status}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
417
429
|
sets.push('status = ?');
|
|
418
430
|
values.push(patch.status);
|
|
419
431
|
}
|