@cleocode/core 2026.3.58 → 2026.3.60

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 (153) hide show
  1. package/dist/agents/agent-registry.d.ts +206 -0
  2. package/dist/agents/agent-registry.d.ts.map +1 -0
  3. package/dist/agents/agent-registry.js +288 -0
  4. package/dist/agents/agent-registry.js.map +1 -0
  5. package/dist/agents/agent-schema.js +5 -0
  6. package/dist/agents/agent-schema.js.map +1 -1
  7. package/dist/agents/execution-learning.js +474 -0
  8. package/dist/agents/execution-learning.js.map +1 -0
  9. package/dist/agents/health-monitor.d.ts +161 -0
  10. package/dist/agents/health-monitor.d.ts.map +1 -0
  11. package/dist/agents/health-monitor.js +217 -0
  12. package/dist/agents/health-monitor.js.map +1 -0
  13. package/dist/agents/index.d.ts +3 -1
  14. package/dist/agents/index.d.ts.map +1 -1
  15. package/dist/agents/index.js +9 -1
  16. package/dist/agents/index.js.map +1 -1
  17. package/dist/agents/retry.d.ts +57 -4
  18. package/dist/agents/retry.d.ts.map +1 -1
  19. package/dist/agents/retry.js +57 -4
  20. package/dist/agents/retry.js.map +1 -1
  21. package/dist/backfill/index.d.ts +27 -0
  22. package/dist/backfill/index.d.ts.map +1 -1
  23. package/dist/backfill/index.js +229 -0
  24. package/dist/backfill/index.js.map +1 -0
  25. package/dist/bootstrap.d.ts +2 -1
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +135 -28
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/cleo.d.ts +40 -0
  30. package/dist/cleo.d.ts.map +1 -1
  31. package/dist/config.js +83 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1036 -536
  36. package/dist/index.js.map +4 -4
  37. package/dist/intelligence/adaptive-validation.js +497 -0
  38. package/dist/intelligence/adaptive-validation.js.map +1 -0
  39. package/dist/intelligence/impact.d.ts +34 -1
  40. package/dist/intelligence/impact.d.ts.map +1 -1
  41. package/dist/intelligence/impact.js +176 -0
  42. package/dist/intelligence/impact.js.map +1 -1
  43. package/dist/intelligence/index.d.ts +2 -2
  44. package/dist/intelligence/index.d.ts.map +1 -1
  45. package/dist/intelligence/index.js +6 -1
  46. package/dist/intelligence/index.js.map +1 -1
  47. package/dist/intelligence/types.d.ts +60 -0
  48. package/dist/intelligence/types.d.ts.map +1 -1
  49. package/dist/internal.d.ts +5 -4
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +11 -2
  52. package/dist/internal.js.map +1 -1
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.d.ts.map +1 -0
  55. package/dist/lib/index.js +10 -0
  56. package/dist/lib/index.js.map +1 -0
  57. package/dist/lib/retry.d.ts +128 -0
  58. package/dist/lib/retry.d.ts.map +1 -0
  59. package/dist/lib/retry.js +152 -0
  60. package/dist/lib/retry.js.map +1 -0
  61. package/dist/nexus/sharing/index.d.ts +48 -2
  62. package/dist/nexus/sharing/index.d.ts.map +1 -1
  63. package/dist/nexus/sharing/index.js +110 -1
  64. package/dist/nexus/sharing/index.js.map +1 -1
  65. package/dist/scaffold.d.ts.map +1 -1
  66. package/dist/scaffold.js +22 -2
  67. package/dist/scaffold.js.map +1 -1
  68. package/dist/sessions/session-enforcement.js +4 -0
  69. package/dist/sessions/session-enforcement.js.map +1 -1
  70. package/dist/stats/index.js +2 -0
  71. package/dist/stats/index.js.map +1 -1
  72. package/dist/stats/workflow-telemetry.d.ts +15 -0
  73. package/dist/stats/workflow-telemetry.d.ts.map +1 -1
  74. package/dist/stats/workflow-telemetry.js +400 -0
  75. package/dist/stats/workflow-telemetry.js.map +1 -0
  76. package/dist/store/brain-schema.js +4 -1
  77. package/dist/store/brain-schema.js.map +1 -1
  78. package/dist/store/converters.js +2 -0
  79. package/dist/store/converters.js.map +1 -1
  80. package/dist/store/cross-db-cleanup.d.ts +35 -0
  81. package/dist/store/cross-db-cleanup.d.ts.map +1 -1
  82. package/dist/store/cross-db-cleanup.js +169 -0
  83. package/dist/store/cross-db-cleanup.js.map +1 -0
  84. package/dist/store/db-helpers.js +2 -0
  85. package/dist/store/db-helpers.js.map +1 -1
  86. package/dist/store/migration-sqlite.js +5 -0
  87. package/dist/store/migration-sqlite.js.map +1 -1
  88. package/dist/store/sqlite-data-accessor.js +20 -28
  89. package/dist/store/sqlite-data-accessor.js.map +1 -1
  90. package/dist/store/sqlite.js +13 -2
  91. package/dist/store/sqlite.js.map +1 -1
  92. package/dist/store/task-store.js +4 -0
  93. package/dist/store/task-store.js.map +1 -1
  94. package/dist/store/tasks-schema.js +50 -20
  95. package/dist/store/tasks-schema.js.map +1 -1
  96. package/dist/tasks/add.js +87 -3
  97. package/dist/tasks/add.js.map +1 -1
  98. package/dist/tasks/complete.d.ts.map +1 -1
  99. package/dist/tasks/complete.js +15 -4
  100. package/dist/tasks/complete.js.map +1 -1
  101. package/dist/tasks/enforcement.d.ts.map +1 -1
  102. package/dist/tasks/enforcement.js +8 -1
  103. package/dist/tasks/enforcement.js.map +1 -1
  104. package/dist/tasks/epic-enforcement.d.ts +61 -0
  105. package/dist/tasks/epic-enforcement.d.ts.map +1 -1
  106. package/dist/tasks/epic-enforcement.js +294 -0
  107. package/dist/tasks/epic-enforcement.js.map +1 -0
  108. package/dist/tasks/index.js +1 -1
  109. package/dist/tasks/index.js.map +1 -1
  110. package/dist/tasks/pipeline-stage.d.ts +70 -1
  111. package/dist/tasks/pipeline-stage.d.ts.map +1 -1
  112. package/dist/tasks/pipeline-stage.js +248 -0
  113. package/dist/tasks/pipeline-stage.js.map +1 -0
  114. package/dist/tasks/update.js +28 -0
  115. package/dist/tasks/update.js.map +1 -1
  116. package/package.json +5 -5
  117. package/schemas/config.schema.json +37 -1547
  118. package/src/__tests__/sharing.test.ts +24 -0
  119. package/src/agents/__tests__/agent-registry.test.ts +351 -0
  120. package/src/agents/__tests__/health-monitor.test.ts +332 -0
  121. package/src/agents/agent-registry.ts +394 -0
  122. package/src/agents/health-monitor.ts +279 -0
  123. package/src/agents/index.ts +24 -1
  124. package/src/agents/retry.ts +57 -4
  125. package/src/backfill/index.ts +27 -0
  126. package/src/bootstrap.ts +171 -30
  127. package/src/cleo.ts +103 -2
  128. package/src/config.ts +3 -3
  129. package/src/index.ts +1 -0
  130. package/src/intelligence/__tests__/impact.test.ts +165 -1
  131. package/src/intelligence/impact.ts +203 -0
  132. package/src/intelligence/index.ts +3 -0
  133. package/src/intelligence/types.ts +76 -0
  134. package/src/internal.ts +20 -0
  135. package/src/lib/__tests__/retry.test.ts +321 -0
  136. package/src/lib/index.ts +16 -0
  137. package/src/lib/retry.ts +224 -0
  138. package/src/nexus/sharing/index.ts +142 -2
  139. package/src/scaffold.ts +24 -2
  140. package/src/stats/workflow-telemetry.ts +15 -0
  141. package/src/store/__tests__/session-store.test.ts +43 -7
  142. package/src/store/__tests__/task-store.test.ts +1 -1
  143. package/src/store/__tests__/test-db-helper.ts +7 -3
  144. package/src/store/cross-db-cleanup.ts +35 -0
  145. package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
  146. package/src/tasks/__tests__/minimal-test.test.ts +2 -2
  147. package/src/tasks/__tests__/update.test.ts +25 -25
  148. package/src/tasks/complete.ts +11 -6
  149. package/src/tasks/enforcement.ts +6 -3
  150. package/src/tasks/epic-enforcement.ts +61 -0
  151. package/src/tasks/pipeline-stage.ts +70 -1
  152. package/templates/config.template.json +5 -116
  153. package/templates/global-config.template.json +2 -44
