@unson/brainbase-mcp 0.44.0 → 0.44.1

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.
Files changed (72) hide show
  1. package/contracts/canonical-task/operation-schema.sql +58 -0
  2. package/contracts/canonical-task/search-indexes.sql +10 -0
  3. package/contracts/canonical-task/store-schema.sql +77 -0
  4. package/dist/automation-run.d.ts +213 -0
  5. package/dist/automation-run.js +479 -0
  6. package/dist/canonical-task-operations.d.ts +205 -0
  7. package/dist/canonical-task-operations.js +700 -0
  8. package/dist/canonical-task-postgres.d.ts +139 -0
  9. package/dist/canonical-task-postgres.js +554 -0
  10. package/dist/canonical-task-service.d.ts +13 -0
  11. package/dist/canonical-task-service.js +55 -3
  12. package/dist/canonical-task-store-config.d.ts +31 -0
  13. package/dist/canonical-task-store-config.js +53 -0
  14. package/dist/config-parser.d.ts +298 -0
  15. package/dist/config-parser.js +593 -0
  16. package/dist/connected-onboarding.d.ts +57 -0
  17. package/dist/connected-onboarding.js +37 -0
  18. package/dist/durable-wait-http.d.ts +3 -1
  19. package/dist/durable-wait-http.js +73 -6
  20. package/dist/durable-waits.d.ts +93 -3
  21. package/dist/durable-waits.js +320 -8
  22. package/dist/honcho.d.ts +25 -0
  23. package/dist/honcho.js +81 -0
  24. package/dist/judgment-event-adapter.d.ts +72 -0
  25. package/dist/judgment-event-adapter.js +114 -0
  26. package/dist/judgment-event-outbox.d.ts +182 -0
  27. package/dist/judgment-event-outbox.js +568 -0
  28. package/dist/judgment-execution-outcome.d.ts +46 -0
  29. package/dist/judgment-execution-outcome.js +149 -0
  30. package/dist/judgment-host.d.ts +18 -7
  31. package/dist/judgment-host.js +82 -34
  32. package/dist/judgment-lifecycle-core.d.ts +198 -0
  33. package/dist/judgment-lifecycle-core.js +464 -0
  34. package/dist/judgment-node-evidence.d.ts +57 -0
  35. package/dist/judgment-node-evidence.js +297 -0
  36. package/dist/judgment-runtime-resolution.d.ts +264 -0
  37. package/dist/judgment-runtime-resolution.js +1248 -0
  38. package/dist/judgment-runtime-resolution.public.d.ts +255 -0
  39. package/dist/knowledge-event.d.ts +28 -0
  40. package/dist/knowledge-event.js +114 -0
  41. package/dist/mcp-runtime.d.ts +75 -0
  42. package/dist/mcp-runtime.js +188 -0
  43. package/dist/meeting-automation-runtime.d.ts +102 -0
  44. package/dist/meeting-automation-runtime.js +249 -0
  45. package/dist/meeting-automation.d.ts +155 -0
  46. package/dist/meeting-automation.js +341 -0
  47. package/dist/meeting-minutes-context-receipt.d.ts +91 -0
  48. package/dist/meeting-minutes-context-receipt.js +280 -0
  49. package/dist/meeting-source-sync.d.ts +89 -0
  50. package/dist/meeting-source-sync.js +230 -0
  51. package/dist/mesh.d.ts +58 -0
  52. package/dist/mesh.js +134 -0
  53. package/dist/onboarding-runtime.d.ts +47 -0
  54. package/dist/onboarding-runtime.js +4719 -0
  55. package/dist/routine-cycle.d.ts +51 -0
  56. package/dist/routine-cycle.js +153 -0
  57. package/dist/routine-liveness.d.ts +49 -0
  58. package/dist/routine-liveness.js +227 -0
  59. package/dist/routine-runtime.d.ts +101 -0
  60. package/dist/routine-runtime.js +237 -0
  61. package/dist/routines.d.ts +3 -0
  62. package/dist/routines.js +3 -0
  63. package/dist/run-receipt.d.ts +40 -0
  64. package/dist/run-receipt.js +635 -0
  65. package/dist/runtime-host.d.ts +64 -0
  66. package/dist/runtime-host.js +70 -0
  67. package/dist/world-work.d.ts +16 -0
  68. package/package.json +115 -3
  69. package/ui/objective-editor.js +47 -11
  70. package/ui/world/world-district.js +18 -3
  71. package/ui/world/world-placement.js +13 -0
  72. package/ui/world/world-work-rail.js +7 -1
