@cleocode/core 2026.4.11 → 2026.4.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.
Files changed (169) hide show
  1. package/dist/codebase-map/analyzers/architecture.d.ts.map +1 -1
  2. package/dist/codebase-map/analyzers/architecture.js +0 -1
  3. package/dist/codebase-map/analyzers/architecture.js.map +1 -1
  4. package/dist/conduit/local-transport.d.ts +18 -8
  5. package/dist/conduit/local-transport.d.ts.map +1 -1
  6. package/dist/conduit/local-transport.js +23 -13
  7. package/dist/conduit/local-transport.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +19 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +6 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.js +175 -68950
  16. package/dist/index.js.map +1 -7
  17. package/dist/init.d.ts +1 -2
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -2
  20. package/dist/init.js.map +1 -1
  21. package/dist/internal.d.ts +8 -3
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +13 -6
  24. package/dist/internal.js.map +1 -1
  25. package/dist/memory/learnings.d.ts +2 -2
  26. package/dist/memory/patterns.d.ts +6 -6
  27. package/dist/output.d.ts +32 -11
  28. package/dist/output.d.ts.map +1 -1
  29. package/dist/output.js +67 -67
  30. package/dist/output.js.map +1 -1
  31. package/dist/paths.js +80 -14
  32. package/dist/paths.js.map +1 -1
  33. package/dist/skills/dynamic-skill-generator.d.ts +0 -2
  34. package/dist/skills/dynamic-skill-generator.d.ts.map +1 -1
  35. package/dist/skills/dynamic-skill-generator.js.map +1 -1
  36. package/dist/store/agent-registry-accessor.d.ts +203 -12
  37. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  38. package/dist/store/agent-registry-accessor.js +618 -100
  39. package/dist/store/agent-registry-accessor.js.map +1 -1
  40. package/dist/store/api-key-kdf.d.ts +73 -0
  41. package/dist/store/api-key-kdf.d.ts.map +1 -0
  42. package/dist/store/api-key-kdf.js +84 -0
  43. package/dist/store/api-key-kdf.js.map +1 -0
  44. package/dist/store/cleanup-legacy.js +171 -0
  45. package/dist/store/cleanup-legacy.js.map +1 -0
  46. package/dist/store/conduit-sqlite.d.ts +184 -0
  47. package/dist/store/conduit-sqlite.d.ts.map +1 -0
  48. package/dist/store/conduit-sqlite.js +570 -0
  49. package/dist/store/conduit-sqlite.js.map +1 -0
  50. package/dist/store/global-salt.d.ts +78 -0
  51. package/dist/store/global-salt.d.ts.map +1 -0
  52. package/dist/store/global-salt.js +147 -0
  53. package/dist/store/global-salt.js.map +1 -0
  54. package/dist/store/migrate-signaldock-to-conduit.d.ts +81 -0
  55. package/dist/store/migrate-signaldock-to-conduit.d.ts.map +1 -0
  56. package/dist/store/migrate-signaldock-to-conduit.js +555 -0
  57. package/dist/store/migrate-signaldock-to-conduit.js.map +1 -0
  58. package/dist/store/nexus-sqlite.js +28 -3
  59. package/dist/store/nexus-sqlite.js.map +1 -1
  60. package/dist/store/signaldock-sqlite.d.ts +122 -19
  61. package/dist/store/signaldock-sqlite.d.ts.map +1 -1
  62. package/dist/store/signaldock-sqlite.js +401 -251
  63. package/dist/store/signaldock-sqlite.js.map +1 -1
  64. package/dist/store/sqlite-backup.js +122 -4
  65. package/dist/store/sqlite-backup.js.map +1 -1
  66. package/dist/system/backup.d.ts +0 -26
  67. package/dist/system/backup.d.ts.map +1 -1
  68. package/dist/system/runtime.d.ts +0 -2
  69. package/dist/system/runtime.d.ts.map +1 -1
  70. package/dist/system/runtime.js +3 -3
  71. package/dist/system/runtime.js.map +1 -1
  72. package/dist/tasks/add.d.ts +1 -1
  73. package/dist/tasks/add.d.ts.map +1 -1
  74. package/dist/tasks/add.js +98 -23
  75. package/dist/tasks/add.js.map +1 -1
  76. package/dist/tasks/complete.d.ts.map +1 -1
  77. package/dist/tasks/complete.js +4 -1
  78. package/dist/tasks/complete.js.map +1 -1
  79. package/dist/tasks/find.d.ts.map +1 -1
  80. package/dist/tasks/find.js +4 -1
  81. package/dist/tasks/find.js.map +1 -1
  82. package/dist/tasks/labels.d.ts.map +1 -1
  83. package/dist/tasks/labels.js +4 -1
  84. package/dist/tasks/labels.js.map +1 -1
  85. package/dist/tasks/relates.d.ts.map +1 -1
  86. package/dist/tasks/relates.js +16 -4
  87. package/dist/tasks/relates.js.map +1 -1
  88. package/dist/tasks/show.d.ts.map +1 -1
  89. package/dist/tasks/show.js +4 -1
  90. package/dist/tasks/show.js.map +1 -1
  91. package/dist/tasks/update.d.ts.map +1 -1
  92. package/dist/tasks/update.js +32 -6
  93. package/dist/tasks/update.js.map +1 -1
  94. package/dist/validation/engine.d.ts.map +1 -1
  95. package/dist/validation/engine.js +16 -4
  96. package/dist/validation/engine.js.map +1 -1
  97. package/dist/validation/param-utils.d.ts +5 -3
  98. package/dist/validation/param-utils.d.ts.map +1 -1
  99. package/dist/validation/param-utils.js +8 -6
  100. package/dist/validation/param-utils.js.map +1 -1
  101. package/dist/validation/protocols/_shared.d.ts.map +1 -1
  102. package/dist/validation/protocols/_shared.js +13 -6
  103. package/dist/validation/protocols/_shared.js.map +1 -1
  104. package/package.json +7 -7
  105. package/src/adapters/__tests__/manager.test.ts +0 -1
  106. package/src/codebase-map/analyzers/architecture.ts +0 -1
  107. package/src/conduit/__tests__/local-credential-flow.test.ts +20 -18
  108. package/src/conduit/__tests__/local-transport.test.ts +14 -12
  109. package/src/conduit/local-transport.ts +23 -13
  110. package/src/config.ts +0 -1
  111. package/src/errors.ts +24 -0
  112. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +2 -5
  113. package/src/init.ts +1 -2
  114. package/src/internal.ts +49 -2
  115. package/src/lifecycle/cant/lifecycle-rcasd.cant +133 -0
  116. package/src/memory/__tests__/engine-compat.test.ts +2 -2
  117. package/src/memory/__tests__/pipeline-manifest-sqlite.test.ts +4 -4
  118. package/src/observability/__tests__/index.test.ts +4 -4
  119. package/src/observability/__tests__/log-filter.test.ts +4 -4
  120. package/src/output.ts +73 -75
  121. package/src/sessions/__tests__/session-grade.integration.test.ts +1 -1
  122. package/src/sessions/__tests__/session-grade.test.ts +2 -2
  123. package/src/skills/__tests__/dynamic-skill-generator.test.ts +0 -2
  124. package/src/skills/dynamic-skill-generator.ts +0 -2
  125. package/src/store/__tests__/agent-registry-accessor.test.ts +807 -0
  126. package/src/store/__tests__/api-key-kdf.test.ts +113 -0
  127. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  128. package/src/store/__tests__/global-salt.test.ts +195 -0
  129. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  130. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  131. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  132. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  133. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  134. package/src/store/agent-registry-accessor.ts +847 -140
  135. package/src/store/api-key-kdf.ts +104 -0
  136. package/src/store/conduit-sqlite.ts +655 -0
  137. package/src/store/global-salt.ts +175 -0
  138. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  139. package/src/store/signaldock-sqlite.ts +431 -254
  140. package/src/store/sqlite-backup.ts +185 -10
  141. package/src/system/backup.ts +2 -62
  142. package/src/system/runtime.ts +4 -6
  143. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  144. package/src/tasks/add.ts +99 -9
  145. package/src/tasks/complete.ts +4 -1
  146. package/src/tasks/find.ts +4 -1
  147. package/src/tasks/labels.ts +4 -1
  148. package/src/tasks/relates.ts +16 -4
  149. package/src/tasks/show.ts +4 -1
  150. package/src/tasks/update.ts +32 -3
  151. package/src/validation/__tests__/error-hints.test.ts +97 -0
  152. package/src/validation/engine.ts +16 -1
  153. package/src/validation/param-utils.ts +10 -7
  154. package/src/validation/protocols/_shared.ts +14 -6
  155. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  156. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  157. package/src/validation/protocols/cant/consensus.cant +74 -0
  158. package/src/validation/protocols/cant/contribution.cant +82 -0
  159. package/src/validation/protocols/cant/decomposition.cant +92 -0
  160. package/src/validation/protocols/cant/implementation.cant +67 -0
  161. package/src/validation/protocols/cant/provenance.cant +88 -0
  162. package/src/validation/protocols/cant/release.cant +96 -0
  163. package/src/validation/protocols/cant/research.cant +66 -0
  164. package/src/validation/protocols/cant/specification.cant +67 -0
  165. package/src/validation/protocols/cant/testing.cant +88 -0
  166. package/src/validation/protocols/cant/validation.cant +65 -0
  167. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  168. package/templates/config.template.json +0 -1
  169. package/templates/global-config.template.json +0 -1