@@ -14,14 +14,42 @@ import { join, relative } from 'node:path';
14
14
  import type { SharingConfig } from '@cleocode/contracts';
15
15
  import { loadConfig } from '../../config.js';
16
16
  import { getCleoDirAbsolute, getProjectRoot } from '../../paths.js';
17
+ import { cleoGitCommand, isCleoGitInitialized } from '../../store/git-checkpoint.js';
17
18
 
18
- /** Result of a sharing status check. */
19
+ /**
20
+ * Result of a sharing status check.
21
+ *
22
+ * @remarks
23
+ * Provides a complete view of which `.cleo/` files are tracked vs ignored under
24
+ * the current sharing config, plus git sync state for Nexus multi-project visibility.
25
+ * The `hasGit`, `remotes`, `pendingChanges`, and `lastSync` fields are populated
26
+ * only when a `.cleo/.git` repo exists; otherwise they carry safe defaults.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const status = await getSharingStatus();
31
+ * if (status.hasGit && status.pendingChanges) {
32
+ * console.log('Uncommitted changes in .cleo/ — run: cleo checkpoint');
33
+ * }
34
+ * ```
35
+ */
19
36
  export interface SharingStatus {
20
37
  mode: string;
21
38
  allowlist: string[];
22
39
  denylist: string[];
23
40
  tracked: string[];
24
41
  ignored: string[];
42
+ /** Whether the `.cleo/.git` isolated repo exists and is initialized. */
43
+ hasGit: boolean;
44
+ /** Git remote names configured in `.cleo/.git` (e.g. `['origin']`). */
45
+ remotes: string[];
46
+ /** Whether the `.cleo/.git` working tree has uncommitted changes. */
47
+ pendingChanges: boolean;
48
+ /**
49
+ * ISO 8601 timestamp of the last push or pull to/from a remote, or `null`
50
+ * if no remote sync has ever occurred.
51
+ */
52
+ lastSync: string | null;
25
53
  }
