@cleocode/core 2026.8.9 → 2026.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/conduit/conduit-client.d.ts.map +1 -1
  2. package/dist/conduit/conduit-client.js +10 -2
  3. package/dist/conduit/conduit-client.js.map +1 -1
  4. package/dist/dispatch/mvi-projection.d.ts +38 -0
  5. package/dist/dispatch/mvi-projection.d.ts.map +1 -1
  6. package/dist/dispatch/mvi-projection.js +189 -11
  7. package/dist/dispatch/mvi-projection.js.map +1 -1
  8. package/dist/docs/export-document.js +41 -3
  9. package/dist/docs/export-document.js.map +2 -2
  10. package/dist/doctor/acceptance-drift.d.ts +258 -0
  11. package/dist/doctor/acceptance-drift.d.ts.map +1 -0
  12. package/dist/doctor/acceptance-drift.js +366 -0
  13. package/dist/doctor/acceptance-drift.js.map +1 -0
  14. package/dist/doctor/index.d.ts +4 -0
  15. package/dist/doctor/index.d.ts.map +1 -1
  16. package/dist/doctor/index.js +2 -0
  17. package/dist/doctor/index.js.map +1 -1
  18. package/dist/doctor/legacy-reaper.d.ts +105 -0
  19. package/dist/doctor/legacy-reaper.d.ts.map +1 -0
  20. package/dist/doctor/legacy-reaper.js +151 -0
  21. package/dist/doctor/legacy-reaper.js.map +1 -0
  22. package/dist/doctor/malformed-task-ids.d.ts +111 -0
  23. package/dist/doctor/malformed-task-ids.d.ts.map +1 -0
  24. package/dist/doctor/malformed-task-ids.js +139 -0
  25. package/dist/doctor/malformed-task-ids.js.map +1 -0
  26. package/dist/doctor/nexus-schema-residency.d.ts +115 -0
  27. package/dist/doctor/nexus-schema-residency.d.ts.map +1 -0
  28. package/dist/doctor/nexus-schema-residency.js +178 -0
  29. package/dist/doctor/nexus-schema-residency.js.map +1 -0
  30. package/dist/error-registry.d.ts.map +1 -1
  31. package/dist/error-registry.js +18 -0
  32. package/dist/error-registry.js.map +1 -1
  33. package/dist/internal.d.ts +1 -0
  34. package/dist/internal.d.ts.map +1 -1
  35. package/dist/internal.js +1 -0
  36. package/dist/internal.js.map +1 -1
  37. package/dist/llm/ai-sdk-warnings.d.ts +55 -0
  38. package/dist/llm/ai-sdk-warnings.d.ts.map +1 -0
  39. package/dist/llm/ai-sdk-warnings.js +83 -0
  40. package/dist/llm/ai-sdk-warnings.js.map +1 -0
  41. package/dist/llm/model-runner.d.ts +0 -8
  42. package/dist/llm/model-runner.d.ts.map +1 -1
  43. package/dist/llm/model-runner.js +8 -0
  44. package/dist/llm/model-runner.js.map +1 -1
  45. package/dist/llm/plugin-facade.js +73 -8
  46. package/dist/llm/plugin-facade.js.map +3 -3
  47. package/dist/memory/embedding-queue.d.ts +12 -0
  48. package/dist/memory/embedding-queue.d.ts.map +1 -1
  49. package/dist/memory/embedding-queue.js +39 -5
  50. package/dist/memory/embedding-queue.js.map +1 -1
  51. package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
  52. package/dist/orchestrate/worktree-complete.js +26 -0
  53. package/dist/orchestrate/worktree-complete.js.map +1 -1
  54. package/dist/output.d.ts.map +1 -1
  55. package/dist/output.js +25 -0
  56. package/dist/output.js.map +1 -1
  57. package/dist/release/pr-evidence.d.ts +13 -0
  58. package/dist/release/pr-evidence.d.ts.map +1 -1
  59. package/dist/release/pr-evidence.js +54 -1
  60. package/dist/release/pr-evidence.js.map +1 -1
  61. package/dist/render/session/briefing.d.ts.map +1 -1
  62. package/dist/render/session/briefing.js +9 -0
  63. package/dist/render/session/briefing.js.map +1 -1
  64. package/dist/selfimprove/fix-gen.js +73 -8
  65. package/dist/selfimprove/fix-gen.js.map +3 -3
  66. package/dist/sessions/briefing.d.ts +13 -0
  67. package/dist/sessions/briefing.d.ts.map +1 -1
  68. package/dist/sessions/briefing.js +64 -7
  69. package/dist/sessions/briefing.js.map +1 -1
  70. package/dist/sessions/handoff.d.ts +32 -4
  71. package/dist/sessions/handoff.d.ts.map +1 -1
  72. package/dist/sessions/handoff.js +7 -7
  73. package/dist/sessions/handoff.js.map +1 -1
  74. package/dist/shutdown-deadline.d.ts +144 -0
  75. package/dist/shutdown-deadline.d.ts.map +1 -0
  76. package/dist/shutdown-deadline.js +187 -0
  77. package/dist/shutdown-deadline.js.map +1 -0
  78. package/dist/shutdown.d.ts +5 -2
  79. package/dist/shutdown.d.ts.map +1 -1
  80. package/dist/shutdown.js +19 -14
  81. package/dist/shutdown.js.map +1 -1
  82. package/dist/spawn/branch-lock.d.ts.map +1 -1
  83. package/dist/spawn/branch-lock.js +33 -0
  84. package/dist/spawn/branch-lock.js.map +1 -1
  85. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  86. package/dist/store/sqlite-data-accessor.js +14 -0
  87. package/dist/store/sqlite-data-accessor.js.map +1 -1
  88. package/dist/store/tasks-sqlite.d.ts.map +1 -1
  89. package/dist/store/tasks-sqlite.js +16 -1
  90. package/dist/store/tasks-sqlite.js.map +1 -1
  91. package/dist/tasks/ac-immutability.d.ts.map +1 -1
  92. package/dist/tasks/ac-immutability.js +36 -0
  93. package/dist/tasks/ac-immutability.js.map +1 -1
  94. package/dist/tasks/add.d.ts +18 -0
  95. package/dist/tasks/add.d.ts.map +1 -1
  96. package/dist/tasks/add.js +28 -5
  97. package/dist/tasks/add.js.map +1 -1
  98. package/dist/tasks/axis-filters.d.ts +53 -0
  99. package/dist/tasks/axis-filters.d.ts.map +1 -0
  100. package/dist/tasks/axis-filters.js +77 -0
  101. package/dist/tasks/axis-filters.js.map +1 -0
  102. package/dist/tasks/complete.d.ts.map +1 -1
  103. package/dist/tasks/complete.js +10 -1
  104. package/dist/tasks/complete.js.map +1 -1
  105. package/dist/tasks/duplicate-detector.d.ts +67 -14
  106. package/dist/tasks/duplicate-detector.d.ts.map +1 -1
  107. package/dist/tasks/duplicate-detector.js +197 -54
  108. package/dist/tasks/duplicate-detector.js.map +1 -1
  109. package/dist/tasks/evidence.d.ts +139 -0
  110. package/dist/tasks/evidence.d.ts.map +1 -1
  111. package/dist/tasks/evidence.js +294 -25
  112. package/dist/tasks/evidence.js.map +1 -1
  113. package/dist/tasks/find.d.ts.map +1 -1
  114. package/dist/tasks/find.js +9 -2
  115. package/dist/tasks/find.js.map +1 -1
  116. package/dist/tasks/gate-runner.d.ts.map +1 -1
  117. package/dist/tasks/gate-runner.js +25 -4
  118. package/dist/tasks/gate-runner.js.map +1 -1
  119. package/dist/tasks/heavy-tool-env.d.ts +20 -0
  120. package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
  121. package/dist/tasks/heavy-tool-env.js +74 -4
  122. package/dist/tasks/heavy-tool-env.js.map +1 -1
  123. package/dist/tasks/heavy-tool-limit.d.ts +134 -0
  124. package/dist/tasks/heavy-tool-limit.d.ts.map +1 -0
  125. package/dist/tasks/heavy-tool-limit.js +196 -0
  126. package/dist/tasks/heavy-tool-limit.js.map +1 -0
  127. package/dist/tasks/index.d.ts +1 -1
  128. package/dist/tasks/index.d.ts.map +1 -1
  129. package/dist/tasks/index.js +1 -1
  130. package/dist/tasks/index.js.map +1 -1
  131. package/dist/tasks/infer-add-params.d.ts +28 -0
  132. package/dist/tasks/infer-add-params.d.ts.map +1 -1
  133. package/dist/tasks/infer-add-params.js +96 -6
  134. package/dist/tasks/infer-add-params.js.map +1 -1
  135. package/dist/tasks/list.d.ts +30 -1
  136. package/dist/tasks/list.d.ts.map +1 -1
  137. package/dist/tasks/list.js +14 -0
  138. package/dist/tasks/list.js.map +1 -1
  139. package/dist/tasks/ops.d.ts +4 -0
  140. package/dist/tasks/ops.d.ts.map +1 -1
  141. package/dist/tasks/ops.js +2 -0
  142. package/dist/tasks/ops.js.map +1 -1
  143. package/dist/tasks/session-scope.d.ts +51 -4
  144. package/dist/tasks/session-scope.d.ts.map +1 -1
  145. package/dist/tasks/session-scope.js +68 -10
  146. package/dist/tasks/session-scope.js.map +1 -1
  147. package/dist/tasks/tool-cache.d.ts +116 -9
  148. package/dist/tasks/tool-cache.d.ts.map +1 -1
  149. package/dist/tasks/tool-cache.js +127 -16
  150. package/dist/tasks/tool-cache.js.map +1 -1
  151. package/dist/tasks/tool-semaphore.d.ts +91 -0
  152. package/dist/tasks/tool-semaphore.d.ts.map +1 -1
  153. package/dist/tasks/tool-semaphore.js +203 -14
  154. package/dist/tasks/tool-semaphore.js.map +1 -1
  155. package/dist/validation/doctor/checks.d.ts +64 -0
  156. package/dist/validation/doctor/checks.d.ts.map +1 -1
  157. package/dist/validation/doctor/checks.js +249 -0
  158. package/dist/validation/doctor/checks.js.map +1 -1
  159. package/dist/validation/engine-ops.d.ts.map +1 -1
  160. package/dist/validation/engine-ops.js +41 -5
  161. package/dist/validation/engine-ops.js.map +1 -1
  162. package/package.json +13 -13
  163. package/templates/CLEO-INJECTION.md +28 -44
  164. package/dist/backfill/adr-backfill-walker.d.ts +0 -57
  165. package/dist/backfill/adr-backfill-walker.d.ts.map +0 -1
  166. package/dist/backfill/adr-backfill-walker.js +0 -631
  167. package/dist/backfill/adr-backfill-walker.js.map +0 -1
  168. package/dist/tasks/graph-rag.d.ts +0 -49
  169. package/dist/tasks/graph-rag.d.ts.map +0 -1
  170. package/dist/tasks/graph-rag.js +0 -328
  171. package/dist/tasks/graph-rag.js.map +0 -1