@@ -2,22 +2,36 @@
2
2
  * SQLite backup via VACUUM INTO with snapshot rotation.
3
3
  *
4
4
  * Produces self-contained, WAL-free copies of CLEO SQLite databases
5
- * (tasks.db, brain.db at project tier; nexus.db at global tier) into
6
- * `.cleo/backups/sqlite/` (project) or `$XDG_DATA_HOME/cleo/backups/sqlite/`
7
- * (global) with a configurable rotation limit. All errors are swallowed —
8
- * backup failure must never interrupt normal operation.
5
+ * (tasks.db, brain.db, conduit.db at project tier; nexus.db, signaldock.db at
6
+ * global tier) into `.cleo/backups/sqlite/` (project) or
7
+ * `$XDG_DATA_HOME/cleo/backups/sqlite/` (global) with a configurable rotation
8
+ * limit. Also provides raw-file backup for the global-salt binary (not SQLite).
9
+ * All errors are swallowed — backup failure must never interrupt normal operation.
9
10
  *
10
11
  * @task T4873
11
12
  * @task T5158 — extended to cover brain.db
12
13
  * @task T306 — extended to cover global-tier nexus.db (epic T299)
14
+ * @task T369 — extended to cover conduit.db (project), signaldock.db (global),
15
+ * and global-salt raw-file backup (epic T310)
13
16
  * @epic T4867