26
54
 
27
55
  /** Markers for the managed section in .gitignore. */
@@ -104,8 +132,102 @@ function collectCleoFiles(cleoDir: string): string[] {
104
132
  }
105
133
 
106
134
  /**
107
- * Get the sharing status: which .cleo/ files are tracked vs ignored.
135
+ * Retrieve the names of git remotes configured in the `.cleo/.git` repo.
136
+ *
137
+ * @remarks
138
+ * Returns an empty array if the repo is not initialized or has no remotes.
139
+ * Errors are suppressed — callers should treat an empty array as "no remotes known".
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const remotes = await getCleoGitRemotes('/path/to/project/.cleo');
144
+ * // ['origin']
145
+ * ```
146
+ */
147
+ async function getCleoGitRemotes(cleoDir: string): Promise<string[]> {
148
+ const result = await cleoGitCommand(['remote'], cleoDir);
149
+ if (!result.success || !result.stdout) return [];
150
+ return result.stdout
151
+ .split('\n')
152
+ .map((r) => r.trim())
153
+ .filter(Boolean);
154
+ }
155
+
156
+ /**
157
+ * Determine whether the `.cleo/.git` working tree has any uncommitted changes.
158
+ *
159
+ * @remarks
160
+ * Uses `git status --porcelain`. A non-empty output means pending changes exist.
161
+ * Returns `false` if the repo is not initialized or the command fails.
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const dirty = await hasCleoGitPendingChanges('/path/to/project/.cleo');
166
+ * // true if any files are modified/untracked
167
+ * ```
168
+ */
169
+ async function hasCleoGitPendingChanges(cleoDir: string): Promise<boolean> {
170
+ const result = await cleoGitCommand(['status', '--porcelain'], cleoDir);
171
+ if (!result.success) return false;
172
+ return result.stdout.length > 0;
173
+ }
174
+
175
+ /**
176
+ * Read the ISO 8601 timestamp of the last push or pull recorded in the reflog.
177
+ *
178
+ * @remarks
179
+ * Scans the git reflog for `fetch` or `push` entries and returns the committer
180
+ * date of the most recent one. Returns `null` if no push/pull has occurred or
181
+ * if the repo has no commits yet.
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const lastSync = await getLastSyncTimestamp('/path/to/project/.cleo');
186
+ * // '2026-03-21T18:00:00.000Z' or null
187
+ * ```
188
+ */
189
+ async function getLastSyncTimestamp(cleoDir: string): Promise<string | null> {
190
+ // The reflog format: `%gd %gs %ci` — reflog selector, subject, committer ISO date
191
+ const result = await cleoGitCommand(['reflog', '--format=%gs %ci', 'HEAD'], cleoDir);
192
+ if (!result.success || !result.stdout) return null;
193
+
194
+ for (const line of result.stdout.split('\n')) {
195
+ const trimmed = line.trim();
196
+ // Match lines describing a fetch or push action (e.g. "fetch origin: fast-forward")
197
+ if (/^(fetch|push|pull)\b/i.test(trimmed)) {
198
+ // The date is everything after the action description — last ISO-like token
199
+ const isoMatch = trimmed.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4})$/);
200
+ if (isoMatch?.[1]) {
201
+ return new Date(isoMatch[1]).toISOString();
202
+ }
203
+ }
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Get the sharing status: which .cleo/ files are tracked vs ignored,
211
+ * plus git sync state for Nexus multi-project visibility.
212
+ *
213
+ * @remarks
214
+ * Populates `hasGit`, `remotes`, `pendingChanges`, and `lastSync` by inspecting
215
+ * the `.cleo/.git` isolated repo when it exists. All git operations are
216
+ * non-fatal — if the repo is absent or a command fails, the fields carry safe
217
+ * defaults (`false`, `[]`, `null`).
218
+ *
219
+ * @example
220
+ * ```typescript
221
+ * const status = await getSharingStatus('/path/to/project');
222
+ * console.log(status.mode); // 'project'
223
+ * console.log(status.hasGit); // true
224
+ * console.log(status.remotes); // ['origin']
225
+ * console.log(status.pendingChanges); // false
226
+ * console.log(status.lastSync); // '2026-03-21T18:00:00.000Z'
227
+ * ```
228
+ *
108
229
  * @task T4883
