@cleocode/contracts 2026.9.26 → 2026.10.2

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/dist/cloud/api.d.ts +469 -2
  2. package/dist/cloud/api.d.ts.map +1 -1
  3. package/dist/cloud/api.js +209 -8
  4. package/dist/cloud/api.js.map +1 -1
  5. package/dist/cloud/envelope.d.ts +6 -0
  6. package/dist/cloud/envelope.d.ts.map +1 -1
  7. package/dist/cloud/envelope.js +12 -0
  8. package/dist/cloud/envelope.js.map +1 -1
  9. package/dist/config.d.ts +13 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/heavy-command-hook.d.ts +123 -0
  12. package/dist/heavy-command-hook.d.ts.map +1 -0
  13. package/dist/heavy-command-hook.js +17 -0
  14. package/dist/heavy-command-hook.js.map +1 -0
  15. package/dist/index.d.ts +9 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +6 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/install.d.ts +8 -0
  20. package/dist/install.d.ts.map +1 -1
  21. package/dist/invariants/index.d.ts +2 -0
  22. package/dist/invariants/index.d.ts.map +1 -1
  23. package/dist/invariants/index.js +1 -0
  24. package/dist/invariants/index.js.map +1 -1
  25. package/dist/invariants/sync-write-invariants.d.ts +115 -0
  26. package/dist/invariants/sync-write-invariants.d.ts.map +1 -0
  27. package/dist/invariants/sync-write-invariants.js +123 -0
  28. package/dist/invariants/sync-write-invariants.js.map +1 -0
  29. package/dist/nexus-account.d.ts +91 -3
  30. package/dist/nexus-account.d.ts.map +1 -1
  31. package/dist/nexus-account.js +30 -1
  32. package/dist/nexus-account.js.map +1 -1
  33. package/dist/nexus-cloud.d.ts +718 -0
  34. package/dist/nexus-cloud.d.ts.map +1 -0
  35. package/dist/nexus-cloud.js +418 -0
  36. package/dist/nexus-cloud.js.map +1 -0
  37. package/dist/nexus-vault.d.ts +266 -0
  38. package/dist/nexus-vault.d.ts.map +1 -0
  39. package/dist/nexus-vault.js +82 -0
  40. package/dist/nexus-vault.js.map +1 -0
  41. package/dist/portable-bundle.d.ts +5 -0
  42. package/dist/portable-bundle.d.ts.map +1 -1
  43. package/dist/resource-governor.d.ts +13 -0
  44. package/dist/resource-governor.d.ts.map +1 -1
  45. package/dist/resource-governor.js +13 -0
  46. package/dist/resource-governor.js.map +1 -1
  47. package/dist/sync-schema.d.ts +34 -0
  48. package/dist/sync-schema.d.ts.map +1 -0
  49. package/dist/sync-schema.js +34 -0
  50. package/dist/sync-schema.js.map +1 -0
  51. package/dist/task.d.ts +49 -1
  52. package/dist/task.d.ts.map +1 -1
  53. package/dist/tasks.d.ts +6 -0
  54. package/dist/tasks.d.ts.map +1 -1
  55. package/dist/tasks.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/cloud/api.ts +258 -9
  58. package/src/cloud/envelope.ts +12 -0
  59. package/src/config.ts +13 -0
  60. package/src/heavy-command-hook.ts +136 -0
  61. package/src/index.ts +108 -0
  62. package/src/install.ts +9 -0
  63. package/src/invariants/index.ts +6 -0
  64. package/src/invariants/sync-write-invariants.ts +460 -0
  65. package/src/nexus-account.ts +120 -3
  66. package/src/nexus-cloud.ts +673 -0
  67. package/src/nexus-vault.ts +280 -0
  68. package/src/portable-bundle.ts +5 -0
  69. package/src/resource-governor.ts +15 -0
  70. package/src/sync-schema.ts +34 -0
  71. package/src/task.ts +49 -1
  72. package/src/tasks.ts +6 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Sync write-invariant registry (T12881): every rule TypeScript enforces on a