@@ -0,0 +1,700 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ export function canonicalTaskOperationError(code, message, status = 503, details = {}) {
4
+ const error = new Error(message);
5
+ error.name = 'CanonicalTaskOperationError';
6
+ error.code = code;
7
+ error.status = status;
8
+ error.details = details;
9
+ return error;
10
+ }
11
+ function writerDisabled() {
12
+ return canonicalTaskOperationError('canonical_task_writer_disabled', 'Canonical Task writer is disabled for this process');
13
+ }
14
+ function isUnboundProcessIdentity(value) {
15
+ return !value || typeof value !== 'object' || Object.keys(value).length === 0;
16
+ }
17
+ function sameProcessIdentity(left, right) {
18
+ const normalize = (value) => Object.fromEntries(Object.entries(value || {}).sort(([a], [b]) => a.localeCompare(b)));
19
+ return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
20
+ }
21
+ function authorizationSnapshotRecord(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value))
23
+ return null;
24
+ const record = value;
25
+ return record.task && typeof record.task === 'object' && !Array.isArray(record.task)
26
+ ? record
27
+ : null;
28
+ }
29
+ const READINESS_COLUMNS = `ready, writer_token, manifest_hash, schema_version, source_head,
30
+ evidence_hash, evidence_path, reason, updated_at`;
31
+ /**
32
+ * Single-writer coordination for Canonical Task mutations
33
+ * (`contracts/canonical-task/operation-schema.sql`). One process owns the
34
+ * writer token; every mutation is recorded by scope and operation key so a
35
+ * retry with the same input replays and a different input conflicts.
36
+ */
37
+ export class CanonicalTaskOperationRepository {
38
+ /**
39
+ * A PostgreSQL coordinator is a durable mutation boundary. Callers must
40
+ * bind it to the release readiness gate before exposing mutations.
41
+ */
42
+ requiresReadiness = true;
43
+ pool;
44
+ writerToken;
45
+ processIdentity;
46
+ writerEnabled;
47
+ operationWaitTimeoutMs;
48
+ operationPollIntervalMs;
49
+ writerClaimed = false;
50
+ activeOperations = 0;
51
+ constructor({ pool = null, writerToken = null, processIdentity = null, writerEnabled = false, operationWaitTimeoutMs = 5000, operationPollIntervalMs = 10, } = {}) {
52
+ this.pool = pool;
53
+ this.writerToken = writerToken;
54
+ this.processIdentity = processIdentity;
55
+ this.writerEnabled = writerEnabled === true;
56
+ this.operationWaitTimeoutMs = operationWaitTimeoutMs;
57
+ this.operationPollIntervalMs = operationPollIntervalMs;
58
+ }
59
+ identityJson() {
60
+ return JSON.stringify(this.processIdentity || {});
61
+ }
62
+ async connect() {
63
+ if (typeof this.pool?.connect !== 'function') {
64
+ throw canonicalTaskOperationError('canonical_task_coordination_unavailable', 'Canonical Task coordination database is not configured');
65
+ }
66
+ return this.pool.connect();
67
+ }
68
+ async waitForCompletedOperation({ scope, operationKey, fingerprint }) {
69
+ const deadline = Date.now() + this.operationWaitTimeoutMs;
70
+ while (Date.now() <= deadline) {
71
+ await this.assertWriter();
72
+ const existing = await this.pool.query(`SELECT fingerprint, state, result_json, error_json, writer_token
73
+ FROM canonical_task_operations
74
+ WHERE scope = $1 AND operation_key = $2`, [scope, operationKey]);
75
+ const row = existing.rows[0];
76
+ if (!row || row.fingerprint !== fingerprint) {
77
+ throw canonicalTaskOperationError('idempotency_conflict', 'Idempotency key was reused with different input', 409);
78
+ }
79
+ if (row.state === 'completed')
80
+ return row.result_json;
81
+ if (row.state === 'failed') {
82
+ throw canonicalTaskOperationError('canonical_task_operation_failed', 'Concurrent Canonical Task operation failed', 503, {
83
+ cause: row.error_json || null,
84
+ });
85
+ }
86
+ await new Promise((resolve) => setTimeout(resolve, this.operationPollIntervalMs));
87
+ }
88
+ throw canonicalTaskOperationError('canonical_task_operation_timeout', 'Timed out waiting for the concurrent Canonical Task operation', 503);
89
+ }
90
+ assertConfigured() {
91
+ if (!this.pool || !this.writerToken) {
92
+ throw canonicalTaskOperationError('canonical_task_coordination_unavailable', 'Canonical Task coordination database is not configured');
93
+ }
94
+ }
95
+ assertWriterEnabled() {
96
+ if (this.writerEnabled !== true)
97
+ throw writerDisabled();
98
+ }
99
+ async claimWriter({ sourceHead } = {}) {
100
+ this.assertWriterEnabled();
101
+ this.assertConfigured();
102
+ const client = await this.connect();
103
+ try {
104
+ await client.query('BEGIN');
105
+ await client.query(`INSERT INTO canonical_task_writer (
106
+ singleton_id, writer_token, process_identity, source_head, claimed_at, updated_at
107
+ ) VALUES (TRUE, $1, $2::jsonb, $3, NOW(), NOW())
108
+ ON CONFLICT (singleton_id) DO NOTHING`, [this.writerToken, this.identityJson(), sourceHead || null]);
109
+ const current = await client.query(`SELECT writer_token, process_identity, source_head
110
+ FROM canonical_task_writer
111
+ WHERE singleton_id = TRUE
112
+ FOR UPDATE`);
113
+ const row = current.rows[0];
114
+ const canBindProcess = row
115
+ && row.writer_token === this.writerToken
116
+ && (isUnboundProcessIdentity(row.process_identity) || sameProcessIdentity(row.process_identity, this.processIdentity));
117
+ if (!canBindProcess) {
118
+ throw canonicalTaskOperationError('canonical_task_writer_unavailable', 'Canonical Task writer is owned by another process', 503, {
119
+ active_writer: row?.process_identity || null,
120
+ source_head: row?.source_head || null,
121
+ });
122
+ }
123
+ const claimed = await client.query(`UPDATE canonical_task_writer
124
+ SET process_identity = $2::jsonb, source_head = $3, updated_at = NOW()
125
+ WHERE singleton_id = TRUE AND writer_token = $1
126
+ AND (process_identity = '{}'::jsonb OR process_identity = $2::jsonb)
127
+ RETURNING writer_token, process_identity, source_head`, [this.writerToken, this.identityJson(), sourceHead || null]);
128
+ if (claimed.rowCount !== 1) {
129
+ throw canonicalTaskOperationError('canonical_task_writer_unavailable', 'Canonical Task writer process binding changed during claim');
130
+ }
131
+ await client.query('COMMIT');
132
+ this.writerClaimed = true;
133
+ return claimed.rows[0] || row;
134
+ }
135
+ catch (error) {
136
+ await client.query('ROLLBACK');
137
+ throw error;
138
+ }
139
+ finally {
140
+ client.release?.();
141
+ }
142
+ }
143
+ async assertWriter(client = this.pool) {
144
+ this.assertWriterEnabled();
145
+ this.assertConfigured();
146
+ const result = await client.query(`SELECT 1 FROM canonical_task_writer
147
+ WHERE singleton_id = TRUE AND writer_token = $1
148
+ AND process_identity = $2::jsonb`, [this.writerToken, this.identityJson()]);
149
+ if (!result.rowCount) {
150
+ this.writerClaimed = false;
151
+ throw canonicalTaskOperationError('canonical_task_writer_unavailable', 'This process does not own the Canonical Task writer token');
152
+ }
153
+ }
154
+ async readReadiness() {
155
+ this.assertConfigured();
156
+ const result = await this.pool.query(`SELECT ${READINESS_COLUMNS}
157
+ FROM canonical_task_readiness
158
+ WHERE singleton_id = TRUE`);
159
+ return result.rows[0] || null;
160
+ }
161
+ async reconcileReadiness({ manifestHash, schemaVersion, sourceHead, allowWriterRebind = false } = {}) {
162
+ this.assertWriterEnabled();
163
+ this.assertConfigured();
164
+ const client = await this.connect();
165
+ try {
166
+ await client.query('BEGIN');
167
+ const writer = await client.query(`SELECT writer_token, process_identity FROM canonical_task_writer
168
+ WHERE singleton_id = TRUE FOR UPDATE`);
169
+ if (!writer.rowCount
170
+ || writer.rows[0].writer_token !== this.writerToken
171
+ || !sameProcessIdentity(writer.rows[0].process_identity, this.processIdentity)) {
172
+ throw canonicalTaskOperationError('canonical_task_writer_unavailable', 'This process does not own the Canonical Task writer token');
173
+ }
174
+ const readiness = await client.query(`SELECT ${READINESS_COLUMNS}
175
+ FROM canonical_task_readiness
176
+ WHERE singleton_id = TRUE
177
+ FOR UPDATE`);
178
+ let row = readiness.rows[0] || null;
179
+ const releaseMatches = Boolean(row?.ready
180
+ && row.manifest_hash === manifestHash
181
+ && row.schema_version === schemaVersion
182
+ && row.source_head === sourceHead
183
+ && row.evidence_hash);
184
+ if (allowWriterRebind && releaseMatches && row.writer_token !== this.writerToken) {
185
+ const rebound = await client.query(`UPDATE canonical_task_readiness
186
+ SET writer_token = $1, updated_at = NOW()
187
+ WHERE singleton_id = TRUE
188
+ RETURNING ${READINESS_COLUMNS}`, [this.writerToken]);
189
+ row = rebound.rows[0] || row;
190
+ }
191
+ await client.query('COMMIT');
192
+ return row;
193
+ }
194
+ catch (error) {
195
+ await client.query('ROLLBACK');
196
+ throw error;
197
+ }
198
+ finally {
199
+ client.release?.();
200
+ }
201
+ }
202
+ async rebindReadinessSourceHead({ manifestHash, schemaVersion, fromHead, toHead } = {}) {
203
+ this.assertWriterEnabled();
204
+ this.assertConfigured();
205
+ const client = await this.connect();
206
+ try {
207
+ await client.query('BEGIN');
208
+ await this.assertWriter(client);
209
+ const rebound = await client.query(`UPDATE canonical_task_readiness
210
+ SET source_head = $4, writer_token = $5, updated_at = NOW()
211
+ WHERE singleton_id = TRUE
212
+ AND ready = TRUE
213
+ AND manifest_hash = $1
214
+ AND schema_version = $2
215
+ AND source_head = $3
216
+ AND evidence_hash IS NOT NULL
217
+ RETURNING ${READINESS_COLUMNS}`, [manifestHash, schemaVersion, fromHead, toHead, this.writerToken]);
218
+ if (rebound.rowCount !== 1) {
219
+ throw canonicalTaskOperationError('canonical_task_readiness_rebind_conflict', 'Persisted readiness changed while rebinding source head');
220
+ }
221
+ await client.query('COMMIT');
222
+ return rebound.rows[0];
223
+ }
224
+ catch (error) {
225
+ await client.query('ROLLBACK');
226
+ throw error;
227
+ }
228
+ finally {
229
+ client.release?.();
230
+ }
231
+ }
232
+ async releaseWriter() {
233
+ if (!this.pool || !this.writerToken || !this.writerClaimed || this.activeOperations > 0)
234
+ return false;
235
+ const result = await this.pool.query(`DELETE FROM canonical_task_writer
236
+ WHERE singleton_id = TRUE AND writer_token = $1
237
+ AND process_identity = $2::jsonb`, [this.writerToken, this.identityJson()]);
238
+ this.writerClaimed = false;
239
+ return result.rowCount === 1;
240
+ }
241
+ async execute(args) {
242
+ this.activeOperations += 1;
243
+ try {
244
+ return await this.executeCoordinated(args);
245
+ }
246
+ finally {
247
+ this.activeOperations -= 1;
248
+ }
249
+ }
250
+ async executeCoordinated({ scope, operationKey, fingerprint, recover = null, projectResult = (value) => value, run }) {
251
+ this.assertWriterEnabled();
252
+ this.assertConfigured();
253
+ await this.assertWriter();
254
+ let shouldRecover = false;
255
+ let completedNeedsRehydrate = false;
256
+ let waitForExisting = false;
257
+ const client = await this.connect();
258
+ try {
259
+ await client.query('BEGIN');
260
+ await this.assertWriter(client);
261
+ const inserted = await client.query(`INSERT INTO canonical_task_operations (scope, operation_key, fingerprint, state, writer_token)
262
+ VALUES ($1, $2, $3, 'running', $4)
263
+ ON CONFLICT (scope, operation_key) DO NOTHING
264
+ RETURNING id`, [scope, operationKey, fingerprint, this.writerToken]);
265
+ if (!inserted.rowCount) {
266
+ const existing = await client.query(`SELECT fingerprint, state, result_json, writer_token FROM canonical_task_operations
267
+ WHERE scope = $1 AND operation_key = $2 FOR UPDATE`, [scope, operationKey]);
268
+ const row = existing.rows[0];
269
+ if (row?.state === 'failed' && (row.fingerprint === fingerprint || scope === 'task-version')) {
270
+ const reclaimed = await client.query(`UPDATE canonical_task_operations
271
+ SET fingerprint = $3, state = 'running', writer_token = $4,
272
+ result_json = NULL, error_json = NULL, updated_at = NOW()
273
+ WHERE scope = $1 AND operation_key = $2 AND state = 'failed'`, [scope, operationKey, fingerprint, this.writerToken]);
274
+ if (reclaimed.rowCount !== 1) {
275
+ throw canonicalTaskOperationError('canonical_task_operation_in_progress', 'Canonical Task operation changed while it was being retried', 409);
276
+ }
277
+ shouldRecover = typeof recover === 'function';
278
+ await client.query('COMMIT');
279
+ }
280
+ else if (row?.fingerprint !== fingerprint) {
281
+ throw canonicalTaskOperationError('idempotency_conflict', 'Idempotency key was reused with different input', 409);
282
+ }
283
+ else if (row?.state === 'completed') {
284
+ await client.query('COMMIT');
285
+ if (typeof recover !== 'function')
286
+ return row.result_json;
287
+ completedNeedsRehydrate = true;
288
+ }
289
+ else if (row?.state === 'running' && row.writer_token && row.writer_token !== this.writerToken && typeof recover === 'function') {
290
+ const reclaimed = await client.query(`UPDATE canonical_task_operations
291
+ SET writer_token = $3, updated_at = NOW()
292
+ WHERE scope = $1 AND operation_key = $2
293
+ AND state = 'running' AND writer_token = $4`, [scope, operationKey, this.writerToken, row.writer_token]);
294
+ if (reclaimed.rowCount !== 1) {
295
+ throw canonicalTaskOperationError('canonical_task_operation_in_progress', 'Canonical Task operation is already in progress', 409);
296
+ }
297
+ shouldRecover = true;
298
+ await client.query('COMMIT');
299
+ }
300
+ else {
301
+ waitForExisting = true;
302
+ await client.query('COMMIT');
303
+ }
304
+ }
305
+ else {
306
+ await client.query('COMMIT');
307
+ }
308
+ }
309
+ catch (error) {
310
+ await client.query('ROLLBACK');
311
+ throw error;
312
+ }
313
+ finally {
314
+ client.release?.();
315
+ }
316
+ if (completedNeedsRehydrate) {
317
+ const recovery = await recover();
318
+ if (recovery?.recovered)
319
+ return recovery.result;
320
+ throw canonicalTaskOperationError('canonical_task_operation_result_unavailable', 'Completed Canonical Task operation could not be rehydrated from the canonical store');
321
+ }
322
+ if (waitForExisting) {
323
+ const persistedResult = await this.waitForCompletedOperation({ scope, operationKey, fingerprint });
324
+ if (typeof recover === 'function') {
325
+ const recovery = await recover();
326
+ if (recovery?.recovered)
327
+ return recovery.result;
328
+ throw canonicalTaskOperationError('canonical_task_operation_result_unavailable', 'Completed Canonical Task operation could not be rehydrated from the canonical store');
329
+ }
330
+ return persistedResult;
331
+ }
332
+ try {
333
+ const recovery = shouldRecover ? await recover() : null;
334
+ const result = recovery?.recovered ? recovery.result : await run();
335
+ await this.assertWriter();
336
+ await this.pool.query(`UPDATE canonical_task_operations
337
+ SET state = 'completed', result_json = $3::jsonb, updated_at = NOW()
338
+ WHERE scope = $1 AND operation_key = $2 AND writer_token = $4`, [scope, operationKey, JSON.stringify(projectResult(result)), this.writerToken]);
339
+ return result;
340
+ }
341
+ catch (error) {
342
+ await this.pool.query(`UPDATE canonical_task_operations
343
+ SET state = 'failed', error_json = $3::jsonb, updated_at = NOW()
344
+ WHERE scope = $1 AND operation_key = $2 AND writer_token = $4`, [scope, operationKey, JSON.stringify({ code: error?.code || null, message: error?.message || String(error) }), this.writerToken]).catch(() => { });
345
+ throw error;
346
+ }
347
+ }
348
+ async executePreparedDelete(args) {
349
+ this.activeOperations += 1;
350
+ try {
351
+ return await this.executePreparedDeleteCoordinated(args);
352
+ }
353
+ finally {
354
+ this.activeOperations -= 1;
355
+ }
356
+ }
357
+ async executePreparedDeleteCoordinated({ operationKey, versionClaimKey, fingerprint, versionFingerprint, principalNamespace, prepare, findTask, removeTask, authorizeReplay, }) {
358
+ this.assertWriterEnabled();
359
+ this.assertConfigured();
360
+ await this.assertWriter();
361
+ const existing = await this.pool.query(`SELECT fingerprint, state, result_json, authorization_snapshot
362
+ FROM canonical_task_operations
363
+ WHERE scope = 'task-delete' AND operation_key = $1`, [operationKey]);
364
+ if (existing.rowCount) {
365
+ const row = existing.rows[0];
366
+ if (row.fingerprint !== fingerprint) {
367
+ throw canonicalTaskOperationError('idempotency_conflict', 'Idempotency key was reused with different input', 409);
368
+ }
369
+ if (row.state === 'completed') {
370
+ if (typeof authorizeReplay !== 'function') {
371
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Completed Canonical Task delete operation cannot be replayed without authorization revalidation', 503);
372
+ }
373
+ const authorizationSnapshot = authorizationSnapshotRecord(row.authorization_snapshot);
374
+ if (!authorizationSnapshot) {
375
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Completed Canonical Task delete operation cannot be replayed without authorization revalidation', 503);
376
+ }
377
+ await authorizeReplay(authorizationSnapshot, row.result_json);
378
+ return row.result_json;
379
+ }
380
+ if (row.state !== 'prepared') {
381
+ throw canonicalTaskOperationError('canonical_task_operation_in_progress', 'Canonical Task delete operation is already in progress', 409);
382
+ }
383
+ return this.completePreparedDelete({ operationKey, versionClaimKey, row, findTask, removeTask, authorizeReplay });
384
+ }
385
+ const versionClaim = await this.pool.query(`SELECT authorization_snapshot
386
+ FROM canonical_task_operations
387
+ WHERE scope = 'task-version' AND operation_key = $1`, [versionClaimKey]);
388
+ if (versionClaim.rowCount && versionClaim.rows[0]?.authorization_snapshot?.principal_namespace === principalNamespace) {
389
+ throw canonicalTaskOperationError('version_conflict', 'Task version was already claimed by another operation', 409);
390
+ }
391
+ const prepared = await prepare();
392
+ const authorizationSnapshot = { ...prepared.authorizationSnapshot, principal_namespace: principalNamespace };
393
+ const client = await this.connect();
394
+ try {
395
+ await client.query('BEGIN');
396
+ await this.assertWriter(client);
397
+ const claimed = await client.query(`INSERT INTO canonical_task_operations (
398
+ scope, operation_key, fingerprint, state, writer_token,
399
+ result_json, authorization_snapshot, recovery_checkpoint
400
+ ) VALUES ('task-version', $1, $2, 'prepared', $3, $4::jsonb, $5::jsonb, $6::jsonb)
401
+ ON CONFLICT (scope, operation_key) DO NOTHING
402
+ RETURNING id`, [
403
+ versionClaimKey,
404
+ versionFingerprint,
405
+ this.writerToken,
406
+ JSON.stringify(prepared.result),
407
+ JSON.stringify(authorizationSnapshot),
408
+ JSON.stringify({ phase: 'prepared', task_present: true }),
409
+ ]);
410
+ if (!claimed.rowCount) {
411
+ throw canonicalTaskOperationError('version_conflict', 'Task version was already claimed by another operation', 409);
412
+ }
413
+ const intent = await client.query(`INSERT INTO canonical_task_operations (
414
+ scope, operation_key, fingerprint, state, writer_token,
415
+ result_json, authorization_snapshot, recovery_checkpoint
416
+ ) VALUES ('task-delete', $1, $2, 'prepared', $3, $4::jsonb, $5::jsonb, $6::jsonb)
417
+ ON CONFLICT (scope, operation_key) DO NOTHING
418
+ RETURNING id`, [
419
+ operationKey,
420
+ fingerprint,
421
+ this.writerToken,
422
+ JSON.stringify(prepared.result),
423
+ JSON.stringify(authorizationSnapshot),
424
+ JSON.stringify({ phase: 'prepared', task_present: true, version_claim_key: versionClaimKey }),
425
+ ]);
426
+ if (!intent.rowCount) {
427
+ throw canonicalTaskOperationError('canonical_task_operation_in_progress', 'Canonical Task delete operation is already in progress', 409);
428
+ }
429
+ await client.query('COMMIT');
430
+ }
431
+ catch (error) {
432
+ await client.query('ROLLBACK');
433
+ throw error;
434
+ }
435
+ finally {
436
+ client.release?.();
437
+ }
438
+ return this.completePreparedDelete({
439
+ operationKey,
440
+ versionClaimKey,
441
+ row: { fingerprint, result_json: prepared.result, authorization_snapshot: authorizationSnapshot },
442
+ findTask,
443
+ removeTask,
444
+ authorizeReplay,
445
+ });
446
+ }
447
+ async completePreparedDelete({ operationKey, versionClaimKey, row, findTask, removeTask, authorizeReplay }) {
448
+ await this.assertWriter();
449
+ const snapshotRecord = authorizationSnapshotRecord(row.authorization_snapshot);
450
+ if (!snapshotRecord) {
451
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Prepared Canonical Task delete operation has no authorization snapshot', 503);
452
+ }
453
+ const task = await findTask();
454
+ if (task) {
455
+ await removeTask(task);
456
+ }
457
+ else {
458
+ if (typeof authorizeReplay !== 'function') {
459
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Prepared Canonical Task delete operation cannot be resumed without authorization revalidation', 503);
460
+ }
461
+ await authorizeReplay(snapshotRecord, row.result_json);
462
+ }
463
+ const client = await this.connect();
464
+ try {
465
+ await client.query('BEGIN');
466
+ await this.assertWriter(client);
467
+ const completed = await client.query(`UPDATE canonical_task_operations
468
+ SET state = 'completed', writer_token = $3,
469
+ recovery_checkpoint = $4::jsonb, error_json = NULL, updated_at = NOW()
470
+ WHERE scope = 'task-delete' AND operation_key = $1
471
+ AND fingerprint = $2 AND state = 'prepared'`, [
472
+ operationKey,
473
+ row.fingerprint,
474
+ this.writerToken,
475
+ JSON.stringify({ phase: 'completed', task_present: false, version_claim_key: versionClaimKey }),
476
+ ]);
477
+ if (!completed.rowCount) {
478
+ const replay = await client.query(`SELECT fingerprint, state, result_json, authorization_snapshot
479
+ FROM canonical_task_operations
480
+ WHERE scope = 'task-delete' AND operation_key = $1 FOR UPDATE`, [operationKey]);
481
+ if (replay.rows[0]?.fingerprint !== row.fingerprint || replay.rows[0]?.state !== 'completed') {
482
+ throw canonicalTaskOperationError('canonical_task_operation_in_progress', 'Canonical Task delete completion could not be claimed', 409);
483
+ }
484
+ if (typeof authorizeReplay !== 'function') {
485
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Completed Canonical Task delete operation cannot be replayed without authorization revalidation', 503);
486
+ }
487
+ const replaySnapshot = authorizationSnapshotRecord(replay.rows[0].authorization_snapshot);
488
+ if (!replaySnapshot) {
489
+ throw canonicalTaskOperationError('canonical_task_authorization_snapshot_unavailable', 'Completed Canonical Task delete operation cannot be replayed without authorization revalidation', 503);
490
+ }
491
+ await authorizeReplay(replaySnapshot, replay.rows[0].result_json);
492
+ await client.query('COMMIT');
493
+ return replay.rows[0].result_json;
494
+ }
495
+ await client.query(`UPDATE canonical_task_operations
496
+ SET state = 'completed', writer_token = $2,
497
+ recovery_checkpoint = $3::jsonb, error_json = NULL, updated_at = NOW()
498
+ WHERE scope = 'task-version' AND operation_key = $1 AND state = 'prepared'`, [
499
+ versionClaimKey,
500
+ this.writerToken,
501
+ JSON.stringify({ phase: 'completed', task_present: false, delete_operation_key: operationKey }),
502
+ ]);
503
+ await client.query('COMMIT');
504
+ return row.result_json;
505
+ }
506
+ catch (error) {
507
+ await client.query('ROLLBACK');
508
+ throw error;
509
+ }
510
+ finally {
511
+ client.release?.();
512
+ }
513
+ }
514
+ }
515
+ export class CanonicalTaskReadinessError extends Error {
516
+ code;
517
+ status = 503;
518
+ details;
519
+ constructor(code = 'canonical_task_mutation_not_ready', message = 'Canonical Task mutation is not ready') {
520
+ super(message);
521
+ this.name = 'CanonicalTaskReadinessError';
522
+ this.code = code;
523
+ }
524
+ }
525
+ /**
526
+ * Opens Canonical Task mutation only while the persisted readiness row was
527
+ * blessed for this release: the same writer, manifest, schema and source head.
528
+ */
529
+ export class CanonicalTaskReadiness {
530
+ operationRepository;
531
+ expected;
532
+ sourceHeadRebindGuard;
533
+ logger;
534
+ ready = false;
535
+ reason = 'readiness_not_verified';
536
+ evidence = null;
537
+ constructor({ operationRepository = null, manifestHash = null, schemaVersion = null, sourceHead = null, sourceHeadRebindGuard = null, logger = console, } = {}) {
538
+ this.operationRepository = operationRepository;
539
+ this.expected = { manifestHash, schemaVersion, sourceHead };
540
+ this.sourceHeadRebindGuard = sourceHeadRebindGuard;
541
+ this.logger = logger;
542
+ }
543
+ open(evidence = null) {
544
+ this.ready = true;
545
+ this.reason = null;
546
+ this.evidence = evidence;
547
+ }
548
+ close(reason = 'readiness_disabled') {
549
+ this.ready = false;
550
+ this.reason = reason;
551
+ this.evidence = null;
552
+ }
553
+ matches(row) {
554
+ return Boolean(row?.ready
555
+ && row.writer_token === this.operationRepository?.writerToken
556
+ && row.manifest_hash === this.expected.manifestHash
557
+ && row.schema_version === this.expected.schemaVersion
558
+ && row.source_head === this.expected.sourceHead
559
+ && row.evidence_hash);
560
+ }
561
+ async refresh({ allowWriterRebind = false } = {}) {
562
+ const row = await this.operationRepository.reconcileReadiness({
563
+ manifestHash: this.expected.manifestHash,
564
+ schemaVersion: this.expected.schemaVersion,
565
+ sourceHead: this.expected.sourceHead,
566
+ allowWriterRebind,
567
+ });
568
+ if (!this.matches(row)) {
569
+ this.close(row?.ready ? 'persisted_readiness_mismatch' : (row?.reason || 'persisted_readiness_missing'));
570
+ return { ready: false, reason: this.reason };
571
+ }
572
+ this.open({ evidence_hash: row.evidence_hash, evidence_path: row.evidence_path, updated_at: row.updated_at });
573
+ return { ready: true, evidence: this.evidence };
574
+ }
575
+ // Readiness rows are blessed for one source_head. When a deploy restarts the
576
+ // process on a newer HEAD, mutation stays fail-closed unless the guard proves
577
+ // that nothing Canonical-Task-relevant changed between the two heads.
578
+ async attemptSourceHeadRebind() {
579
+ if (!this.sourceHeadRebindGuard || typeof this.operationRepository?.rebindReadinessSourceHead !== 'function')
580
+ return false;
581
+ const row = await this.operationRepository.readReadiness();
582
+ const rebindCandidate = Boolean(row?.ready
583
+ && row.manifest_hash === this.expected.manifestHash
584
+ && row.schema_version === this.expected.schemaVersion
585
+ && row.evidence_hash
586
+ && row.source_head
587
+ && row.source_head !== this.expected.sourceHead);
588
+ if (!rebindCandidate)
589
+ return false;
590
+ const verdict = await this.sourceHeadRebindGuard({ fromHead: row.source_head, toHead: this.expected.sourceHead });
591
+ if (!verdict?.allowed) {
592
+ const changed = [...(verdict?.changedPaths ?? []), ...(verdict?.changedPackages ?? [])];
593
+ this.logger?.warn?.(`[canonical-task] source_head rebind refused (${verdict?.reason || 'unknown'}): `
594
+ + `${row.source_head} -> ${this.expected.sourceHead}`
595
+ + (changed.length ? ` changed=${changed.join(',')}` : ''));
596
+ return false;
597
+ }
598
+ await this.operationRepository.rebindReadinessSourceHead({
599
+ manifestHash: this.expected.manifestHash,
600
+ schemaVersion: this.expected.schemaVersion,
601
+ fromHead: row.source_head,
602
+ toHead: this.expected.sourceHead,
603
+ });
604
+ this.logger?.warn?.(`[canonical-task] readiness source_head rebound after guarded diff check: ${row.source_head} -> ${this.expected.sourceHead}`);
605
+ return true;
606
+ }
607
+ async initialize() {
608
+ this.close('readiness_not_verified');
609
+ if (!this.operationRepository) {
610
+ this.close('coordination_repository_unavailable');
611
+ return { ready: false, reason: this.reason };
612
+ }
613
+ try {
614
+ await this.operationRepository.claimWriter({ sourceHead: this.expected.sourceHead });
615
+ const result = await this.refresh({ allowWriterRebind: true });
616
+ if (result.ready || this.reason !== 'persisted_readiness_mismatch')
617
+ return result;
618
+ if (await this.attemptSourceHeadRebind())
619
+ return await this.refresh({ allowWriterRebind: true });
620
+ return result;
621
+ }
622
+ catch (error) {
623
+ this.close(error?.code || 'readiness_reconcile_failed');
624
+ return { ready: false, reason: this.reason, error };
625
+ }
626
+ }
627
+ async assertMutationReady() {
628
+ try {
629
+ await this.refresh({ allowWriterRebind: false });
630
+ }
631
+ catch (error) {
632
+ this.close(error?.code || 'readiness_reconcile_failed');
633
+ }
634
+ if (!this.ready) {
635
+ const error = new CanonicalTaskReadinessError();
636
+ error.details = { reason: this.reason };
637
+ throw error;
638
+ }
639
+ }
640
+ }
641
+ const execFileAsync = promisify(execFileCallback);
642
+ /**
643
+ * Decides whether readiness blessed for one source head may follow a deploy
644
+ * to another head. It allows the rebind only when git proves that no guarded
645
+ * path and no guarded package changed between them; anything unknown stays
646
+ * fail-closed.
647
+ */
648
+ export function createCanonicalTaskSourceHeadGuard({ repoDir, guardedPaths, guardedPackages = [], lockfilePath = 'package-lock.json', execFile = execFileAsync, }) {
649
+ if (!Array.isArray(guardedPaths) || guardedPaths.length === 0 || guardedPaths.some((path) => typeof path !== 'string' || !path)) {
650
+ throw new Error('Canonical Task source head guard requires guarded paths');
651
+ }
652
+ const lockedEntry = async (head, name) => {
653
+ try {
654
+ const text = String((await execFile('git', ['-C', repoDir, 'show', `${head}:${lockfilePath}`])).stdout);
655
+ const lock = JSON.parse(text);
656
+ const entry = lock?.packages?.[`node_modules/${name}`] ?? lock?.dependencies?.[name] ?? null;
657
+ return entry ? { version: entry.version ?? null, resolved: entry.resolved ?? null, integrity: entry.integrity ?? null } : null;
658
+ }
659
+ catch {
660
+ return null;
661
+ }
662
+ };
663
+ return async function canRebindSourceHead({ fromHead, toHead } = {}) {
664
+ if (!repoDir)
665
+ return { allowed: false, reason: 'source_head_guard_repo_missing' };
666
+ if (!fromHead || !toHead)
667
+ return { allowed: false, reason: 'source_head_guard_head_missing' };
668
+ if (fromHead === toHead)
669
+ return { allowed: true, changedPaths: [] };
670
+ try {
671
+ const { stdout } = await execFile('git', ['-C', repoDir, 'diff', '--name-only', `${fromHead}..${toHead}`, '--', ...guardedPaths]);
672
+ const changedPaths = String(stdout).split('\n').map((line) => line.trim()).filter(Boolean);
673
+ if (changedPaths.length > 0)
674
+ return { allowed: false, reason: 'canonical_task_paths_changed', changedPaths };
675
+ const changedPackages = [];
676
+ for (const name of guardedPackages) {
677
+ const [before, after] = [await lockedEntry(fromHead, name), await lockedEntry(toHead, name)];
678
+ if (!before || !after) {
679
+ return {
680
+ allowed: false,
681
+ reason: 'canonical_task_package_lock_unreadable',
682
+ changedPaths: [],
683
+ changedPackages: [name],
684
+ };
685
+ }
686
+ if (JSON.stringify(before) !== JSON.stringify(after))
687
+ changedPackages.push(name);
688
+ }
689
+ if (changedPackages.length > 0) {
690
+ return { allowed: false, reason: 'canonical_task_packages_changed', changedPaths: [], changedPackages };
691
+ }
692
+ return { allowed: true, changedPaths: [] };
693
+ }
694
+ catch (error) {
695
+ // Unknown commits (shallow clone, rewritten history) or an unreadable
696
+ // lockfile must stay fail-closed.
697
+ return { allowed: false, reason: 'source_head_diff_failed', error: error?.message || String(error) };
698
+ }
699
+ };
700
+ }