@@ -1,631 +0,0 @@
1
- /**
2
- * ADR Backfill Walker — T1829
3
- *
4
- * One-time idempotent script that parses every `.cleo/adrs/*.md` (and the 9
5
- * unique ADRs in `docs/adr/`) and creates a `brain_decisions` row for each one.
6
- *
7
- * ## Usage
8
- *
9
- * ```bash
10
- * # Dry-run (no writes):
11
- * node packages/core/dist/tools/adr-backfill-walker.js --dry-run
12
- *
13
- * # Apply (writes to brain.db):
14
- * node packages/core/dist/tools/adr-backfill-walker.js --apply
15
- * ```
16
- *
17
- * ## Idempotency
18
- *
19
- * Rows are skipped when an existing `brain_decisions` row already has
20
- * `adrNumber` populated for that ADR number. The check is transactional:
21
- * `SELECT … WHERE adr_number = ?`.
22
- *
23
- * ## Collisions (ADR-051..054)
24
- *
25
- * The four ADR numbers 051–054 each have two conflicting files in two
26
- * different directories. This walker uses `.cleo/adrs/` as the canonical
27
- * SSoT (per Epsilon audit decision) and SKIPS the `docs/adr/` versions.
28
- * A note is written to the report for each skipped collision.
29
- *
30
- * ## Duplicates (ADR-031/033, ADR-032/034)
31
- *
32
- * ADR-033 duplicates ADR-031 and ADR-034 duplicates ADR-032. The first
33
- * number is kept as canonical; the second is inserted with
34
- * `supersededBy = <first>` and `confirmationState = 'superseded'`.
35
- *
36
- * @task T1829
37
- * @epic T1824
38
- */
39
- import { existsSync } from 'node:fs';
40
- import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
41
- import { basename, join, relative } from 'node:path';
42
- // ─── Config ───────────────────────────────────────────────────────────────────
43
- /**
44
- * ADR numbers in the collision zone (051–054).
45
- * `.cleo/adrs/` versions are canonical; `docs/adr/` versions are skipped.
46
- */
47
- const COLLISION_ADR_NUMBERS = new Set([51, 52, 53, 54]);
48
- /**
49
- * Exact-duplicate pair map: secondary ADR → canonical ADR.
50
- * The secondary ADR will be inserted with `supersededBy = canonical`.
51
- */
52
- const DUPLICATE_PAIRS = {
53
- 33: 31, // ADR-033 duplicates ADR-031
54
- 34: 32, // ADR-034 duplicates ADR-032
55
- };
56
- /**
57
- * Extract ADR number from a filename like `ADR-031-foo.md`.
58
- * Returns -1 if no match.
59
- */
60
- function extractAdrNumberFromFilename(filename) {
61
- const m = filename.match(/^ADR-(\d{3})/i);
62
- return m ? parseInt(m[1], 10) : -1;
63
- }
64
- /**
65
- * Parse the frontmatter and first heading of an ADR markdown file.
66
- *
67
- * Supports two formats:
68
- * 1. YAML frontmatter (---…---)
69
- * 2. Markdown bold-field format (**Status**: …)
70
- *
71
- * Falls back to sensible defaults when fields are absent.
72
- */
73
- function parseAdrFrontmatter(content, filename) {
74
- const adrNumber = extractAdrNumberFromFilename(filename);
75
- let title = filename.replace(/\.md$/, '');
76
- let status = 'accepted';
77
- let date;
78
- let supersedesAdrNumber;
79
- let supersededByAdrNumber;
80
- // ── YAML frontmatter (--- ... ---) ──────────────────────────────────────────
81
- const yamlMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
82
- if (yamlMatch) {
83
- const yamlBlock = yamlMatch[1];
84
- // title
85
- const titleLine = yamlBlock.match(/^title:\s*(.+)$/m);
86
- if (titleLine)
87
- title = titleLine[1].trim().replace(/^['"]|['"]$/g, '');
88
- // status
89
- const statusLine = yamlBlock.match(/^status:\s*(.+)$/m);
90
- if (statusLine)
91
- status = statusLine[1].trim().toLowerCase();
92
- // date
93
- const dateLine = yamlBlock.match(/^(?:created|date|updated):\s*(.+)$/m);
94
- if (dateLine)
95
- date = dateLine[1].trim();
96
- // supersedes: ADR-NNN or just NNN
97
- const supersedesLine = yamlBlock.match(/^supersedes:\s*(.+)$/im);
98
- if (supersedesLine) {
99
- const val = supersedesLine[1].trim();
100
- if (val !== '~' && val !== 'null' && val !== '') {
101
- const m = val.match(/(\d{3})/);
102
- if (m)
103
- supersedesAdrNumber = parseInt(m[1], 10);
104
- }
105
- }
106
- // superseded-by
107
- const supersededByLine = yamlBlock.match(/^superseded.by:\s*(.+)$/im);
108
- if (supersededByLine) {
109
- const val = supersededByLine[1].trim();
110
- if (val !== '~' && val !== 'null' && val !== '') {
111
- const m = val.match(/(\d{3})/);
112
- if (m)
113
- supersededByAdrNumber = parseInt(m[1], 10);
114
- }
115
- }
116
- }
117
- // ── Markdown heading extraction (after possible YAML block) ─────────────────
118
- // Find first H1 or H2 heading for a better title
119
- const headingMatch = content.match(/^#+\s+(.+)$/m);
120
- if (headingMatch && (title === filename.replace(/\.md$/, '') || yamlMatch)) {
121
- // Only override with heading if we got a title from YAML that looks synthetic,
122
- // or if there was no YAML title. Use heading as secondary source.
123
- if (!yamlMatch?.toString().includes('title:')) {
124
- title = headingMatch[1].trim();
125
- }
126
- }
127
- // ── Markdown bold-field format: **Status**: ... ──────────────────────────────
128
- if (!yamlMatch) {
129
- // status
130
- const boldStatus = content.match(/\*\*[Ss]tatus\*\*\s*:?\s*(.+?)(?:\n|$)/m);
131
- if (boldStatus) {
132
- // Strip markdown formatting and extra notes
133
- status = boldStatus[1].replace(/\*+/g, '').split(/[·(,]/)[0].trim().toLowerCase();
134
- }
135
- // date
136
- const boldDate = content.match(/\*\*[Dd]ate\*\*\s*:?\s*(.+?)(?:\n|$)/m);
137
- if (boldDate)
138
- date = boldDate[1].replace(/\*+/g, '').trim();
139
- // supersedes (prose / bold)
140
- const supersedesMatch = content.match(/[Ss]upersedes[:\s]+(?:ADR-)?(\d{3})/g);
141
- if (supersedesMatch) {
142
- const m = supersedesMatch[0].match(/(\d{3})/);
143
- if (m)
144
- supersedesAdrNumber = parseInt(m[1], 10);
145
- }
146
- // superseded-by
147
- const supersededByMatch = content.match(/[Ss]uperseded[- ][Bb]y[:\s]+(?:ADR-)?(\d{3})/g);
148
- if (supersededByMatch) {
149
- const m = supersededByMatch[0].match(/(\d{3})/);
150
- if (m)
151
- supersededByAdrNumber = parseInt(m[1], 10);
152
- }
153
- // Get title from first heading
154
- const h1 = content.match(/^#\s+(.+)$/m);
155
- const h2 = content.match(/^##\s+(.+)$/m);
156
- if (h1)
157
- title = h1[1].trim();
158
- else if (h2)
159
- title = h2[1].trim();
160
- }
161
- // Normalise status
162
- const statusLower = status.toLowerCase();
163
- if (statusLower.includes('accept'))
164
- status = 'accepted';
165
- else if (statusLower.includes('supersed'))
166
- status = 'superseded';
167
- else if (statusLower.includes('reject'))
168
- status = 'rejected';
169
- else if (statusLower.includes('archive'))
170
- status = 'archived';
171
- else if (statusLower.includes('draft'))
172
- status = 'draft';
173
- else if (statusLower.includes('propos'))
174
- status = 'proposed';
175
- else
176
- status = 'accepted'; // default for established ADRs
177
- return { adrNumber, title, status, date, supersedesAdrNumber, supersededByAdrNumber };
178
- }
179
- /**
180
- * Build a concise decision text and rationale from an ADR.
181
- * Uses the title + first non-heading paragraph from the body as rationale.
182
- */
183
- function buildDecisionText(title, content) {
184
- const decision = title.slice(0, 250);
185
- // Strip YAML frontmatter if present
186
- let body = content.replace(/^---[\s\S]*?---\r?\n/, '');
187
- // Strip headings
188
- body = body.replace(/^#+\s+.+$/gm, '').trim();
189
- // Strip bold-field metadata lines like **Status**: ...
190
- body = body.replace(/^\*\*[^*]+\*\*\s*:.*$/gm, '').trim();
191
- // Strip horizontal rules
192
- body = body.replace(/^---+$/gm, '').trim();
193
- // Find first non-empty paragraph
194
- const paragraphs = body
195
- .split(/\n{2,}/)
196
- .map((p) => p.trim())
197
- .filter(Boolean);
198
- const rationale = paragraphs.length > 0
199
- ? paragraphs[0].replace(/\n/g, ' ').replace(/\s+/g, ' ').slice(0, 500)
200
- : `ADR file: ${title}`;
201
- return { decision, rationale: rationale || `Architectural decision: ${title}` };
202
- }
203
- // ─── Database helpers ─────────────────────────────────────────────────────────
204
- /** Check if a brain_decisions row with this adrNumber already exists. */
205
- async function rowExistsForAdrNumber(projectRoot, adrNumber) {
206
- const { getBrainDb } = await import('../store/memory-sqlite.js');
207
- const { brainDecisions } = await import('../store/schema/memory-schema.js');
208
- const { eq } = await import('drizzle-orm');
209
- const db = await getBrainDb(projectRoot);
210
- const rows = await db
211
- .select({ id: brainDecisions.id })
212
- .from(brainDecisions)
213
- .where(eq(brainDecisions.adrNumber, adrNumber))
214
- .limit(1);
215
- return rows.length > 0 ? { exists: true, rowId: rows[0].id } : { exists: false };
216
- }
217
- /** Get the next available decision ID (D001, D002, …). */
218
- async function nextDecisionId(projectRoot) {
219
- const { getBrainDb } = await import('../store/memory-sqlite.js');
220
- const { brainDecisions } = await import('../store/schema/memory-schema.js');
221
- const { desc } = await import('drizzle-orm');
222
- const db = await getBrainDb(projectRoot);
223
- const rows = await db
224
- .select({ id: brainDecisions.id })
225
- .from(brainDecisions)
226
- .orderBy(desc(brainDecisions.id))
227
- .limit(1);
228
- if (rows.length === 0)
229
- return 'D001';
230
- const last = rows[0].id;
231
- const num = parseInt(last.replace(/\D/g, ''), 10);
232
- return Number.isNaN(num) ? 'D001' : `D${String(num + 1).padStart(3, '0')}`;
233
- }
234
- /** Resolve the brain_decisions ID for a given ADR number (used for supersedure links). */
235
- async function decisionIdForAdrNumber(projectRoot, adrNumber) {
236
- const { getBrainDb } = await import('../store/memory-sqlite.js');
237
- const { brainDecisions } = await import('../store/schema/memory-schema.js');
238
- const { eq } = await import('drizzle-orm');
239
- const db = await getBrainDb(projectRoot);
240
- const rows = await db
241
- .select({ id: brainDecisions.id })
242
- .from(brainDecisions)
243
- .where(eq(brainDecisions.adrNumber, adrNumber))
244
- .limit(1);
245
- return rows.length > 0 ? rows[0].id : null;
246
- }
247
- /** Write a brain_decisions row directly via Drizzle (bypasses storeDecision gate pipeline). */
248
- async function insertAdrDecisionRow(projectRoot, opts) {
249
- const { getBrainDb } = await import('../store/memory-sqlite.js');
250
- const { brainDecisions } = await import('../store/schema/memory-schema.js');
251
- const db = await getBrainDb(projectRoot);
252
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
253
- await db.insert(brainDecisions).values({
254
- id: opts.id,
255
- type: 'architecture',
256
- decision: opts.decision,
257
- rationale: opts.rationale,
258
- confidence: 'high',
259
- outcome: 'success',
260
- qualityScore: 0.85,
261
- memoryTier: 'long',
262
- memoryType: 'semantic',
263
- sourceConfidence: 'owner',
264
- verified: true,
265
- contentHash: opts.contentHash,
266
- adrNumber: opts.adrNumber,
267
- adrPath: opts.adrPath,
268
- supersedes: opts.supersedes,
269
- supersededBy: opts.supersededBy,
270
- confirmationState: opts.confirmationState,
271
- decidedBy: opts.decidedBy,
272
- createdAt: now,
273
- updatedAt: now,
274
- provenanceClass: 'swept-clean',
275
- peerId: 'adr-backfill-walker',
276
- peerScope: 'project',
277
- decisionCategory: 'architectural',
278
- });
279
- }
280
- // ─── Main walker ──────────────────────────────────────────────────────────────
281
- /**
282
- * Main walker function.
283
- *
284
- * @param projectRoot - Absolute path to the project root.
285
- * @param dryRun - When true, no writes are performed.
286
- * @returns Array of results per ADR file processed.
287
- */
288
- export async function runAdrBackfillWalker(projectRoot, dryRun) {
289
- const results = [];
290
- const cleoCanonicalsDir = join(projectRoot, '.cleo', 'adrs');
291
- const docsAdrDir = join(projectRoot, 'docs', 'adr');
292
- // ── Collect all ADR files ─────────────────────────────────────────────────
293
- const allFiles = [];
294
- // Primary: .cleo/adrs/
295
- const cleoFiles = await readdir(cleoCanonicalsDir).catch(() => []);
296
- for (const f of cleoFiles) {
297
- if (f.endsWith('.md') && /^ADR-\d{3}/i.test(f)) {
298
- allFiles.push({ dir: cleoCanonicalsDir, filename: f, source: 'cleo' });
299
- }
300
- }
301
- // Secondary: docs/adr/ — only include ADR numbers NOT already in .cleo/adrs/
302
- const cleoAdrNumbers = new Set(cleoFiles.map((f) => extractAdrNumberFromFilename(f)).filter((n) => n > 0));
303
- if (existsSync(docsAdrDir)) {
304
- const docsFiles = await readdir(docsAdrDir).catch(() => []);
305
- for (const f of docsFiles) {
306
- if (!f.endsWith('.md'))
307
- continue;
308
- const n = extractAdrNumberFromFilename(f);
309
- if (n <= 0)
310
- continue; // skip unnumbered drafts
311
- if (COLLISION_ADR_NUMBERS.has(n)) {
312
- // Collision: docs/adr/ version skipped; .cleo/adrs/ is canonical
313
- results.push({
314
- adrNumber: n,
315
- filename: f,
316
- filePath: join(docsAdrDir, f),
317
- action: 'skipped-collision',
318
- note: `ADR-${String(n).padStart(3, '0')} collision: docs/adr/ version skipped. .cleo/adrs/ is canonical. HITL needed before merging.`,
319
- });
320
- continue;
321
- }
322
- if (!cleoAdrNumbers.has(n)) {
323
- allFiles.push({ dir: docsAdrDir, filename: f, source: 'docs' });
324
- }
325
- }
326
- }
327
- // Sort by ADR number for deterministic order
328
- allFiles.sort((a, b) => {
329
- const na = extractAdrNumberFromFilename(a.filename);
330
- const nb = extractAdrNumberFromFilename(b.filename);
331
- return na - nb;
332
- });
333
- // ── Process each ADR file ─────────────────────────────────────────────────
334
- for (const { dir, filename } of allFiles) {
335
- const filePath = join(dir, filename);
336
- const adrNumber = extractAdrNumberFromFilename(filename);
337
- const relPath = relative(projectRoot, filePath).replace(/\\/g, '/');
338
- let content;
339
- try {
340
- content = await readFile(filePath, 'utf8');
341
- }
342
- catch (err) {
343
- results.push({
344
- adrNumber,
345
- filename,
346
- filePath,
347
- action: 'skipped-error',
348
- note: `Could not read file: ${String(err)}`,
349
- });
350
- continue;
351
- }
352
- const fm = parseAdrFrontmatter(content, filename);
353
- const { decision, rationale } = buildDecisionText(fm.title, content);
354
- // Idempotency check: skip if row with this adrNumber already exists
355
- const existing = await rowExistsForAdrNumber(projectRoot, adrNumber);
356
- if (existing.exists) {
357
- results.push({
358
- adrNumber,
359
- filename,
360
- filePath,
361
- action: 'skipped-exists',
362
- note: `brain_decisions row ${existing.rowId} already has adrNumber=${adrNumber}`,
363
- decisionId: existing.rowId,
364
- });
365
- continue;
366
- }
367
- // Determine confirmation state
368
- let confirmationState = 'accepted';
369
- if (fm.status === 'superseded' || fm.status === 'archived' || fm.status === 'rejected') {
370
- confirmationState = 'superseded';
371
- }
372
- else if (fm.status === 'proposed' || fm.status === 'draft') {
373
- confirmationState = 'proposed';
374
- }
375
- // Check if this is a known duplicate (secondary of a pair)
376
- const canonicalAdrNumber = DUPLICATE_PAIRS[adrNumber];
377
- let supersededByDecisionId;
378
- let supersededByNote;
379
- if (canonicalAdrNumber !== undefined) {
380
- confirmationState = 'superseded';
381
- supersededByNote = `Duplicate of ADR-${String(canonicalAdrNumber).padStart(3, '0')} — marked superseded`;
382
- if (!dryRun) {
383
- supersededByDecisionId =
384
- (await decisionIdForAdrNumber(projectRoot, canonicalAdrNumber)) ?? undefined;
385
- }
386
- }
387
- // Resolve supersedes link from frontmatter
388
- let supersedesDecisionId;
389
- if (fm.supersedesAdrNumber !== undefined && !dryRun) {
390
- supersedesDecisionId =
391
- (await decisionIdForAdrNumber(projectRoot, fm.supersedesAdrNumber)) ?? undefined;
392
- }
393
- const { createHash } = await import('node:crypto');
394
- const contentHash = createHash('sha256')
395
- .update((decision + '\n' + rationale).toLowerCase())
396
- .digest('hex')
397
- .slice(0, 16);
398
- if (dryRun) {
399
- results.push({
400
- adrNumber,
401
- filename,
402
- filePath,
403
- action: 'inserted',
404
- note: `[DRY-RUN] Would insert: decision="${decision.slice(0, 80)}…" status=${confirmationState} path=${relPath}${supersededByNote ? ` note=${supersededByNote}` : ''}`,
405
- supersedureTo: canonicalAdrNumber
406
- ? `supersededBy ADR-${String(canonicalAdrNumber).padStart(3, '0')}`
407
- : undefined,
408
- });
409
- continue;
410
- }
411
- // Live insert
412
- try {
413
- const id = await nextDecisionId(projectRoot);
414
- await insertAdrDecisionRow(projectRoot, {
415
- id,
416
- adrNumber,
417
- adrPath: relPath,
418
- decision,
419
- rationale,
420
- confirmationState,
421
- decidedBy: 'owner',
422
- supersedes: supersedesDecisionId,
423
- supersededBy: supersededByDecisionId,
424
- contentHash,
425
- });
426
- results.push({
427
- adrNumber,
428
- filename,
429
- filePath,
430
- action: 'inserted',
431
- note: `Inserted as ${id} (adrNumber=${adrNumber}, state=${confirmationState})${supersededByNote ? ` — ${supersededByNote}` : ''}`,
432
- decisionId: id,
433
- supersedureTo: canonicalAdrNumber
434
- ? `supersededBy ADR-${String(canonicalAdrNumber).padStart(3, '0')}`
435
- : undefined,
436
- });
437
- }
438
- catch (err) {
439
- results.push({
440
- adrNumber,
441
- filename,
442
- filePath,
443
- action: 'skipped-error',
444
- note: `Insert failed: ${String(err)}`,
445
- });
446
- }
447
- }
448
- return results;
449
- }
450
- // ─── Report writer ────────────────────────────────────────────────────────────
451
- /**
452
- * Write the backfill report to `.cleo/agent-outputs/T1824-5-backfill-report.md`.
453
- *
454
- * @deprecated Since v2026.5.93 (T9795 / Saga T9787). Raw `.md` writes to
455
- * `.cleo/agent-outputs/` bypass the docs registry SSoT — migrate to
456
- * `cleo docs add --type note` (T9788). Removal: no earlier than
457
- * v2026.6.0. See `.cleo/deprecations.yml` id:`agent-output-raw-md-write`.
458
- */
459
- async function writeReport(projectRoot, results, dryRun) {
460
- const inserted = results.filter((r) => r.action === 'inserted');
461
- const skippedExists = results.filter((r) => r.action === 'skipped-exists');
462
- const skippedCollision = results.filter((r) => r.action === 'skipped-collision');
463
- const skippedError = results.filter((r) => r.action === 'skipped-error');
464
- const duplicatePairs = results.filter((r) => r.supersedureTo);
465
- const date = new Date().toISOString().split('T')[0];
466
- const mode = dryRun ? 'DRY-RUN (no writes)' : 'APPLIED';
467
- const lines = [
468
- '# ADR Backfill Walker Report — T1829',
469
- '',
470
- `**Date**: ${date}`,
471
- `**Mode**: ${mode}`,
472
- `**Task**: T1829`,
473
- `**Epic**: T1824`,
474
- '',
475
- '---',
476
- '',
477
- '## Summary',
478
- '',
479
- `| Category | Count |`,
480
- `|----------|-------|`,
481
- `| Inserted (or would-insert in dry-run) | ${inserted.length} |`,
482
- `| Skipped — row already exists | ${skippedExists.length} |`,
483
- `| Skipped — collision (HITL needed) | ${skippedCollision.length} |`,
484
- `| Skipped — error | ${skippedError.length} |`,
485
- `| Duplicate-pair supersession applied | ${duplicatePairs.length} |`,
486
- '',
487
- '---',
488
- '',
489
- ];
490
- if (inserted.length > 0) {
491
- lines.push('## Inserted Rows');
492
- lines.push('');
493
- for (const r of inserted) {
494
- const prefix = dryRun ? '[DRY-RUN] ' : '';
495
- lines.push(`- **ADR-${String(r.adrNumber).padStart(3, '0')}** (\`${basename(r.filePath)}\`)`);
496
- if (r.decisionId)
497
- lines.push(` - Decision ID: \`${r.decisionId}\``);
498
- if (r.note)
499
- lines.push(` - ${r.note.replace(/^\[DRY-RUN\] /, prefix)}`);
500
- if (r.supersedureTo)
501
- lines.push(` - Supersession: ${r.supersedureTo}`);
502
- }
503
- lines.push('');
504
- lines.push('---');
505
- lines.push('');
506
- }
507
- if (skippedExists.length > 0) {
508
- lines.push('## Skipped — Already Populated');
509
- lines.push('');
510
- for (const r of skippedExists) {
511
- lines.push(`- **ADR-${String(r.adrNumber).padStart(3, '0')}**: ${r.note ?? ''}`);
512
- }
513
- lines.push('');
514
- lines.push('---');
515
- lines.push('');
516
- }
517
- if (skippedCollision.length > 0) {
518
- lines.push('## Skipped — ADR Number Collisions (HITL Required)');
519
- lines.push('');
520
- lines.push('These ADR numbers exist in BOTH `.cleo/adrs/` and `docs/adr/` with **different content**.');
521
- lines.push('The `.cleo/adrs/` version is canonical. Owner must decide how to renumber the `docs/adr/` versions.');
522
- lines.push('');
523
- for (const r of skippedCollision) {
524
- lines.push(`### ADR-${String(r.adrNumber).padStart(3, '0')}`);
525
- lines.push('');
526
- lines.push(`- **File skipped**: \`${r.filePath}\``);
527
- lines.push(`- **Reason**: ${r.note ?? ''}`);
528
- lines.push('');
529
- }
530
- lines.push('**Action required**: File HITL request to owner for renumbering decision.');
531
- lines.push('');
532
- lines.push('---');
533
- lines.push('');
534
- }
535
- if (skippedError.length > 0) {
536
- lines.push('## Skipped — Errors');
537
- lines.push('');
538
- for (const r of skippedError) {
539
- lines.push(`- **ADR-${String(r.adrNumber).padStart(3, '0')}**: ${r.note ?? ''}`);
540
- }
541
- lines.push('');
542
- lines.push('---');
543
- lines.push('');
544
- }
545
- if (duplicatePairs.length > 0) {
546
- lines.push('## Duplicate-Pair Supersession');
547
- lines.push('');
548
- lines.push('The following ADRs were identified as exact duplicates within `.cleo/adrs/` and were');
549
- lines.push('inserted with `confirmationState=superseded` and a `supersededBy` link to the canonical ADR.');
550
- lines.push('');
551
- for (const r of duplicatePairs) {
552
- lines.push(`- **ADR-${String(r.adrNumber).padStart(3, '0')}** → ${r.supersedureTo} (\`${basename(r.filePath)}\`)`);
553
- }
554
- lines.push('');
555
- lines.push('---');
556
- lines.push('');
557
- }
558
- lines.push('## Collision Details');
559
- lines.push('');
560
- lines.push('### ADR-051..054 Collision Pairs');
561
- lines.push('');
562
- lines.push('| ADR Number | `.cleo/adrs/` file (canonical) | `docs/adr/` file (skipped) |');
563
- lines.push('|------------|-------------------------------|----------------------------|');
564
- lines.push('| ADR-051 | `ADR-051-programmatic-gate-integrity.md` | `ADR-051-override-patterns.md` |');
565
- lines.push('| ADR-052 | `ADR-052-caamp-keeps-commander.md` | `ADR-052-sdk-consolidation.md` |');
566
- lines.push('| ADR-053 | `ADR-053-project-agnostic-release-pipeline.md` | `ADR-053-playbook-runtime.md` |');
567
- lines.push('| ADR-054 | `ADR-054-manifest-unification.md` | `ADR-054-migration-system-hybrid-path-a-plus.md` |');
568
- lines.push('');
569
- lines.push('Owner must decide renumbering: e.g. `docs/adr/ADR-052-sdk-consolidation.md` → ADR-065.');
570
- lines.push('');
571
- lines.push('---');
572
- lines.push('');
573
- lines.push('*Generated by `packages/core/src/tools/adr-backfill-walker.ts` (T1829)*');
574
- const report = lines.join('\n');
575
- const reportPath = join(projectRoot, '.cleo', 'agent-outputs', 'T1824-5-backfill-report.md');
576
- await mkdir(join(projectRoot, '.cleo', 'agent-outputs'), { recursive: true });
577
- await writeFile(reportPath, report, 'utf8');
578
- return reportPath;
579
- }
580
- // ─── CLI entrypoint ───────────────────────────────────────────────────────────
581
- async function main() {
582
- const args = process.argv.slice(2);
583
- const dryRun = args.includes('--dry-run');
584
- const apply = args.includes('--apply');
585
- if (!dryRun && !apply) {
586
- console.error('Usage: adr-backfill-walker.js --dry-run | --apply\n\n' +
587
- ' --dry-run Show what would be inserted, no writes\n' +
588
- ' --apply Write brain_decisions rows for all ADRs\n');
589
- process.exit(1);
590
- }
591
- // Resolve project root (walk up from CWD looking for .cleo/)
592
- const { getProjectRoot } = await import('../paths.js');
593
- const projectRoot = getProjectRoot();
594
- console.log(`[T1829] ADR Backfill Walker`);
595
- console.log(`[T1829] Mode: ${dryRun ? 'DRY-RUN' : 'APPLY'}`);
596
- console.log(`[T1829] Project root: ${projectRoot}`);
597
- console.log('');
598
- const results = await runAdrBackfillWalker(projectRoot, dryRun);
599
- const inserted = results.filter((r) => r.action === 'inserted');
600
- const skippedExists = results.filter((r) => r.action === 'skipped-exists');
601
- const skippedCollision = results.filter((r) => r.action === 'skipped-collision');
602
- const skippedError = results.filter((r) => r.action === 'skipped-error');
603
- console.log(`Results:`);
604
- console.log(` Inserted (or would-insert): ${inserted.length}`);
605
- console.log(` Skipped (already exist): ${skippedExists.length}`);
606
- console.log(` Skipped (collision/HITL): ${skippedCollision.length}`);
607
- console.log(` Skipped (error): ${skippedError.length}`);
608
- console.log('');
609
- for (const r of results) {
610
- const icon = r.action === 'inserted'
611
- ? '[+]'
612
- : r.action === 'skipped-exists'
613
- ? '[=]'
614
- : r.action === 'skipped-collision'
615
- ? '[!]'
616
- : '[E]';
617
- const num = String(r.adrNumber).padStart(3, '0');
618
- console.log(` ${icon} ADR-${num} ${r.filename}${r.note ? ` — ${r.note}` : ''}`);
619
- }
620
- console.log('');
621
- const reportPath = await writeReport(projectRoot, results, dryRun);
622
- console.log(`[T1829] Report written to: ${reportPath}`);
623
- if (skippedError.length > 0) {
624
- process.exit(1);
625
- }
626
- }
627
- main().catch((err) => {
628
- console.error('[T1829] Fatal error:', err);
629
- process.exit(1);
630
- });
631
- //# sourceMappingURL=adr-backfill-walker.js.map