230
+ * @task T110
109
231
  */
110
232
  export async function getSharingStatus(cwd?: string): Promise<SharingStatus> {
111
233
  const config = await loadConfig(cwd);
@@ -126,12 +248,30 @@ export async function getSharingStatus(cwd?: string): Promise<SharingStatus> {
126
248
  }
127
249
  }
128
250
 
251
+ // Populate git sync fields
252
+ const hasGit = isCleoGitInitialized(cleoDir);
253
+ let remotes: string[] = [];
254
+ let pendingChanges = false;
255
+ let lastSync: string | null = null;
256
+
257
+ if (hasGit) {
258
+ [remotes, pendingChanges, lastSync] = await Promise.all([
259
+ getCleoGitRemotes(cleoDir),
260
+ hasCleoGitPendingChanges(cleoDir),
261
+ getLastSyncTimestamp(cleoDir),
262
+ ]);
263
+ }
264
+
129
265
  return {
130
266
  mode: sharing.mode,
131
267
  allowlist: sharing.commitAllowlist,
132
268
  denylist: sharing.denylist,
133
269
  tracked,
134
270
  ignored,
271
+ hasGit,
272
+ remotes,
273
+ pendingChanges,
274
+ lastSync,
135
275
  };
136
276
  }
137
277
 
package/src/scaffold.ts CHANGED
@@ -14,6 +14,7 @@ import { execFile } from 'node:child_process';
14
14
  import { randomUUID } from 'node:crypto';
15
15
  import { existsSync, constants as fsConstants, readFileSync, statSync } from 'node:fs';
16
16
  import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
17
+ import { homedir as getHomedir } from 'node:os';
17
18
  import { dirname, join, resolve } from 'node:path';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import { promisify } from 'node:util';
@@ -1278,12 +1279,33 @@ export function checkGlobalTemplates(): CheckResult {
1278
1279
  };
1279
1280
  }
1280
1281
 
