@cleocode/core 2026.4.11 → 2026.4.13

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 (184) 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 +9 -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 +96 -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__/backup-crypto.test.ts +101 -0
  128. package/src/store/__tests__/backup-pack.test.ts +491 -0
  129. package/src/store/__tests__/backup-unpack.test.ts +298 -0
  130. package/src/store/__tests__/conduit-sqlite.test.ts +413 -0
  131. package/src/store/__tests__/global-salt.test.ts +195 -0
  132. package/src/store/__tests__/migrate-signaldock-to-conduit.test.ts +715 -0
  133. package/src/store/__tests__/regenerators.test.ts +234 -0
  134. package/src/store/__tests__/restore-conflict-report.test.ts +274 -0
  135. package/src/store/__tests__/restore-json-merge.test.ts +521 -0
  136. package/src/store/__tests__/signaldock-sqlite.test.ts +652 -0
  137. package/src/store/__tests__/sqlite-backup-global.test.ts +307 -3
  138. package/src/store/__tests__/sqlite-backup.test.ts +5 -1
  139. package/src/store/__tests__/t310-integration.test.ts +1150 -0
  140. package/src/store/__tests__/t310-readiness.test.ts +111 -0
  141. package/src/store/__tests__/t311-integration.test.ts +661 -0
  142. package/src/store/agent-registry-accessor.ts +847 -140
  143. package/src/store/api-key-kdf.ts +104 -0
  144. package/src/store/backup-crypto.ts +209 -0
  145. package/src/store/backup-pack.ts +739 -0
  146. package/src/store/backup-unpack.ts +583 -0
  147. package/src/store/conduit-sqlite.ts +655 -0
  148. package/src/store/global-salt.ts +175 -0
  149. package/src/store/migrate-signaldock-to-conduit.ts +669 -0
  150. package/src/store/regenerators.ts +243 -0
  151. package/src/store/restore-conflict-report.ts +317 -0
  152. package/src/store/restore-json-merge.ts +653 -0
  153. package/src/store/signaldock-sqlite.ts +431 -254
  154. package/src/store/sqlite-backup.ts +185 -10
  155. package/src/store/t310-readiness.ts +119 -0
  156. package/src/system/backup.ts +2 -62
  157. package/src/system/runtime.ts +4 -6
  158. package/src/tasks/__tests__/error-hints.test.ts +256 -0
  159. package/src/tasks/add.ts +99 -9
  160. package/src/tasks/complete.ts +4 -1
  161. package/src/tasks/find.ts +4 -1
  162. package/src/tasks/labels.ts +4 -1
  163. package/src/tasks/relates.ts +16 -4
  164. package/src/tasks/show.ts +4 -1
  165. package/src/tasks/update.ts +32 -3
  166. package/src/validation/__tests__/error-hints.test.ts +97 -0
  167. package/src/validation/engine.ts +16 -1
  168. package/src/validation/param-utils.ts +10 -7
  169. package/src/validation/protocols/_shared.ts +14 -6
  170. package/src/validation/protocols/cant/architecture-decision.cant +80 -0
  171. package/src/validation/protocols/cant/artifact-publish.cant +95 -0
  172. package/src/validation/protocols/cant/consensus.cant +74 -0
  173. package/src/validation/protocols/cant/contribution.cant +82 -0
  174. package/src/validation/protocols/cant/decomposition.cant +92 -0
  175. package/src/validation/protocols/cant/implementation.cant +67 -0
  176. package/src/validation/protocols/cant/provenance.cant +88 -0
  177. package/src/validation/protocols/cant/release.cant +96 -0
  178. package/src/validation/protocols/cant/research.cant +66 -0
  179. package/src/validation/protocols/cant/specification.cant +67 -0
  180. package/src/validation/protocols/cant/testing.cant +88 -0
  181. package/src/validation/protocols/cant/validation.cant +65 -0
  182. package/src/validation/protocols/protocols-markdown/decomposition.md +0 -4
  183. package/templates/config.template.json +0 -1
  184. package/templates/global-config.template.json +0 -1
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Dry-run JSON file generators for the three CLEO runtime state files.
3
+ *
4
+ * Returns the content that `cleo init` WOULD write on the target machine,
5
+ * capturing machine-local state (projectRoot, hostname, timestamps) without
6
+ * touching the filesystem.
7
+ *
8
+ * Used by T354's A/B regenerate-and-compare engine (ADR-038 §10): the "A"
9
+ * side of the comparison is always what a fresh init would produce locally.
10
+ *
11
+ * @task T352
12
+ * @epic T311
13
+ * @why ADR-038 §10 — A/B regenerate-and-compare needs the "A" side: what the
14
+ * JSON files would look like if freshly initialized on the target machine.
15
+ * Captures machine-local state (projectRoot, hostname, timestamps).
16
+ * @what Pure dry-run versions of the cleo init JSON file generators.
17
+ *
18
+ * DRIFT WARNING: These generators mirror the logic inside:
19
+ * - packages/core/src/scaffold.ts :: createDefaultConfig() (config.json)
20
+ * - packages/core/src/scaffold.ts :: ensureProjectInfo() (project-info.json)
21
+ * - packages/core/src/store/project-detect.ts :: detectProjectType() (project-context.json)
22
+ *
23
+ * If any of those functions change the shape or defaults of their generated
24
+ * files, this module MUST be updated to match. There is no runtime link — the
25
+ * similarity is maintained manually.
26
+ */
27
+
28
+ import { randomUUID } from 'node:crypto';
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join, resolve } from 'node:path';
31
+ import { generateProjectHash } from '../nexus/hash.js';
32
+ import { createDefaultConfig, getCleoVersion } from '../scaffold.js';
33
+ import { getSchemaVersion } from '../schema-management.js';
34
+ import { detectProjectType, type ProjectContext } from './project-detect.js';
35
+
36
+ /**
37
+ * Mirror of SQLITE_SCHEMA_VERSION from ./sqlite.ts.
38
+ *
39
+ * We do not import sqlite.ts here to avoid its module-level side effects
40
+ * (node:sqlite bootstrap via createRequire). If the canonical value in
41
+ * sqlite.ts changes, update this constant to match.
42
+ *
43
+ * Canonical source: packages/core/src/store/sqlite.ts :: SQLITE_SCHEMA_VERSION
44
+ */
45
+ const SQLITE_SCHEMA_VERSION_MIRROR = '2.0.0';
46
+
47
+ // ── Types ────────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * The result of a dry-run file generator.
51
+ *
52
+ * @typeParam T - The shape of the generated file content. Defaults to
53
+ * `Record<string, unknown>` when the exact shape is not statically known.
54
+ *
55
+ * @task T352
56
+ * @epic T311
57
+ */
58
+ export interface RegeneratedFile<T = Record<string, unknown>> {
59
+ /** The filename that `cleo init` would write. */
60
+ filename: 'config.json' | 'project-info.json' | 'project-context.json';
61
+ /** The parsed content that would be written to disk. */
62
+ content: T;
63
+ }
64
+
65
+ // ── Internal helpers ─────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Detect whether the given `projectRoot` is the CLEO contributor project.
69
+ * Mirrors the private `isCleoContributorProject()` helper in scaffold.ts.
70
+ *
71
+ * Returns `true` only if all three fingerprints match:
72
+ * 1. `src/dispatch/` directory exists
73
+ * 2. `src/core/` directory exists
74
+ * 3. `package.json` identifies as `@cleocode/cleo`
75
+ */
76
+ function isContributorProject(projectRoot: string): boolean {
77
+ const at = (p: string) => existsSync(join(projectRoot, p));
78
+ if (!at('src/dispatch') || !at('src/core')) return false;
79
+ try {
80
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8')) as {
81
+ name?: string;
82
+ };
83
+ return pkg.name === '@cleocode/cleo';
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ // ── Public API ───────────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Returns the `config.json` content that `cleo init` would write for
93
+ * `projectRoot` on the current machine.
94
+ *
95
+ * PURE — no disk writes. Reads package.json only to detect the contributor
96
+ * project fingerprint (same as `ensureConfig` in scaffold.ts).
97
+ *
98
+ * Mirrors: `packages/core/src/scaffold.ts :: ensureConfig()` and
99
+ * `packages/core/src/scaffold.ts :: createDefaultConfig()`.
100
+ *
101
+ * DRIFT: if `createDefaultConfig()` adds new keys, regenerateConfigJson must
102
+ * be updated to reflect the same structure. The coupling is intentional but
103
+ * manual — see the TSDoc on this file.
104
+ *
105
+ * @param projectRoot - Absolute or relative path to the project root.
106
+ * @returns A `RegeneratedFile` whose `content` matches a freshly written
107
+ * `config.json`.
108
+ */
109
+ export function regenerateConfigJson(projectRoot: string): RegeneratedFile {
110
+ const resolvedRoot = resolve(projectRoot);
111
+
112
+ // createDefaultConfig() is the single source of truth for the config shape.
113
+ const content = createDefaultConfig() as Record<string, unknown>;
114
+
115
+ // Conditionally append the contributor block — mirrors ensureConfig() logic.
116
+ if (isContributorProject(resolvedRoot)) {
117
+ content['contributor'] = {
118
+ isContributorProject: true,
119
+ devCli: 'cleo-dev',
120
+ verifiedAt: new Date().toISOString(),
121
+ };
122
+ }
123
+
124
+ return { filename: 'config.json', content };
125
+ }
126
+
127
+ /**
128
+ * Returns the `project-info.json` content that `cleo init` would write for
129
+ * `projectRoot` on the current machine.
130
+ *
131
+ * Captures machine-local fields:
132
+ * - `projectHash` — SHA-256 of the resolved absolute path (first 12 chars)
133
+ * - `projectId` — fresh UUID (volatile; each call produces a new value)
134
+ * - `lastUpdated` — current ISO timestamp (volatile)
135
+ * - `cleoVersion` — read from `@cleocode/core/package.json` at runtime
136
+ * - `schemas.*` — schema version strings from bundled JSON schema files
137
+ *
138
+ * PURE — no disk writes.
139
+ *
140
+ * Mirrors: `packages/core/src/scaffold.ts :: ensureProjectInfo()`.
141
+ *
142
+ * DRIFT: if `ensureProjectInfo` adds, removes, or renames fields in the
143
+ * written JSON, update this function to match.
144
+ *
145
+ * @param projectRoot - Absolute or relative path to the project root.
146
+ * @returns A `RegeneratedFile` whose `content` matches a freshly written
147
+ * `project-info.json`.
148
+ */
149
+ export function regenerateProjectInfoJson(projectRoot: string): RegeneratedFile {
150
+ const resolvedRoot = resolve(projectRoot);
151
+ const projectHash = generateProjectHash(resolvedRoot);
152
+ const cleoVersion = getCleoVersion();
153
+ const now = new Date().toISOString();
154
+
155
+ // Read schema versions using the synchronous helper — falls back to safe
156
+ // defaults when schema files are not available (e.g. fresh clone, test env).
157
+ const configSchemaVersion = getSchemaVersion('config.schema.json') ?? cleoVersion;
158
+ const projectContextSchemaVersion = getSchemaVersion('project-context.schema.json') ?? '1.0.0';
159
+
160
+ const content: Record<string, unknown> = {
161
+ $schema: './schemas/project-info.schema.json',
162
+ schemaVersion: '1.0.0',
163
+ projectId: randomUUID(),
164
+ projectHash,
165
+ cleoVersion,
166
+ lastUpdated: now,
167
+ schemas: {
168
+ config: configSchemaVersion,
169
+ sqlite: SQLITE_SCHEMA_VERSION_MIRROR,
170
+ projectContext: projectContextSchemaVersion,
171
+ },
172
+ injection: {
173
+ 'CLAUDE.md': null,
174
+ 'AGENTS.md': null,
175
+ 'GEMINI.md': null,
176
+ },
177
+ health: {
178
+ status: 'unknown',
179
+ lastCheck: null,
180
+ issues: [],
181
+ },
182
+ features: {
183
+ multiSession: false,
184
+ verification: false,
185
+ contextAlerts: false,
186
+ },
187
+ };
188
+
189
+ return { filename: 'project-info.json', content };
190
+ }
191
+
192
+ /**
193
+ * Returns the `project-context.json` content that `cleo init` would write for
194
+ * `projectRoot` on the current machine.
195
+ *
196
+ * Runs full project-type detection by inspecting the project directory:
197
+ * testing framework, build command, primary language, monorepo topology,
198
+ * file-naming conventions, and LLM hints. DOES NOT spawn child processes.
199
+ *
200
+ * PURE — no disk writes.
201
+ *
202
+ * Mirrors: `packages/core/src/scaffold.ts :: ensureProjectContext()` and
203
+ * `packages/core/src/store/project-detect.ts :: detectProjectType()`.
204
+ *
205
+ * DRIFT: if `detectProjectType()` changes its output shape, the downstream
206
+ * A/B compare engine (T354) will still work because it compares fields by
207
+ * name — but regenerateProjectContextJson may produce unexpected keys or
208
+ * miss new ones until updated.
209
+ *
210
+ * @param projectRoot - Absolute or relative path to the project root.
211
+ * @returns A `RegeneratedFile` whose `content` matches a freshly written
212
+ * `project-context.json`.
213
+ */
214
+ export function regenerateProjectContextJson(projectRoot: string): RegeneratedFile<ProjectContext> {
215
+ const resolvedRoot = resolve(projectRoot);
216
+ const content = detectProjectType(resolvedRoot);
217
+ return { filename: 'project-context.json', content };
218
+ }
219
+
220
+ /**
221
+ * Convenience wrapper that returns all three regenerated files in one call.
222
+ *
223
+ * Each generator runs independently; volatile fields in `project-info.json`
224
+ * (`projectId`, `lastUpdated`) will differ from a separate call to
225
+ * `regenerateProjectInfoJson`.
226
+ *
227
+ * @param projectRoot - Absolute or relative path to the project root.
228
+ * @returns Object containing all three `RegeneratedFile` results.
229
+ *
230
+ * @task T352
231
+ * @epic T311
232
+ */
233
+ export function regenerateAllJson(projectRoot: string): {
234
+ config: RegeneratedFile;
235
+ projectInfo: RegeneratedFile;
236
+ projectContext: RegeneratedFile<ProjectContext>;
237
+ } {
238
+ return {
239
+ config: regenerateConfigJson(projectRoot),
240
+ projectInfo: regenerateProjectInfoJson(projectRoot),
241
+ projectContext: regenerateProjectContextJson(projectRoot),
242
+ };
243
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Conflict report formatter for the T311 A/B JSON restore engine.
3
+ *
4
+ * Reads {@link JsonRestoreReport}[] (from T354) plus optional re-auth and
5
+ * schema-compatibility warnings, and emits the markdown report format
6
+ * defined in T311 spec §6.5.
7
+ *
8
+ * PURE FORMATTER — the only I/O in this module is inside
9
+ * {@link writeConflictReport}, which writes a single file via
10
+ * `fs.writeFileSync`. Everything else is a pure string transformation.
11
+ *
12
+ * @task T357
13
+ * @epic T311
14
+ * @why ADR-038 §10 — restore writes a markdown conflict report at
15
+ * .cleo/restore-conflicts.md so users (and downstream agents) can
16
+ * review classifications + resolve manual-review fields + finalize
17
+ * via `cleo restore finalize`.
18
+ * @what Pure formatter. Reads JsonRestoreReport[] from T354 + extra
19
+ * metadata (re-auth warnings, schema warnings) and emits markdown.
20
+ * @module restore-conflict-report
21
+ */
22
+
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+
26
+ import type { JsonRestoreReport } from './restore-json-merge.js';
27
+
28
+ // ============================================================================
29
+ // Public types
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Warning emitted when a signaldock.db agent was encrypted with the
34
+ * source machine's global-salt and therefore cannot be decrypted on the
35
+ * target machine.
36
+ *
37
+ * @task T357
38
+ * @epic T311
39
+ */
40
+ export interface ReauthWarning {
41
+ /** Canonical agent identifier, e.g. `"cleo-prime"`. */
42
+ agentId: string;
43
+ /** Human-readable reason the agent needs re-authentication. */
44
+ reason: string;
45
+ }
46
+
47
+ /**
48
+ * Warning emitted when a bundled database's schema version differs from
49
+ * the local schema version.
50
+ *
51
+ * @task T357
52
+ * @epic T311
53
+ */
54
+ export interface SchemaCompatWarning {
55
+ /** Database name without extension, e.g. `"brain"` or `"conduit"`. */
56
+ db: string;
57
+ /** Schema version string found in the bundle. */
58
+ bundleVersion: string;
59
+ /** Schema version string found in the local installation. */
60
+ localVersion: string;
61
+ /**
62
+ * Severity of the mismatch:
63
+ * - `older-bundle` — bundle schema is behind local; forward migration will
64
+ * run on first open.
65
+ * - `newer-bundle` — bundle schema is ahead of local; upgrading cleo is
66
+ * recommended before using the restored database.
67
+ */
68
+ severity: 'older-bundle' | 'newer-bundle';
69
+ }
70
+
71
+ /**
72
+ * All inputs required to build the `.cleo/restore-conflicts.md` report.
73
+ *
74
+ * @task T357
75
+ * @epic T311
76
+ */
77
+ export interface BuildConflictReportInput {
78
+ /** Per-file A/B comparison results produced by T354. */
79
+ reports: JsonRestoreReport[];
80
+ /** Filesystem path of the bundle file that was imported. */
81
+ bundlePath: string;
82
+ /** Machine fingerprint recorded in the bundle manifest (source machine). */
83
+ sourceMachineFingerprint: string;
84
+ /** Machine fingerprint of the local machine (target machine). */
85
+ targetMachineFingerprint: string;
86
+ /** Cleo version string of the importing installation, e.g. `"2026.4.13"`. */
87
+ cleoVersion: string;
88
+ /**
89
+ * Agent re-authentication warnings for agents whose credentials were
90
+ * encrypted with the source machine's global-salt.
91
+ * Omit or pass an empty array when no agents need re-auth.
92
+ */
93
+ reauthWarnings?: ReauthWarning[];
94
+ /**
95
+ * Schema version mismatch warnings for bundled databases.
96
+ * Omit or pass an empty array when all schemas match.
97
+ */
98
+ schemaWarnings?: SchemaCompatWarning[];
99
+ }
100
+
101
+ // ============================================================================
102
+ // Value formatting helpers
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Formats an arbitrary field value as a compact inline markdown literal.
107
+ *
108
+ * - `undefined` → `_(not present)_`
109
+ * - `null` → `` `null` ``
110
+ * - strings → `` `"value"` `` with interior double-quotes escaped
111
+ * - other → `` `JSON.stringify(value)` ``
112
+ *
113
+ * @param val - The value to format.
114
+ * @returns A markdown-formatted string representation.
115
+ */
116
+ function formatValue(val: unknown): string {
117
+ if (val === undefined) return '_(not present)_';
118
+ if (val === null) return '`null`';
119
+ if (typeof val === 'string') return '`"' + val.replace(/"/g, '\\"') + '"`';
120
+ return '`' + JSON.stringify(val) + '`';
121
+ }
122
+
123
+ // ============================================================================
124
+ // Per-file section renderer
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Renders the markdown section for a single {@link JsonRestoreReport}.
129
+ *
130
+ * Groups field classifications into three buckets:
131
+ * - **identical** — skipped (no conflict).
132
+ * - **resolved** — values differed but auto-resolution produced A or B.
133
+ * - **manual-review** — no safe auto-resolution; operator must decide.
134
+ *
135
+ * @param report - The comparison result for one JSON file.
136
+ * @returns Markdown string for the file section, without a trailing newline.
137
+ */
138
+ function renderReportSection(report: JsonRestoreReport): string {
139
+ const resolved = report.classifications.filter(
140
+ (c) => c.category !== 'identical' && (c.resolution === 'A' || c.resolution === 'B'),
141
+ );
142
+ const manual = report.classifications.filter((c) => c.resolution === 'manual-review');
143
+ const totalClassified = report.classifications.length;
144
+ const conflictCount = manual.length;
145
+
146
+ const lines: string[] = [];
147
+
148
+ lines.push(`## ${report.filename}`);
149
+ lines.push('');
150
+ lines.push(
151
+ `_${totalClassified} fields classified, ${conflictCount} conflict${conflictCount === 1 ? '' : 's'}._`,
152
+ );
153
+ lines.push('');
154
+
155
+ // Resolved section
156
+ if (resolved.length === 0) {
157
+ lines.push('_No resolved conflicts._');
158
+ } else {
159
+ lines.push('### Resolved (auto-applied)');
160
+ lines.push('');
161
+ for (const c of resolved) {
162
+ lines.push(`- \`${c.path}\``);
163
+ lines.push(` - Local (A): ${formatValue(c.local)}`);
164
+ lines.push(` - Imported (B): ${formatValue(c.imported)}`);
165
+ lines.push(` - Resolution: **${c.resolution}**`);
166
+ lines.push(` - Rationale: ${c.rationale}`);
167
+ }
168
+ }
169
+
170
+ lines.push('');
171
+
172
+ // Manual review section
173
+ if (manual.length === 0) {
174
+ lines.push('_No manual review needed._');
175
+ } else {
176
+ lines.push('### Manual review needed');
177
+ lines.push('');
178
+ for (const c of manual) {
179
+ lines.push(`- \`${c.path}\``);
180
+ lines.push(` - Local (A): ${formatValue(c.local)}`);
181
+ lines.push(` - Imported (B): ${formatValue(c.imported)}`);
182
+ lines.push(` - Resolution: **manual-review**`);
183
+ lines.push(` - Rationale: ${c.rationale}`);
184
+ lines.push(
185
+ ` - RESOLVED: (edit this line to set 'A', 'B', or a custom value, then run 'cleo restore finalize')`,
186
+ );
187
+ }
188
+ }
189
+
190
+ return lines.join('\n');
191
+ }
192
+
193
+ // ============================================================================
194
+ // Public API
195
+ // ============================================================================
196
+
197
+ /**
198
+ * Builds the complete markdown content for `.cleo/restore-conflicts.md`.
199
+ *
200
+ * The output follows the T311 spec §6.5 format:
201
+ * - Header with bundle metadata and timestamps
202
+ * - One `##` section per file in `input.reports`
203
+ * - Agent re-authentication section (or _None_ when empty)
204
+ * - Schema compatibility warnings section (or _None_ when empty)
205
+ * - Footer instruction for `cleo restore finalize`
206
+ *
207
+ * @task T357
208
+ * @epic T311
209
+ * @param input - All data required to render the report.
210
+ * @returns The full markdown string. Does NOT write to disk.
211
+ */
212
+ export function buildConflictReport(input: BuildConflictReportInput): string {
213
+ const {
214
+ reports,
215
+ bundlePath,
216
+ sourceMachineFingerprint,
217
+ targetMachineFingerprint,
218
+ cleoVersion,
219
+ reauthWarnings = [],
220
+ schemaWarnings = [],
221
+ } = input;
222
+
223
+ const restoredAt = new Date().toISOString();
224
+
225
+ const lines: string[] = [];
226
+
227
+ // ---- Header ----------------------------------------------------------------
228
+
229
+ lines.push('# T311 Import Conflict Report');
230
+ lines.push('');
231
+ lines.push(`**Source bundle**: ${bundlePath}`);
232
+ lines.push(`**Source machine**: ${sourceMachineFingerprint}`);
233
+ lines.push(`**Target machine**: ${targetMachineFingerprint}`);
234
+ lines.push(`**Restored at**: ${restoredAt}`);
235
+ lines.push(`**Cleo version**: ${cleoVersion}`);
236
+ lines.push('');
237
+ lines.push('---');
238
+ lines.push('');
239
+
240
+ // ---- Per-file sections -----------------------------------------------------
241
+
242
+ for (const report of reports) {
243
+ lines.push(renderReportSection(report));
244
+ lines.push('');
245
+ lines.push('---');
246
+ lines.push('');
247
+ }
248
+
249
+ // ---- Agent re-authentication section ---------------------------------------
250
+
251
+ lines.push('## Agent re-authentication required');
252
+ lines.push('');
253
+ if (reauthWarnings.length === 0) {
254
+ lines.push('_None_');
255
+ } else {
256
+ lines.push('The following agents in `signaldock.db` were encrypted with the source');
257
+ lines.push("machine's `global-salt` and cannot be decrypted on this machine. Run");
258
+ lines.push('`cleo agent auth <id>` to re-authenticate:');
259
+ lines.push('');
260
+ for (const w of reauthWarnings) {
261
+ lines.push(`- ${w.agentId}: ${w.reason}`);
262
+ }
263
+ }
264
+ lines.push('');
265
+ lines.push('---');
266
+ lines.push('');
267
+
268
+ // ---- Schema compatibility warnings section ---------------------------------
269
+
270
+ lines.push('## Schema compatibility warnings');
271
+ lines.push('');
272
+ if (schemaWarnings.length === 0) {
273
+ lines.push('_None_');
274
+ } else {
275
+ for (const w of schemaWarnings) {
276
+ lines.push(
277
+ `- \`${w.db}\`: schema version \`${w.bundleVersion}\` (local: \`${w.localVersion}\`)`,
278
+ );
279
+ if (w.severity === 'older-bundle') {
280
+ lines.push(' - Status: **older-bundle: forward migration will run on first open**');
281
+ } else {
282
+ lines.push(' - Status: **newer-bundle: upgrade cleo for full support**');
283
+ }
284
+ }
285
+ }
286
+ lines.push('');
287
+ lines.push('---');
288
+ lines.push('');
289
+
290
+ // ---- Footer ----------------------------------------------------------------
291
+
292
+ lines.push(
293
+ '_Run `cleo restore finalize` after editing manual-review resolutions above to apply._',
294
+ );
295
+
296
+ return lines.join('\n');
297
+ }
298
+
299
+ /**
300
+ * Writes the conflict report markdown to
301
+ * `<projectRoot>/.cleo/restore-conflicts.md`.
302
+ *
303
+ * Creates the `.cleo/` directory if it does not already exist.
304
+ *
305
+ * @task T357
306
+ * @epic T311
307
+ * @param projectRoot - Absolute path to the project root directory.
308
+ * @param content - Markdown string produced by {@link buildConflictReport}.
309
+ * @returns The absolute path of the written file.
310
+ */
311
+ export function writeConflictReport(projectRoot: string, content: string): string {
312
+ const cleoDir = path.join(projectRoot, '.cleo');
313
+ fs.mkdirSync(cleoDir, { recursive: true });
314
+ const filePath = path.join(cleoDir, 'restore-conflicts.md');
315
+ fs.writeFileSync(filePath, content, 'utf-8');
316
+ return filePath;
317
+ }