14
17
  */
15
18
 
16
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
19
+ import {
20
+ chmodSync,
21
+ copyFileSync,
22
+ existsSync,
23
+ mkdirSync,
24
+ readdirSync,
25
+ statSync,
26
+ unlinkSync,
27
+ } from 'node:fs';
17
28
  import { join } from 'node:path';
18
29
  import { getCleoDir, getCleoHome } from '../paths.js';
19
30
  import { getBrainNativeDb } from './brain-sqlite.js';
31
+ import { getConduitNativeDb } from './conduit-sqlite.js';
32
+ import { getGlobalSaltPath } from './global-salt.js';
20
33
  import { getNexusNativeDb } from './nexus-sqlite.js';
34
+ import { getGlobalSignaldockNativeDb } from './signaldock-sqlite.js';
21
35
  import { getNativeDb } from './sqlite.js';
22
36
 
23
37
  /** Maximum number of snapshots retained per database (oldest rotated out). */
@@ -46,11 +60,16 @@ interface SnapshotTarget {
46
60
 
47
61
  /**
48
62
  * Canonical list of snapshot targets. Ordering is insertion order — tasks.db
49
- * snapshots first (highest-value operational state), then brain.db.
63
+ * snapshots first (highest-value operational state), then brain.db, then
64
+ * conduit.db (project messaging state).
65
+ *
66
+ * @task T369
67
+ * @epic T310
50
68
  */
51
69
  const SNAPSHOT_TARGETS: SnapshotTarget[] = [
52
70
  { prefix: 'tasks', getDb: getNativeDb },
53
71
  { prefix: 'brain', getDb: getBrainNativeDb },
72
+ { prefix: 'conduit', getDb: getConduitNativeDb }, // Added T369 — project messaging DB
54
73
  ];
55
74
 
56
75
  /**
@@ -308,10 +327,16 @@ export function listSqliteBackupsAll(
308
327
  export type BackupScope = 'project' | 'global';
309
328
 
310
329
  /**
311
- * Registered global-tier snapshot targets. `signaldock` is reserved for T310
312
- * only `nexus` is active in v2026.4.11.
330
+ * Registered global-tier snapshot targets. Both `nexus` and `signaldock` are
331
+ * active as of T369 (epic T310).
332
+ *
333
+ * @task T369
334
+ * @epic T310
313
335
  */
314
- const GLOBAL_SNAPSHOT_TARGETS: SnapshotTarget[] = [{ prefix: 'nexus', getDb: getNexusNativeDb }];
336
+ const GLOBAL_SNAPSHOT_TARGETS: SnapshotTarget[] = [
337
+ { prefix: 'nexus', getDb: getNexusNativeDb },
338
+ { prefix: 'signaldock', getDb: getGlobalSignaldockNativeDb }, // Activated T369 — global agent registry
339
+ ];
315
340
 
316
341
  /**
317
342
  * Resolve the global-tier backup directory, creating it on first use.
@@ -333,12 +358,13 @@ function resolveGlobalBackupDir(cleoHomeOverride?: string): string {
333
358
  * Non-fatal: errors from any individual step are surfaced via the return value
334
359
  * but never thrown — a failed snapshot MUST NOT interrupt normal operation.
335
360
  *
336
- * @param dbName - Which global-tier DB to snapshot (`'nexus'`; `'signaldock'` reserved for T310)
361
+ * @param dbName - Which global-tier DB to snapshot (`'nexus'` or `'signaldock'`)
337
362
  * @param opts.rotation - Maximum retained snapshots per prefix (default 10)
338
363
  * @param opts.cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
339
364
  * @returns Object containing the new snapshot path and any rotated (deleted) file paths
340
365
  *
341
366
  * @task T306
367
+ * @task T369 — activated signaldock target (epic T310)
342
368
  * @epic T299
343
369
  * @why ADR-036 §Backup Mechanism requires VACUUM INTO rotation at the global tier;
344
370
  * nexus.db has zero backup coverage prior to v2026.4.11.
@@ -453,3 +479,152 @@ export function listGlobalSqliteBackups(
453
479
  return [];
454
480
  }
455
481
  }
482
+
483
+ // ============================================================================
484
+ // Global-salt raw-file backup (ADR-037 §5)
485
+ // @task T369
486
+ // @epic T310
487
+ // ============================================================================
488
+
489
+ /** Filename prefix for global-salt backup files. */
490
+ const GLOBAL_SALT_BACKUP_PREFIX = 'global-salt';
491
+
492
+ /** Regex matching global-salt backup filenames: `global-salt-YYYYMMDD-HHmmss`. */
493
+ const GLOBAL_SALT_BACKUP_PATTERN = /^global-salt-\d{8}-\d{6}$/;
494
+
495
+ /**
496
+ * Resolve the backup directory for global-salt files: `{cleoHome}/backups/`.
497
+ * Global-salt backups live directly under `backups/` (not `backups/sqlite/`)
498
+ * to make clear they are binary files, not SQLite databases.
499
+ */
500
+ function resolveGlobalSaltBackupDir(cleoHomeOverride?: string): string {
501
+ const base = cleoHomeOverride ?? getCleoHome();
502
+ return join(base, 'backups');
503
+ }
504
+
505
+ /**
506
+ * Rotate global-salt backup files: delete the oldest until fewer than
507
+ * {@link MAX_SNAPSHOTS} remain. Returns the paths of deleted files.
508
+ * Non-fatal on any filesystem error.
509
+ */
510
+ function rotateGlobalSaltBackups(backupDir: string): string[] {
511
+ const rotated: string[] = [];
512
+ try {
513
+ const files = readdirSync(backupDir)
514
+ .filter((f) => GLOBAL_SALT_BACKUP_PATTERN.test(f))
515
+ .map((f) => ({
516
+ name: f,
517
+ path: join(backupDir, f),
518
+ mtimeMs: statSync(join(backupDir, f)).mtimeMs,
519
+ }))
520
+ .sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
521
+
522
+ while (files.length >= MAX_SNAPSHOTS) {
523
+ const oldest = files.shift();
524
+ if (!oldest) break;
525
+ try {
526
+ unlinkSync(oldest.path);
527
+ rotated.push(oldest.path);
528
+ } catch {
529
+ // non-fatal rotation failure
530
+ }
531
+ }
532
+ } catch {
533
+ // non-fatal
534
+ }
535
+ return rotated;
536
+ }
537
+
538
+ /**
539
+ * Creates a raw-file backup of the global-salt binary at
540
+ * `${getCleoHome()}/backups/global-salt-YYYYMMDD-HHmmss` with `0o600`
541
+ * permissions. Rotates to {@link MAX_SNAPSHOTS} (10) copies, deleting the
542
+ * oldest when the limit is reached.
543
+ *
544
+ * Non-fatal: errors are swallowed — salt backup failure must never block cleo.
545
+ * Returns empty strings and no rotated paths on failure.
546
+ *
547
+ * @param opts.cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
548
+ * @returns Object with the new snapshot path and any rotated (deleted) file paths
549
+ *
550
+ * @task T369
551
+ * @epic T310
552
+ * @why ADR-037 §5 — global-salt is security-critical; losing it invalidates
553
+ * all API keys. Backup enables recovery from accidental deletion.
554
+ */
555
+ export async function backupGlobalSalt(opts?: {
556
+ cleoHomeOverride?: string;
557
+ }): Promise<{ snapshotPath: string; rotated: string[] }> {
558
+ try {
559
+ const cleoHome = opts?.cleoHomeOverride ?? getCleoHome();
560
+ const saltSourcePath = opts?.cleoHomeOverride
561
+ ? join(cleoHome, 'global-salt')
562
+ : getGlobalSaltPath();
563
+
564
+ if (!existsSync(saltSourcePath)) {
565
+ return { snapshotPath: '', rotated: [] };
566
+ }
567
+
568
+ const backupDir = resolveGlobalSaltBackupDir(opts?.cleoHomeOverride);
569
+ mkdirSync(backupDir, { recursive: true });
570
+
571
+ const rotated = rotateGlobalSaltBackups(backupDir);
572
+
573
+ const snapshotName = `${GLOBAL_SALT_BACKUP_PREFIX}-${formatTimestamp(new Date())}`;
574
+ const snapshotPath = join(backupDir, snapshotName);
575
+
576
+ copyFileSync(saltSourcePath, snapshotPath);
577
+ chmodSync(snapshotPath, 0o600);
578
+
579
+ return { snapshotPath, rotated };
580
+ } catch {
581
+ // non-fatal — backup failure must never interrupt normal operation
582
+ return { snapshotPath: '', rotated: [] };
583
+ }
584
+ }
585
+
586
+ /**
587
+ * A single entry returned by {@link listGlobalSaltBackups}.
588
+ *
589
+ * @task T369
590
+ * @epic T310
591
+ */
592
+ export interface GlobalSaltBackupEntry {
593
+ /** Backup filename, e.g. `global-salt-20260408-143022`. */
594
+ name: string;
595
+ /** Absolute path to the backup file. */
596
+ path: string;
597
+ /** File size in bytes (should be 32 for a valid global-salt). */
598
+ size: number;
599
+ /** Last-modified timestamp. */
600
+ mtime: Date;
601
+ }
602
+
603
+ /**
604
+ * List global-salt backup files from `$XDG_DATA_HOME/cleo/backups/`, sorted
605
+ * newest-first by mtime.
606
+ *
607
+ * Returns an empty array when the backup directory does not exist.
608
+ *
609
+ * @param cleoHomeOverride - Override `getCleoHome()` path (use in tests to target a tmp dir)
610
+ *
611
+ * @task T369
612
+ * @epic T310
613
+ */
614
+ export function listGlobalSaltBackups(cleoHomeOverride?: string): GlobalSaltBackupEntry[] {
615
+ try {
616
+ const backupDir = resolveGlobalSaltBackupDir(cleoHomeOverride);
617
+ if (!existsSync(backupDir)) return [];
618
+
619
+ return readdirSync(backupDir)
620
+ .filter((f) => GLOBAL_SALT_BACKUP_PATTERN.test(f))
621
+ .map((f) => {
622
+ const filePath = join(backupDir, f);
623
+ const s = statSync(filePath);
624
+ return { name: f, path: filePath, size: s.size, mtime: new Date(s.mtimeMs) };
625
+ })
626
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // newest first
627
+ } catch {
628
+ return [];
629
+ }
630
+ }
@@ -22,7 +22,6 @@
22
22
  *
23
23
  * @task T4783
24
24
  * @task T5158 — extended to use VACUUM INTO for .db files and atomicWrite for JSON
25
- * @task T306 — extended to support global-tier scope (nexus.db; epic T299)
26
25
  */
27
26
 
28
27
  import {
@@ -87,15 +86,6 @@ export interface BackupResult {
87
86
  type: string;
88
87
  /** Files that were successfully captured into this backup. */
89
88
  files: string[];
90
- /**
91
- * Global-tier snapshot results, populated when `opts.includeGlobal` is true.
92
- * Key is the DB name (e.g. `'nexus'`), value is the snapshot path or an
93
- * empty string when the DB was not initialized.
94
- *
95
- * @task T306
96
- * @epic T299
97
- */
98
- global?: Record<string, string>;
99
89
  }
100
90
 
101
91
  /** Result shape returned by {@link restoreBackup}. */
@@ -122,31 +112,12 @@ export interface RestoreResult {
122
112
  * when `safeSqliteSnapshot` asks for them. This makes the function
123
113
  * self-contained — callers do not need to pre-open the DBs.
124
114
  *
125
- * When `opts.includeGlobal` is true, also snapshots global-tier databases
126
- * (currently `nexus.db`) via {@link vacuumIntoGlobalBackup}. Global snapshots
127
- * are written to `$XDG_DATA_HOME/cleo/backups/sqlite/` and returned in
128
- * `result.global`.
129
- *
130
115
  * Async because opening the database engines requires async migration
131
116
  * reconciliation (ADR-012). The CLI dispatch layer awaits this result.
132
- *
133
- * @task T306
134
- * @epic T299
135
117
  */
136
118
  export async function createBackup(
137
119
  projectRoot: string,
138
- opts?: {
139
- type?: string;
140
- note?: string;
141
- /**
142
- * When true, also snapshot global-tier databases (nexus.db).
143
- * Defaults to false for backwards compatibility.
144
- *
145
- * @task T306
146
- * @epic T299
147
- */
148
- includeGlobal?: boolean;
149
- },
120
+ opts?: { type?: string; note?: string },
150
121
  ): Promise<BackupResult> {
151
122
  const cleoDir = join(projectRoot, '.cleo');
152
123
  const btype = opts?.type || 'snapshot';
@@ -241,38 +212,7 @@ export async function createBackup(
241
212
  // non-fatal
242
213
  }
243
214
 
244
- // Global-tier backup (nexus.db) only when explicitly requested.
245
- // @task T306 @epic T299
246
- const globalResults: Record<string, string> = {};
247
- if (opts?.includeGlobal) {
248
- // Ensure nexus.db is initialized so its native handle is available.
249
- try {
250
- const { getNexusDb } = await import('../store/nexus-sqlite.js');
251
- await getNexusDb();
252
- } catch {
253
- // nexus.db open failed — vacuumIntoGlobalBackup will return empty path
254
- }
255
- try {
256
- const { vacuumIntoGlobalBackup } = await import('../store/sqlite-backup.js');
257
- const nexusResult = await vacuumIntoGlobalBackup('nexus');
258
- globalResults['nexus'] = nexusResult.snapshotPath;
259
- } catch {
260
- // non-fatal — global backup failure must not block project backup
261
- globalResults['nexus'] = '';
262
- }
263
- }
264
-
265
- const result: BackupResult = {
266
- backupId,
267
- path: backupDir,
268
- timestamp,
269
- type: btype,
270
- files: backedUp,
271
- };
272
- if (opts?.includeGlobal) {
273
- result.global = globalResults;
274
- }
275
- return result;
215
+ return { backupId, path: backupDir, timestamp, type: btype, files: backedUp };
276
216
  }
277
217
 
278
218
  /** A single backup entry returned by listSystemBackups. */
@@ -35,8 +35,6 @@ export interface RuntimeDiagnostics {
35
35
  };
36
36
  naming: {
37
37
  cli: string;
38
- /** Legacy field. CLI dispatch only. */
39
- mcp: string;
40
38
  server: string;
41
39
  };
42
40
  node: string;
@@ -73,14 +71,14 @@ function detectFromDataRoot(dataRoot: string): RuntimeChannel | null {
73
71
  return null;
74
72
  }
75
73
 
76
- function getExpectedNaming(channel: RuntimeChannel): { cli: string; mcp: string; server: string } {
74
+ function getExpectedNaming(channel: RuntimeChannel): { cli: string; server: string } {
77
75
  switch (channel) {
78
76
  case 'dev':
79
- return { cli: 'cleo-dev', mcp: 'cli', server: 'cleo-dev' };
77
+ return { cli: 'cleo-dev', server: 'cleo-dev' };
80
78
  case 'beta':
81
- return { cli: 'cleo-beta', mcp: 'cli', server: 'cleo-beta' };
79
+ return { cli: 'cleo-beta', server: 'cleo-beta' };
82
80
  default:
83
- return { cli: 'cleo', mcp: 'cli', server: 'cleo' };
81
+ return { cli: 'cleo', server: 'cleo' };
84
82
  }
85
83
  }
86
84
 
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Regression tests: CleoError fix hints on task-layer throws.
3
+ *
4
+ * Each test invokes a core function with invalid input and asserts that
5
+ * the thrown CleoError carries:
6
+ * - options.fix — a non-empty string with a concrete recovery action
7
+ * - options.details.field — the specific field that failed
8
+ *
9
+ * @task T341
10
+ * @epic T335
11
+ */
12
+
13
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
14
+ import { CleoError } from '../../errors.js';
15
+ import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
16
+ import { resetDbState } from '../../store/sqlite.js';
17
+ import {
18
+ addTask,
19
+ validateDepends,
20
+ validateLabels,
21
+ validatePhaseFormat,
22
+ validateSize,
23
+ validateStatus,
24
+ validateTaskType,
25
+ validateTitle,
26
+ } from '../add.js';
27
+ import { findTasks } from '../find.js';
28
+ import { showLabelTasks } from '../labels.js';
29
+ import { showTask } from '../show.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Helper: assert a CleoError has fix + details
33
+ // ---------------------------------------------------------------------------
34
+
35
+ function assertErrorHints(
36
+ fn: () => unknown,
37
+ opts: { fixIncludes?: string; detailsField?: string },
38
+ ): void {
39
+ let caught: unknown;
40
+ try {
41
+ fn();
42
+ } catch (err) {
43
+ caught = err;
44
+ }
45
+ expect(caught).toBeInstanceOf(CleoError);
46
+ const e = caught as CleoError;
47
+ expect(e.fix).toBeTruthy();
48
+ if (opts.fixIncludes) {
49
+ expect(e.fix).toContain(opts.fixIncludes);
50
+ }
51
+ if (opts.detailsField) {
52
+ expect(e.details).toBeDefined();
53
+ expect(e.details!.field).toBe(opts.detailsField);
54
+ }
55
+ }
56
+
57
+ async function assertAsyncErrorHints(
58
+ fn: () => Promise<unknown>,
59
+ opts: { fixIncludes?: string; detailsField?: string },
60
+ ): Promise<void> {
61
+ let caught: unknown;
62
+ try {
63
+ await fn();
64
+ } catch (err) {
65
+ caught = err;
66
+ }
67
+ expect(caught).toBeInstanceOf(CleoError);
68
+ const e = caught as CleoError;
69
+ expect(e.fix).toBeTruthy();
70
+ if (opts.fixIncludes) {
71
+ expect(e.fix).toContain(opts.fixIncludes);
72
+ }
73
+ if (opts.detailsField) {
74
+ expect(e.details).toBeDefined();
75
+ expect(e.details!.field).toBe(opts.detailsField);
76
+ }
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Tests
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe('error-hints: validateTitle', () => {
84
+ it('empty title — fix mentions cleo add, field=title', () => {
85
+ assertErrorHints(() => validateTitle(''), {
86
+ fixIncludes: 'cleo add',
87
+ detailsField: 'title',
88
+ });
89
+ });
90
+
91
+ it('whitespace-only title — fix mentions cleo add, field=title', () => {
92
+ assertErrorHints(() => validateTitle(' '), {
93
+ fixIncludes: 'cleo add',
94
+ detailsField: 'title',
95
+ });
96
+ });
97
+
98
+ it('title too long — fix mentions 200, field=title, details has expected/actual', () => {
99
+ let caught: unknown;
100
+ try {
101
+ validateTitle('a'.repeat(201));
102
+ } catch (err) {
103
+ caught = err;
104
+ }
105
+ expect(caught).toBeInstanceOf(CleoError);
106
+ const e = caught as CleoError;
107
+ expect(e.fix).toContain('200');
108
+ expect(e.details).toBeDefined();
109
+ expect(e.details!.field).toBe('title');
110
+ expect(e.details!.expected).toBe(200);
111
+ expect(e.details!.actual).toBe(201);
112
+ });
113
+ });
114
+
115
+ describe('error-hints: validateStatus', () => {
116
+ it('invalid status — fix mentions --status, field=status', () => {
117
+ assertErrorHints(() => validateStatus('broken'), {
118
+ fixIncludes: '--status',
119
+ detailsField: 'status',
120
+ });
121
+ });
122
+ });
123
+
124
+ describe('error-hints: validateSize', () => {
125
+ it('invalid size — fix mentions --size, field=size', () => {
126
+ assertErrorHints(() => validateSize('giant'), {
127
+ fixIncludes: '--size',
128
+ detailsField: 'size',
129
+ });
130
+ });
131
+ });
132
+
133
+ describe('error-hints: validateTaskType', () => {
134
+ it('invalid type — fix mentions --type, field=type', () => {
135
+ assertErrorHints(() => validateTaskType('mega-task'), {
136
+ fixIncludes: '--type',
137
+ detailsField: 'type',
138
+ });
139
+ });
140
+ });
141
+
142
+ describe('error-hints: validateLabels', () => {
143
+ it('invalid label — fix mentions pattern, field=labels', () => {
144
+ assertErrorHints(() => validateLabels(['UPPERCASE']), {
145
+ fixIncludes: '^[a-z]',
146
+ detailsField: 'labels',
147
+ });
148
+ });
149
+ });
150
+
151
+ describe('error-hints: validatePhaseFormat', () => {
152
+ it('invalid phase — fix mentions pattern, field=phase', () => {
153
+ assertErrorHints(() => validatePhaseFormat('UPPER_CASE'), {
154
+ fixIncludes: '^[a-z]',
155
+ detailsField: 'phase',
156
+ });
157
+ });
158
+ });
159
+
160
+ describe('error-hints: validateDepends (sync helper)', () => {
161
+ it('invalid dep ID format — fix mentions T### format, field=depends', () => {
162
+ assertErrorHints(() => validateDepends(['invalid'], []), {
163
+ fixIncludes: 'T###',
164
+ detailsField: 'depends',
165
+ });
166
+ });
167
+
168
+ it('dep not found — fix mentions cleo find, field=depends', () => {
169
+ assertErrorHints(() => validateDepends(['T999'], []), {
170
+ fixIncludes: 'find',
171
+ detailsField: 'depends',
172
+ });
173
+ });
174
+ });
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Anti-hallucination throw (add.ts line ~436)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe('error-hints: anti-hallucination (title === description)', () => {
181
+ let env: TestDbEnv;
182
+
183
+ beforeEach(async () => {
184
+ resetDbState();
185
+ env = await createTestDb();
186
+ });
187
+
188
+ afterEach(async () => {
189
+ await env.cleanup();
190
+ });
191
+
192
+ it('throws with fix mentioning --desc, field=description', async () => {
193
+ await assertAsyncErrorHints(
194
+ () =>
195
+ addTask(
196
+ {
197
+ title: 'same text',
198
+ description: 'same text',
199
+ },
200
+ env.tempDir,
201
+ env.accessor,
202
+ ),
203
+ { fixIncludes: '--desc', detailsField: 'description' },
204
+ );
205
+ });
206
+ });
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // findTasks — query required
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('error-hints: findTasks (query required)', () => {
213
+ it('missing query — fix mentions cleo find, field=query', async () => {
214
+ await assertAsyncErrorHints(() => findTasks({}), {
215
+ fixIncludes: 'cleo find',
216
+ detailsField: 'query',
217
+ });
218
+ });
219
+ });
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // showTask — task ID required
223
+ // ---------------------------------------------------------------------------
224
+
225
+ describe('error-hints: showTask (id required)', () => {
226
+ it('empty id — fix mentions cleo show, field=taskId', async () => {
227
+ await assertAsyncErrorHints(() => showTask(''), {
228
+ fixIncludes: 'cleo show',
229
+ detailsField: 'taskId',
230
+ });
231
+ });
232
+ });
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // showLabelTasks — no tasks with label
236
+ // ---------------------------------------------------------------------------
237
+
238
+ describe('error-hints: showLabelTasks (label not found)', () => {
239
+ let env: TestDbEnv;
240
+
241
+ beforeEach(async () => {
242
+ resetDbState();
243
+ env = await createTestDb();
244
+ });
245
+
246
+ afterEach(async () => {
247
+ await env.cleanup();
248
+ });
249
+
250
+ it('no tasks with label — fix mentions cleo labels, field=label', async () => {
251
+ await assertAsyncErrorHints(
252
+ () => showLabelTasks('nonexistent-label', env.tempDir, env.accessor),
253
+ { fixIncludes: 'cleo labels', detailsField: 'label' },
254
+ );
255
+ });
256
+ });