1282
+ // Check version sync between XDG and legacy paths
1283
+ const xdgContent = readFileSync(injectionPath, 'utf-8');
1284
+ const xdgVersion = xdgContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
1285
+ const home = getHomedir();
1286
+ const legacyPath = join(home, '.cleo', 'templates', 'CLEO-INJECTION.md');
1287
+
1288
+ if (existsSync(legacyPath)) {
1289
+ const legacyContent = readFileSync(legacyPath, 'utf-8');
1290
+ const legacyVersion = legacyContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
1291
+ if (legacyVersion && xdgVersion && legacyVersion !== xdgVersion) {
1292
+ return {
1293
+ id: 'global_templates',
1294
+ category: 'global',
1295
+ status: 'warning',
1296
+ message: `Legacy template version (${legacyVersion}) out of sync with XDG (${xdgVersion})`,
1297
+ details: { path: injectionPath, exists: true, xdgVersion, legacyVersion, legacyPath },
1298
+ fix: 'npm install -g @cleocode/cleo (reinstall syncs both paths)',
1299
+ };
1300
+ }
1301
+ }
1302
+
1281
1303
  return {
1282
1304
  id: 'global_templates',
1283
1305
  category: 'global',
1284
1306
  status: 'passed',
1285
- message: 'Global injection template present',
1286
- details: { path: injectionPath, exists: true },
1307
+ message: `Global injection template present (v${xdgVersion ?? 'unknown'})`,
1308
+ details: { path: injectionPath, exists: true, version: xdgVersion },
1287
1309
  fix: null,
1288
1310
  };
1289
1311
  }