3
+ * write to a synced table, with the class that says how it survives merged
4
+ * apply (spec `t12859-sync-write-validator-inventory` §3.6.7, journal design
5
+ * §3.6).
6
+ *
7
+ * The raw applier never runs the TypeScript write paths, so a rejection site
8
+ * on a synced write path carries `// @sync-invariant <id>` naming an entry
9
+ * here, or an escape tag (`none:input-shape <reason>`,
10
+ * `none:local-only <reason>`). Gate 38 (`scripts/lint-sync-write-invariants.mjs`)
11
+ * finds the sites with the TypeScript compiler API and fails on an untagged
12
+ * one that is not in its shrink-only baseline, on a dangling tag, and on a
13
+ * registry entry that does not close (§3.6.7 rule 5).
14
+ *
15
+ * Seeded from the inventory's distinct catalogues: the trigger and index
16
+ * guards of §3.6.2, the fifteen post-apply check families PAC-01..PAC-15 of
17
+ * §3.6.5 and the typed merge rules of §3.6.6. The executable post-apply
18
+ * checks and the merge-rule registry belong to the merge engine (T12344) and
19
+ * do not exist yet, so each PAC and each rule carries `pending` naming its
20
+ * own open T12344 item (T12922..T12936 for PAC-01..15, T12937..T12945 for the
21
+ * rules); `--verify-tasks` checks those tasks are open. Trigger-covered
22
+ * entries carry no pending: their triggers and indexes are checked against
23
+ * migrations and fresh stores. The §3.5 Rule 4 guard-class footprint check is
24
+ * not enforced until that class exists.
25
+ *
26
+ * Types and const data only (arch gate 10).
27
+ *
28
+ * @task T12881
29
+ * @epic T12323
30
+ */
31
+
32
+ /**
33
+ * How a write rule survives merged apply (§3.6 intro).
34
+ *
35
+ * - `trigger-covered` — a migration-created trigger, CHECK, UNIQUE or FK
36
+ * enforces the same rule on apply.
37
+ * - `post-apply-check` — a multi-row or cross-table rule evaluated over the
38
+ * apply page's footprint (a T12344 PAC item).
39
+ * - `monotonic-merge-rule` — a per-row or per-field transition or grouping
40
+ * rule, enforced by a T12344 typed merge rule.
41
+ * - `not-sync-relevant` — input shape, local-only data or a single-column
42
+ * value rule LWW cannot break.
43
+ * - `identity-layer` — really about id minting; T12341 uids and re-minting
44
+ * own it.
45
+ *
46
+ * @task T12881
47
+ */
48
+ export type SyncWriteInvariantClass =
49
+ | 'trigger-covered'
50
+ | 'post-apply-check'
51
+ | 'monotonic-merge-rule'
52
+ | 'not-sync-relevant'
53
+ | 'identity-layer';
54
+
55
+ /**
56
+ * A closure requirement the entry cannot meet yet, and the task that meets it.
57
+ *
58
+ * @task T12881
59
+ */
60
+ export interface SyncWriteInvariantPending {
61
+ /** The task that lands the missing piece (e.g. the T12344 check or rule). */
62
+ readonly task: string;
63
+ /** What is missing. */
64
+ readonly reason: string;
65
+ }
66
+
67
+ /**
68
+ * One distinct write rule on synced tables.
69
+ *
70
+ * @task T12881
71
+ */
72
+ export interface SyncWriteInvariant {
73
+ /** Stable id named by `@sync-invariant <id>` tags, e.g. `task.status.absorbing`. */
74
+ readonly id: string;
75
+ /** How the rule survives merged apply. */
76
+ readonly class: SyncWriteInvariantClass;
77
+ /** Physical tables; each must be classified by Gate A. */
78
+ readonly tables: readonly string[];
79
+ /** Known rejection sites (repo-relative file, enclosing symbol, codes). */
80
+ readonly sites: readonly {
81
+ readonly file: string;
82
+ readonly symbol: string;
83
+ readonly codes: readonly string[];
84
+ }[];
85
+ /** `trigger-covered`: trigger or index names a migration creates. */
86
+ readonly triggers?: readonly string[];
87
+ /**
88
+ * `post-apply-check`: the check and its footprint. `module` and
89
+ * `functionName` may be absent only while `pending` is set.
90
+ */
91
+ readonly check?: {
92
+ readonly module?: string;
93
+ readonly functionName?: string;
94
+ readonly footprint: readonly string[];
95
+ };
96
+ /** `monotonic-merge-rule`: the table and columns the rule merges (`*` = whole row). */
97
+ readonly mergeRule?: { readonly table: string; readonly columns: readonly string[] };
98
+ /** A runtime guard enforcing the rule; it must have a production caller. */
99
+ readonly runtimeGate?: { readonly module: string; readonly functionName: string };
100
+ /** Convergence sources the rule reads (config, file, env, local-only column). */
101
+ readonly readsNonSynced?: readonly string[];
102
+ /** Set when every replica is pinned to the same `readsNonSynced` policy (§3.5 Rule 1). */
103
+ readonly pinnedPolicy?: boolean;
104
+ /** Inventory rows behind the entry (PAC item, V/C/S/B/D/… row ids). */
105
+ readonly inventory?: readonly string[];
106
+ /** A closure requirement not met yet. */
107
+ readonly pending?: SyncWriteInvariantPending;
108
+ /** Why; required for `not-sync-relevant` and `identity-layer`. */
109
+ readonly reason: string;
110
+ }
111
+
112
+ /** A post-apply check not implemented yet; `task` is its T12344 item. */
113
+ function checkPending(task: string): SyncWriteInvariantPending {
114
+ return { task, reason: 'the post-apply check is not implemented yet (T12344 item)' };
115
+ }
116
+
117
+ /** A merge rule the merge-rule registry does not define yet; `task` is its T12344 item. */
118
+ function rulePending(task: string): SyncWriteInvariantPending {
119
+ return { task, reason: 'the typed merge-rule registry does not define it yet (T12344 item)' };
120
+ }
121
+
122
+ function trigger(
123
+ id: string,
124
+ tables: readonly string[],
125
+ triggers: readonly string[],
126
+ inventory: readonly string[],
127
+ reason: string,
128
+ ): SyncWriteInvariant {
129
+ return { id, class: 'trigger-covered', tables, sites: [], triggers, inventory, reason };
130
+ }
131
+
132
+ function pac(
133
+ id: string,
134
+ item: string,
135
+ task: string,
136
+ tables: readonly string[],
137
+ footprint: readonly string[],
138
+ reason: string,
139
+ ): SyncWriteInvariant {
140
+ return {
141
+ id,
142
+ class: 'post-apply-check',
143
+ tables,
144
+ sites: [],
145
+ check: { footprint },
146
+ inventory: [item],
147
+ pending: checkPending(task),
148
+ reason,
149
+ };
150
+ }
151
+
152
+ function rule(
153
+ id: string,
154
+ task: string,
155
+ table: string,
156
+ columns: readonly string[],
157
+ inventory: readonly string[],
158
+ reason: string,
159
+ tables: readonly string[] = [table],
160
+ ): SyncWriteInvariant {
161
+ return {
162
+ id,
163
+ class: 'monotonic-merge-rule',
164
+ tables,
165
+ sites: [],
166
+ mergeRule: { table, columns },
167
+ inventory,
168
+ pending: rulePending(task),
169
+ reason,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * The sync write-invariant registry, in inventory order.
175
+ *
176
+ * @task T12881
177
+ */
178
+ export const SYNC_WRITE_INVARIANTS: readonly SyncWriteInvariant[] = Object.freeze([
179
+ // §3.6.2: RAISE guards and UNIQUE indexes a migration creates on synced tables.
180
+ trigger(
181
+ 'task.parent.no-cycle',
182
+ ['tasks_tasks'],
183
+ ['tasks_tasks_parent_cycle_guard_insert', 'tasks_tasks_parent_cycle_guard_update'],
184
+ ['V10', 'V11'],
185
+ 'the parent chain is acyclic (the INSERT variant misses a self-parent, §3.6.3 item 2)',
186
+ ),
187
+ trigger(
188
+ 'task.parent.type-matrix',
189
+ ['tasks_tasks'],
190
+ ['tasks_tasks_parent_type_matrix_insert', 'tasks_tasks_parent_type_matrix_update'],
191
+ ['X1'],
192
+ 'parent/child type matrix; weaker than TypeScript (§3.6.3 item 1), the rest is PAC-01',
193
+ ),
194
+ trigger(
195
+ 'task.status.pipeline-consistency',
196
+ ['tasks_tasks'],
197
+ ['trg_tasks_tasks_status_pipeline_insert', 'trg_tasks_tasks_status_pipeline_update'],
198
+ ['V19', 'C21'],
199
+ 'T877: status and pipeline_stage agree on one row',
200
+ ),
201
+ trigger(
202
+ 'task.relation.non-containment',
203
+ ['tasks_task_relations'],
204
+ ['tasks_task_relations_non_containment_insert', 'tasks_task_relations_non_containment_update'],
205
+ ['V31'],
206
+ 'no relation between a parent and its child, on relation writes only (§3.6.3 item 4)',
207
+ ),
208
+ trigger(
209
+ 'task.ac.child-target',
210
+ ['tasks_task_acceptance_criteria'],
211
+ ['tasks_task_acceptance_child_target_insert', 'tasks_task_acceptance_child_target_update'],
212
+ ['C28'],
213
+ 'a child_task AC targets a direct child, on AC writes only (§3.6.3 item 4)',
214
+ ),
215
+ trigger(
216
+ 'session.handoff.append-only',
217
+ ['tasks_session_handoff_entries'],
218
+ ['trg_tasks_session_handoff_no_update'],
219
+ ['H2'],
220
+ 'handoff entries are never updated',
221
+ ),
222
+ trigger(
223
+ 'task.claim.lease-iso',
224
+ ['tasks_tasks'],
225
+ ['tasks_tasks_lease_iso_insert', 'tasks_tasks_lease_iso_update'],
226
+ [],
227
+ 'claim lease timestamps are ISO-8601 (t12736)',
228
+ ),
229
+ trigger(
230
+ 'task.ac-binding.unique',
231
+ ['tasks_evidence_ac_bindings'],
232
+ ['uq_tasks_evidence_ac_bindings_atom_ac_type'],
233
+ ['C31'],
234
+ 'one binding per (evidence atom, AC, binding type)',
235
+ ),
236
+ trigger(
237
+ 'release.version.unique',
238
+ ['tasks_releases'],
239
+ ['uq_tasks_releases_version'],
240
+ ['REL-1'],
241
+ 'one release row per version',
242
+ ),
243
+ trigger(
244
+ 'selfimprove.dhq.one-open',
245
+ ['selfimprove_dhq'],
246
+ ['ux_selfimprove_dhq_open'],
247
+ ['D49'],
248
+ 'at most one open DHQ entry per question_hash',
249
+ ),
250
+ trigger(
251
+ 'account.one-active-per-provider',
252
+ ['accounts'],
253
+ ['ux_accounts_active_provider'],
254
+ ['A40'],
255
+ 'at most one active account per provider (partial UNIQUE)',
256
+ ),
257
+
258
+ // §3.6.5: post-apply check families (T12344 items).
259
+ pac(
260
+ 'task.tree.shape',
261
+ 'PAC-01',
262
+ 'T12922',
263
+ ['tasks_tasks'],
264
+ ['rows with a changed parent_id or type', 'their direct children and ancestor chain'],
265
+ 'saga→epic→task→subtask matrix, non-saga has a parent, depth ≤ 3, no self-parent',
266
+ ),
267
+ pac(
268
+ 'task.terminal-parent.live-children',
269
+ 'PAC-02',
270
+ 'T12923',
271
+ ['tasks_tasks'],
272
+ ['rows whose status, stage or parent changed', 'their parent and siblings'],
273
+ 'no terminal parent with a live child; epic stage ≥ children; rollup re-evaluated',
274
+ ),
275
+ pac(
276
+ 'task.dependency.graph',
277
+ 'PAC-03',
278
+ 'T12924',
279
+ ['tasks_task_dependencies', 'tasks_tasks'],
280
+ ['dependency edges inserted in the page', 'reachability from each depends_on'],
281
+ 'dependencies are acyclic, with no self-edge',
282
+ ),
283
+ pac(
284
+ 'task.done.evidence',
285
+ 'PAC-04',
286
+ 'T12925',
287
+ ['tasks_tasks', 'tasks_task_acceptance_criteria', 'tasks_evidence_ac_bindings'],
288
+ ['done rows in the page', 'their AC rows, bindings, acceptance_json and verification_json'],
289
+ 'a done task has passed verification and every AC bound or waived',
290
+ ),
291
+ pac(
292
+ 'task.ac.integrity',
293
+ 'PAC-05',
294
+ 'T12926',
295
+ ['tasks_tasks', 'tasks_task_acceptance_criteria', 'tasks_evidence_ac_bindings'],
296
+ ['tasks whose AC rows, bindings, acceptance_json, parent_id or relations changed'],
297
+ 'acceptance_json ⇔ AC rows, child_task ACs match children, bindings resolve',
298
+ ),
299
+ pac(
300
+ 'lifecycle.rows',
301
+ 'PAC-06',
302
+ 'T12927',
303
+ ['tasks_lifecycle_pipelines', 'tasks_lifecycle_stages', 'tasks_tasks'],
304
+ ['lifecycle pipeline, stage and gate rows in the page', 'the owning task row'],
305
+ 'one pipeline per task, one stage row per name, pipeline_stage ≥ completed stages',
306
+ ),
307
+ pac(
308
+ 'derived.mirrors',
309
+ 'PAC-07',
310
+ 'T12928',
311
+ ['tasks_task_labels', 'brain_sticky_tags', 'docs_wikilinks', 'tasks_tasks'],
312
+ ['owning rows whose JSON or junction changed'],
313
+ 'junction tables equal their JSON source',
314
+ ),
315
+ pac(
316
+ 'session.goal.singletons',
317
+ 'PAC-08',
318
+ 'T12929',
319
+ ['tasks_sessions', 'tasks_task_work_history', 'tasks_goal'],
320
+ ['session rows whose status, scope or chain changed', 'work-history rows per session'],
321
+ 'one active session per scope, reciprocal chain, one open interval, one live goal',
322
+ ),
323
+ pac(
324
+ 'audit.uniqueness',
325
+ 'PAC-09',
326
+ 'T12930',
327
+ ['tasks_audit_log'],
328
+ ['audit rows with a non-null idempotency key', 'rollback audit rows'],
329
+ 'idempotency keys are unique across replicas; at most one rollback per receipt',
330
+ ),
331
+ pac(
332
+ 'identity.collisions',
333
+ 'PAC-10',
334
+ 'T12931',
335
+ ['tasks_tasks', 'tasks_sessions', 'brain_observations', 'docs_attachments'],
336
+ ['inserted rows', "the natural key's index"],
337
+ 'display-id and natural-key clashes become an alias or re-mint (T12341)',
338
+ ),
339
+ pac(
340
+ 'dedupe.at-most-one',
341
+ 'PAC-11',
342
+ 'T12932',
343
+ ['brain_decisions', 'brain_learnings', 'tasks_external_task_links'],
344
+ ['inserted rows grouped by the natural key'],
345
+ 'duplicates created on two devices are merged',
346
+ ),
347
+ pac(
348
+ 'ref.soft-orphans',
349
+ 'PAC-12',
350
+ 'T12933',
351
+ ['brain_memory_links', 'brain_page_edges', 'docs_attachment_refs', 'tasks_task_commits'],
352
+ ['reference columns of the rows in the page', 'their targets'],
353
+ 'cross-DB and polymorphic references resolve',
354
+ ),
355
+ pac(
356
+ 'supersession.graph',
357
+ 'PAC-13',
358
+ 'T12934',
359
+ ['brain_decisions', 'docs_attachments', 'tasks_architecture_decisions'],
360
+ ['rows whose supersession columns changed', 'a chain walk'],
361
+ 'supersession chains are acyclic with at most one successor',
362
+ ),
363
+ pac(
364
+ 'derived.counters',
365
+ 'PAC-14',
366
+ 'T12935',
367
+ ['docs_attachments', 'conduit_attachments', 'docs_pipeline_manifest'],
368
+ ['the owning rows'],
369
+ 'derived counters and cross-column facts agree with their rows',
370
+ ),
371
+ pac(
372
+ 'apply.preconditions',
373
+ 'PAC-15',
374
+ 'T12936',
375
+ ['tasks_tasks'],
376
+ ['the whole page'],
377
+ 'no apply while the local twin collapse has failed; restore is a re-baseline',
378
+ ),
379
+
380
+ // §3.6.6: typed merge rules (T12344).
381
+ rule(
382
+ 'task.status.absorbing',
383
+ 'T12937',
384
+ 'tasks_tasks',
385
+ ['status', 'completed_at', 'cancelled_at', 'cancellation_reason', 'pipeline_stage'],
386
+ ['V18', 'V20', 'C22', 'P40', 'P46', 'P47'],
387
+ 'done, cancelled and archived are absorbing; leaving them needs an explicit op',
388
+ ),
389
+ rule(
390
+ 'task.pipeline-stage.max',
391
+ 'T12938',
392
+ 'tasks_tasks',
393
+ ['pipeline_stage'],
394
+ ['V21', 'C20', 'P01', 'F3'],
395
+ 'pipeline_stage is the maximum by STAGE_ORDER, except an explicit restore',
396
+ ),
397
+ rule(
398
+ 'task.verification.frozen-on-done',
399
+ 'T12939',
400
+ 'tasks_tasks',
401
+ ['verification_json'],
402
+ ['C01', 'C02', 'C03', 'C17'],
403
+ 'verification is frozen once done; otherwise merged per gate',
404
+ ),
405
+ rule(
406
+ 'session.status.terminal',
407
+ 'T12940',
408
+ 'tasks_sessions',
409
+ ['status', 'ended_at'],
410
+ ['S7', 'S8', 'S9', 'S10', 'S11', 'S17', 'S18', 'SC2', 'N1', 'SK5', 'P32'],
411
+ 'ended and orphaned are terminal except on an explicit resume',
412
+ ),
413
+ rule('audit.immutable', 'T12941', 'tasks_audit_log', ['*'], ['SA3'], 'audit rows are immutable', [
414
+ 'tasks_audit_log',
415
+ 'audit_log',
416
+ ]),
417
+ rule(
418
+ 'sticky.status.one-way',
419
+ 'T12942',
420
+ 'brain_sticky_notes',
421
+ ['status', 'converted_to_json'],
422
+ ['SK2', 'SK3', 'B24'],
423
+ 'active→{converted, archived} is one-way; converted_to_json is write-once',
424
+ ),
425
+ rule(
426
+ 'release.status.absorbing',
427
+ 'T12943',
428
+ 'tasks_releases',
429
+ ['status'],
430
+ ['REL-2', 'REL-3', 'REL-4', 'REL-5', 'REL-6', 'REL-7', 'REL-8'],
431
+ 'shipped release statuses are absorbing',
432
+ ),
433
+ rule(
434
+ 'playbook.approval.once',
435
+ 'T12944',
436
+ 'tasks_playbook_approvals',
437
+ ['status'],
438
+ ['PB-1', 'PB-4', 'PB-7', 'P33'],
439
+ 'an approval goes pending→decided once',
440
+ ),
441
+ rule(
442
+ 'docs.lifecycle.transitions',
443
+ 'T12945',
444
+ 'docs_attachments',
445
+ ['lifecycle_status'],
446
+ ['D04', 'D05', 'D31', 'D32', 'D39', 'D50'],
447
+ 'superseded is sticky; lifecycle transitions follow a matrix',
448
+ ),
449
+
450
+ // §3.6.0 identity-layer rows.
451
+ {
452
+ id: 'identity.local-minting',
453
+ class: 'identity-layer',
454
+ tables: ['tasks_tasks', 'tasks_sessions'],
455
+ sites: [],
456
+ inventory: ['V12', 'B07', 'B12'],
457
+ reason:
458
+ 'ids minted from local state (display ids, integer PKs, device-derived ids) collide across replicas; T12341 uids and re-minting own it, no validator port can',
459
+ },
460
+ ]);
@@ -65,8 +65,37 @@ export const NEXUS_ACCOUNT_ERROR_CODES = [
65
65
  'E_NEXUS_BUSY',
66
66
  /** Several accounts hold a device credential on one origin and none was named. */
67
67
  'E_NEXUS_ACCOUNT_AMBIGUOUS',
68
- /** An option that needs device credentials (`--read-only`) without `CLEO_NEXUS_DEVICE=1`. */
68
+ /** An option that needs device credentials (`--read-only`) with `CLEO_NEXUS_DEVICE=0`. */
69
69
  'E_NEXUS_DEVICE_REQUIRED',
70
+ /** `.cleo/nexus-link.json` is in a format this CLEO cannot update (written by a newer CLEO). */
71
+ 'E_NEXUS_LINK_FILE_UNSUPPORTED',
72
+ // Cloud vault (encrypted snapshots, T12336 / T12337 / T12338).
73
+ /** Another device holds this stream's write lease (`--force` takes it and labels a fork). */
74
+ 'E_NEXUS_VAULT_LEASE_HELD',
75
+ /** Another device pushed a newer snapshot than this machine last pushed or restored: pull first. */
76
+ 'E_NEXUS_VAULT_BEHIND',
77
+ /** Local data changed since the last push or restore; pulling would overwrite it. */
78
+ 'E_NEXUS_VAULT_LOCAL_CHANGES',
79
+ /** A snapshot's counts or hashes do not match its manifest; nothing was activated. */
80
+ 'E_NEXUS_VAULT_VERIFY_FAILED',
81
+ /** The account encryption key could not be obtained for this device. */
82
+ 'E_NEXUS_VAULT_KEY_UNAVAILABLE',
83
+ /** The project is not linked to Cleo Nexus (`cleo project link`). */
84
+ 'E_NEXUS_VAULT_NOT_LINKED',
85
+ /** The cloud holds no snapshot to restore. */
86
+ 'E_NEXUS_VAULT_EMPTY',
87
+ /** The server refused the snapshot (lineage or regression check). */
88
+ 'E_NEXUS_VAULT_REFUSED',
89
+ /**
90
+ * The stream moved past what this CLEO's vault writes (T13034): it takes only checkpoint/v3
91
+ * snapshots (the server's `E_STREAM_VERSION`), or it holds data of a newer sync schema.
92
+ * Pull, restore and verify still work; nothing was written.
93
+ */
94
+ 'E_NEXUS_VAULT_STREAM_UPGRADED',
95
+ /** Another CLEO process is writing to the store a restore would replace. */
96
+ 'E_NEXUS_VAULT_STORE_BUSY',
97
+ /** A project restore target already holds a different project. */
98
+ 'E_NEXUS_VAULT_TARGET_OCCUPIED',
70
99
  ] as const;
71
100
 
72
101
  /** One of {@link NEXUS_ACCOUNT_ERROR_CODES}. */
@@ -161,9 +190,9 @@ export interface NexusLoginResult {
161
190
  credentialsPath: string;
162
191
  /** Non-fatal problems, e.g. the account lookup failed after a good login. */
163
192
  warnings: string[];
164
- /** The enrolled device; present only with `CLEO_NEXUS_DEVICE=1` (device credentials). */
193
+ /** The enrolled device; present only with device credentials (the default; off with `CLEO_NEXUS_DEVICE=0`). */
165
194
  device?: NexusLoginDevice;
166
- /** Scopes of the stored device credential; present only with `CLEO_NEXUS_DEVICE=1`. */
195
+ /** Scopes of the stored device credential; present only with device credentials. */
167
196
  scopes?: string[];
168
197
  }
169
198
 
@@ -184,6 +213,56 @@ export interface NexusLogoutResult {
184
213
  warnings: string[];
185
214
  }
186
215
 
216
+ /**
217
+ * What happened to one device's sign-out (E9) or revoke (E10) in
218
+ * `cleo logout nexus [--revoke]` with device credentials (the default; `CLEO_NEXUS_DEVICE=0` turns them off) (cleo-nexus
219
+ * device contract §3.5, M3). Holds no secret.
220
+ *
221
+ * - `confirmed`: the server answered 200, or 401 `device-signed-out` /
222
+ * `device-revoked` (a revoke accepts only `device-revoked`);
223
+ * - `pending`: no usable answer (network, timeout, 429, 5xx, or a route the
224
+ * server does not have yet); the credentials stay in their slot and the
225
+ * next `cleo logout nexus`, `cleo login nexus` or cloud command retries;
226
+ * - `unconfirmed`: every credential held was refused as stale, so the CLI
227
+ * cannot finish it; the slot is kept, and a sign-out or revoke of the
228
+ * device on cleocode.dev lets the next retry confirm it;
229
+ * - `signed-out`: a revoke found the device already signed out (E10 401
230
+ * `device-signed-out`). Its credentials are dead, so the CLI cannot revoke
231
+ * it; the revoke slot is cleared (never retried) and the device must be
232
+ * revoked on cleocode.dev.
233
+ */
234
+ export type NexusDeviceEndOutcome = 'confirmed' | 'pending' | 'unconfirmed' | 'signed-out';
235
+
236
+ /** One device row of a {@link NexusDeviceLogoutResult}. */
237
+ export interface NexusDeviceLogoutRow {
238
+ /** Nexus user id of the entry. */
239
+ userId: string;
240
+ /** The device the request ended. */
241
+ deviceId: string;
242
+ /** `sign-out` (E9) or `revoke` (E10). */
243
+ action: 'sign-out' | 'revoke';
244
+ /** `true` for a replaced device's leftover request retried from `retired`. */
245
+ retired: boolean;
246
+ /** Server-side outcome. */
247
+ outcome: NexusDeviceEndOutcome;
248
+ /** `true` when a confirmed revoke removed the (origin, user) entry from `nexus-device.json`. */
249
+ removedLocally: boolean;
250
+ }
251
+
252
+ /** Result of `cleo logout nexus [--revoke]` with device credentials. Holds no secret. */
253
+ export interface NexusDeviceLogoutResult {
254
+ /** API origin. */
255
+ apiUrl: string;
256
+ /** `sign-out` or `revoke` (`--revoke`). */
257
+ action: 'sign-out' | 'revoke';
258
+ /** One row per device request settled or retried in this run, sorted by user then device. */
259
+ devices: NexusDeviceLogoutRow[];
260
+ /** The 9.24 session sign-out, when a leftover session was found for the origin. */
261
+ session: NexusLogoutResult | null;
262
+ /** Non-fatal problems: every non-`confirmed` row is named here. */
263
+ warnings: string[];
264
+ }
265
+
187
266
  /**
188
267
  * The binding of a local CLEO project to its Nexus registration, persisted in
189
268
  * the project's machine-local `.cleo/nexus-link.json`, keyed by API origin.
@@ -203,6 +282,31 @@ export interface NexusProjectLink {
203
282
  streamId: string;
204
283
  /** ISO time of the last successful link. */
205
284
  linkedAt: string;
285
+ /**
286
+ * This store's replica id as attached on the server (device contract
287
+ * §3.6, §3.7). A cache, not the source of truth: the store's active
288
+ * `_sync_replica` row is. Absent before the first device-credential link.
289
+ */
290
+ replicaId?: string;
291
+ /** The Nexus device id that attached {@link NexusProjectLink.replicaId}. */
292
+ nexusDeviceId?: string;
293
+ /** ISO time the replica was last attached or confirmed. */
294
+ attachedAt?: string;
295
+ }
296
+
297
+ /**
298
+ * What `cleo project link` did for this machine's copy of the project with a
299
+ * device credential (contract §3.6 steps 3 to 5). Holds no path or secret.
300
+ */
301
+ export interface NexusReplicaAttachment {
302
+ /** The store's replica id (UUIDv7), now attached on the server. */
303
+ replicaId: string;
304
+ /** The Nexus device that holds it. */
305
+ deviceId: string;
306
+ /** The retired replica id when this link rebound the store (a copy, or a re-enrolled device). */
307
+ reboundFrom: string | null;
308
+ /** Server time the presence report was received, or `null` when sending it failed (see warnings). */
309
+ presenceAt: string | null;
206
310
  }
207
311
 
208
312
  /** Result of `cleo project link`. */
@@ -217,4 +321,17 @@ export interface NexusProjectLinkResult {
217
321
  alreadyLinked: boolean;
218
322
  /** Absolute path of the local binding file. */
219
323
  linkPath: string;
324
+ /**
325
+ * The replica attach and presence report, with device credentials; `null`
326
+ * with the 9.24 session (`CLEO_NEXUS_DEVICE=0`), which cannot attach.
327
+ */
328
+ replica: NexusReplicaAttachment | null;
329
+ /**
330
+ * Why this machine's copy was not attached, when device mode tried and
331
+ * failed (for example `E_NEXUS_REPLICA_COPIED`); `null` otherwise. The same
332
+ * text is also in {@link NexusProjectLinkResult.warnings}.
333
+ */
334
+ attachError: { code: string; message: string; fix: string | null } | null;
335
+ /** Non-fatal problems (for example, presence could not be sent). */
336
+ warnings: string[];
220
337
  }