@@ -229,6 +229,21 @@ function gradeFromScore(score: number): string {
229
229
  * WF-003: Completed tasks SHOULD have verification gates set (T061)
230
230
  * WF-004: Tasks with verification SHOULD have all 3 gates set
231
231
  * WF-005: Tasks MUST have session binding on creation (non-epic)
232
+ *
233
+ * @remarks
234
+ * Derives all metrics from existing audit_log and tasks tables — no new
235
+ * tracking infrastructure is required.
236
+ *
237
+ * @param opts - Report options
238
+ * @param opts.since - ISO 8601 date string to filter metrics from
239
+ * @param opts.cwd - Working directory for database resolution
240
+ * @returns Compliance report with per-rule pass/fail counts and overall rate
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * const report = await getWorkflowComplianceReport({ cwd: '/my/project' });
245
+ * console.log(report.overall.passRate); // e.g. 0.85
246
+ * ```
232
247
  */
233
248
  export async function getWorkflowComplianceReport(opts: {
234
249
  since?: string;
@@ -8,7 +8,7 @@
8
8
  * @epic T4638
9
9
  */
10
10
 
11
- import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
11
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import type { Session } from '@cleocode/contracts';
@@ -404,9 +404,27 @@ describe('SQLite session-store', () => {
404
404
  const db = await getDb();
405
405
  db.insert(tasksTable)
406
406
  .values([
407
- { id: 'T001', title: 'Task T001', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
408
- { id: 'T002', title: 'Task T002', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
409
- { id: 'T003', title: 'Task T003', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
407
+ {
408
+ id: 'T001',
409
+ title: 'Task T001',
410
+ status: 'pending',
411
+ priority: 'medium',
412
+ createdAt: new Date().toISOString(),
413
+ },
414
+ {
415
+ id: 'T002',
416
+ title: 'Task T002',
417
+ status: 'pending',
418
+ priority: 'medium',
419
+ createdAt: new Date().toISOString(),
420
+ },
421
+ {
422
+ id: 'T003',
423
+ title: 'Task T003',
424
+ status: 'pending',
425
+ priority: 'medium',
426
+ createdAt: new Date().toISOString(),
427
+ },
410
428
  ])
411
429
  .run();
412
430
  await createSession(makeSession({ id: 'sess-001' }));
@@ -430,9 +448,27 @@ describe('SQLite session-store', () => {
430
448
  const db = await getDb();
431
449
  db.insert(tasksTable)
432
450
  .values([
433
- { id: 'T001', title: 'Task T001', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
434
- { id: 'T002', title: 'Task T002', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
435
- { id: 'T003', title: 'Task T003', status: 'pending', priority: 'medium', createdAt: new Date().toISOString() },
451
+ {
452
+ id: 'T001',
453
+ title: 'Task T001',
454
+ status: 'pending',
455
+ priority: 'medium',
456
+ createdAt: new Date().toISOString(),
457
+ },
458
+ {
459
+ id: 'T002',
460
+ title: 'Task T002',
461
+ status: 'pending',
462
+ priority: 'medium',
463
+ createdAt: new Date().toISOString(),
464
+ },
465
+ {
466
+ id: 'T003',
467
+ title: 'Task T003',
468
+ status: 'pending',
469
+ priority: 'medium',
470
+ createdAt: new Date().toISOString(),
471
+ },
436
472
  ])
437
473
  .run();
438
474
  await createSession(makeSession({ id: 'sess-001' }));
@@ -8,7 +8,7 @@
8
8
  * @epic T4638
9
9
  */
10
10
 
11
- import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
11
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import type { Task } from '@cleocode/contracts';
@@ -8,7 +8,7 @@
8
8
  * @task T5244
9
9
  */
10
10
 
11
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import type { Task } from '@cleocode/contracts';
@@ -57,7 +57,9 @@ export async function createTestDb(): Promise<TestDbEnv> {
57
57
  const { readdirSync } = await import('node:fs');
58
58
  const contents = readdirSync(cleoDir);
59
59
  if (!contents.includes('config.json')) {
60
- throw new Error(`createTestDb: config.json not found in ${cleoDir} after write (contents: ${JSON.stringify(contents)})`);
60
+ throw new Error(
61
+ `createTestDb: config.json not found in ${cleoDir} after write (contents: ${JSON.stringify(contents)})`,
62
+ );
61
63
  }
62
64
 
63
65
  const accessor = await createSqliteDataAccessor(tempDir);
@@ -66,7 +68,9 @@ export async function createTestDb(): Promise<TestDbEnv> {
66
68
  const { readdirSync: readdirSync2 } = await import('node:fs');
67
69
  const contentsAfterDb = readdirSync2(cleoDir);
68
70
  if (!contentsAfterDb.includes('config.json')) {
69
- throw new Error(`createTestDb: config.json DELETED by createSqliteDataAccessor! ${cleoDir}: ${JSON.stringify(contentsAfterDb)}`);
71
+ throw new Error(
72
+ `createTestDb: config.json DELETED by createSqliteDataAccessor! ${cleoDir}: ${JSON.stringify(contentsAfterDb)}`,
73
+ );
70
74
  }
71
75
 
72
76
  return {
@@ -26,8 +26,17 @@ import { getBrainDb } from './brain-sqlite.js';
26
26
  * This is a best-effort cleanup — brain.db is a cognitive store and minor
27
27
  * staleness is preferable to failing task deletions due to brain.db errors.
28
28
  *
29
+ * @remarks
30
+ * Best-effort: failures in brain.db cleanup do not propagate to the caller.
31
+ * A background reconciliation pass can clean up any residual stale refs.
32
+ *
29
33
  * @param taskId - The ID of the task being deleted from tasks.db
30
34
  * @param cwd - Optional working directory
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * await cleanupBrainRefsOnTaskDelete('T042');
39
+ * ```
31
40
  */
32
41
  export async function cleanupBrainRefsOnTaskDelete(taskId: string, cwd?: string): Promise<void> {
33
42
  let brainDb: Awaited<ReturnType<typeof getBrainDb>> | null = null;
@@ -84,8 +93,16 @@ export async function cleanupBrainRefsOnTaskDelete(taskId: string, cwd?: string)
84
93
  * Handles:
85
94
  * - XFKB-004: Nullify brain_observations.source_session_id where it matches
86
95
  *
96
+ * @remarks
97
+ * Best-effort: failures do not propagate. brain.db may not be initialised.
98
+ *
87
99
  * @param sessionId - The ID of the session being deleted from tasks.db
88
100
  * @param cwd - Optional working directory
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * await cleanupBrainRefsOnSessionDelete('ses_20260321_abc');
105
+ * ```
89
106
  */
90
107
  export async function cleanupBrainRefsOnSessionDelete(
91
108
  sessionId: string,
@@ -116,8 +133,17 @@ export async function cleanupBrainRefsOnSessionDelete(
116
133
  *
117
134
  * Provides write-guard for XFKB-001/002/003 on brain.db insert.
118
135
  *
136
+ * @remarks
137
+ * Used as a write-guard before inserting cross-DB references into brain.db.
138
+ *
119
139
  * @param taskId - Task ID to verify
120
140
  * @param tasksDb - The tasks.db drizzle instance
141
+ * @returns True if the task exists in tasks.db
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * if (await taskExistsInTasksDb('T042', db)) { /* safe to reference *\/ }
146
+ * ```
121
147
  */
122
148
  export async function taskExistsInTasksDb(
123
149
  taskId: string,
@@ -139,8 +165,17 @@ export async function taskExistsInTasksDb(
139
165
  *
140
166
  * Provides write-guard for XFKB-004 on brain.db insert.
141
167
  *
168
+ * @remarks
169
+ * Used as a write-guard before inserting cross-DB references into brain.db.
170
+ *
142
171
  * @param sessionId - Session ID to verify
143
172
  * @param tasksDb - The tasks.db drizzle instance
173
+ * @returns True if the session exists in tasks.db
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * if (await sessionExistsInTasksDb('ses_abc', db)) { /* safe to reference *\/ }
178
+ * ```
144
179
  */
145
180
  export async function sessionExistsInTasksDb(
146
181
  sessionId: string,
@@ -19,8 +19,13 @@ import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-help
19
19
 
20
20
  // Epic enforcement tests NEED enforcement active — temporarily clear VITEST
21
21
  const savedVitest = process.env.VITEST;
22
- beforeAll(() => { delete process.env.VITEST; });
23
- afterAll(() => { if (savedVitest) process.env.VITEST = savedVitest; });
22
+ beforeAll(() => {
23
+ delete process.env.VITEST;
24
+ });
25
+ afterAll(() => {
26
+ if (savedVitest) process.env.VITEST = savedVitest;
27
+ });
28
+
24
29
  import type { DataAccessor } from '../../store/data-accessor.js';
25
30
  import { addTask } from '../add.js';
26
31
  import {
@@ -380,7 +385,7 @@ describe('validateEpicStageAdvancement (strict)', () => {
380
385
  id: 'T002',
381
386
  title: 'Child',
382
387
  description: 'Child',
383
- status: 'in-progress',
388
+ status: 'active',
384
389
  priority: 'medium',
385
390
  type: 'task',
386
391
  parentId: 'T001',
@@ -416,7 +421,7 @@ describe('validateEpicStageAdvancement (strict)', () => {
416
421
  id: 'T002',
417
422
  title: 'Child',
418
423
  description: 'Child',
419
- status: 'in-progress',
424
+ status: 'active',
420
425
  priority: 'medium',
421
426
  type: 'task',
422
427
  parentId: 'T001',
@@ -1,7 +1,7 @@
1
- import { mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { beforeEach, afterEach, describe, it, expect } from 'vitest';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
5
 
6
6
  describe('minimal repro', () => {
7
7
  let tempDir: string;
@@ -231,11 +231,11 @@ describe('updateTask', () => {
231
231
  await writeFile(
232
232
  join(env.cleoDir, 'config.json'),
233
233
  JSON.stringify({
234
- enforcement: { session: { requiredForMutate: false } },
235
- lifecycle: { mode: 'off' },
236
- verification: { enabled: false },
237
- hierarchy: { maxDepth: 3, maxSiblings: 20 },
238
- }),
234
+ enforcement: { session: { requiredForMutate: false } },
235
+ lifecycle: { mode: 'off' },
236
+ verification: { enabled: false },
237
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
238
+ }),
239
239
  );
240
240
 
241
241
  const result = await updateTask({ taskId: 'T002', parentId: 'T001' }, env.tempDir, accessor);
@@ -266,11 +266,11 @@ describe('updateTask', () => {
266
266
  await writeFile(
267
267
  join(env.cleoDir, 'config.json'),
268
268
  JSON.stringify({
269
- enforcement: { session: { requiredForMutate: false } },
270
- lifecycle: { mode: 'off' },
271
- verification: { enabled: false },
272
- hierarchy: { maxDepth: 3, maxSiblings: 20 },
273
- }),
269
+ enforcement: { session: { requiredForMutate: false } },
270
+ lifecycle: { mode: 'off' },
271
+ verification: { enabled: false },
272
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
273
+ }),
274
274
  );
275
275
 
276
276
  const result = await updateTask({ taskId: 'T002', parentId: null }, env.tempDir, accessor);
@@ -301,11 +301,11 @@ describe('updateTask', () => {
301
301
  await writeFile(
302
302
  join(env.cleoDir, 'config.json'),
303
303
  JSON.stringify({
304
- enforcement: { session: { requiredForMutate: false } },
305
- lifecycle: { mode: 'off' },
306
- verification: { enabled: false },
307
- hierarchy: { maxDepth: 3, maxSiblings: 20 },
308
- }),
304
+ enforcement: { session: { requiredForMutate: false } },
305
+ lifecycle: { mode: 'off' },
306
+ verification: { enabled: false },
307
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
308
+ }),
309
309
  );
310
310
 
311
311
  const result = await updateTask({ taskId: 'T002', parentId: '' }, env.tempDir, accessor);
@@ -336,11 +336,11 @@ describe('updateTask', () => {
336
336
  await writeFile(
337
337
  join(env.cleoDir, 'config.json'),
338
338
  JSON.stringify({
339
- enforcement: { session: { requiredForMutate: false } },
340
- lifecycle: { mode: 'off' },
341
- verification: { enabled: false },
342
- hierarchy: { maxDepth: 3, maxSiblings: 20 },
343
- }),
339
+ enforcement: { session: { requiredForMutate: false } },
340
+ lifecycle: { mode: 'off' },
341
+ verification: { enabled: false },
342
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
343
+ }),
344
344
  );
345
345
 
346
346
  await expect(
@@ -370,11 +370,11 @@ describe('updateTask', () => {
370
370
  await writeFile(
371
371
  join(env.cleoDir, 'config.json'),
372
372
  JSON.stringify({
373
- enforcement: { session: { requiredForMutate: false } },
374
- lifecycle: { mode: 'off' },
375
- verification: { enabled: false },
376
- hierarchy: { maxDepth: 3, maxSiblings: 20 },
377
- }),
373
+ enforcement: { session: { requiredForMutate: false } },
374
+ lifecycle: { mode: 'off' },
375
+ verification: { enabled: false },
376
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
377
+ }),
378
378
  );
379
379
 
380
380
  const result = await updateTask(
@@ -76,15 +76,18 @@ async function loadCompletionEnforcement(cwd?: string): Promise<CompletionEnforc
76
76
  const acceptanceMode =
77
77
  modeRaw === 'off' || modeRaw === 'warn' || modeRaw === 'block'
78
78
  ? modeRaw
79
- : isTest ? 'off' : 'block';
79
+ : isTest
80
+ ? 'off'
81
+ : 'block';
80
82
 
81
83
  const acceptanceRequiredForPriorities = Array.isArray(prioritiesRaw)
82
84
  ? prioritiesRaw.filter((p): p is string => typeof p === 'string')
83
- : isTest ? [] : ['critical', 'high', 'medium', 'low'];
85
+ : isTest
86
+ ? []
87
+ : ['critical', 'high', 'medium', 'low'];
84
88
 
85
- const verificationEnabled = verificationEnabledRaw === true ? true
86
- : verificationEnabledRaw === false ? false
87
- : !isTest;
89
+ const verificationEnabled =
90
+ verificationEnabledRaw === true ? true : verificationEnabledRaw === false ? false : !isTest;
88
91
 
89
92
  const verificationRequiredGates = Array.isArray(verificationRequiredGatesRaw)
90
93
  ? verificationRequiredGatesRaw
@@ -104,7 +107,9 @@ async function loadCompletionEnforcement(cwd?: string): Promise<CompletionEnforc
104
107
  lifecycleModeRaw === 'none' ||
105
108
  lifecycleModeRaw === 'off'
106
109
  ? lifecycleModeRaw
107
- : isTest ? 'off' : 'strict';
110
+ : isTest
111
+ ? 'off'
112
+ : 'strict';
108
113
 
109
114
  return {
110
115
  acceptanceMode,
@@ -41,9 +41,12 @@ export async function createAcceptanceEnforcement(cwd?: string): Promise<Accepta
41
41
  const minCriteriaRaw = await getRawConfigValue('enforcement.acceptance.minimumCriteria', cwd);
42
42
  const defaultPriorityRaw = await getRawConfigValue('defaults.priority', cwd);
43
43
 
44
- const mode = modeRaw === 'off' || modeRaw === 'warn' || modeRaw === 'block'
45
- ? modeRaw
46
- : isTest ? 'off' : 'block';
44
+ const mode =
45
+ modeRaw === 'off' || modeRaw === 'warn' || modeRaw === 'block'
46
+ ? modeRaw
47
+ : isTest
48
+ ? 'off'
49
+ : 'block';
47
50
 
48
51
  const requiredForPriorities = Array.isArray(prioritiesRaw)
49
52
  ? prioritiesRaw.filter((p): p is string => typeof p === 'string')