@cleocode/core 2026.3.57 → 2026.3.59

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 (136) 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-schema.d.ts.map +1 -1
  4. package/dist/agents/execution-learning.d.ts +223 -0
  5. package/dist/agents/execution-learning.d.ts.map +1 -0
  6. package/dist/agents/health-monitor.d.ts +161 -0
  7. package/dist/agents/health-monitor.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +4 -1
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/retry.d.ts +57 -4
  11. package/dist/agents/retry.d.ts.map +1 -1
  12. package/dist/backfill/index.d.ts +83 -0
  13. package/dist/backfill/index.d.ts.map +1 -0
  14. package/dist/bootstrap.d.ts +1 -1
  15. package/dist/config.d.ts +47 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6985 -5068
  20. package/dist/index.js.map +4 -4
  21. package/dist/intelligence/adaptive-validation.d.ts +151 -0
  22. package/dist/intelligence/adaptive-validation.d.ts.map +1 -0
  23. package/dist/intelligence/impact.d.ts +34 -1
  24. package/dist/intelligence/impact.d.ts.map +1 -1
  25. package/dist/intelligence/index.d.ts +7 -2
  26. package/dist/intelligence/index.d.ts.map +1 -1
  27. package/dist/intelligence/types.d.ts +60 -0
  28. package/dist/intelligence/types.d.ts.map +1 -1
  29. package/dist/internal.d.ts +8 -4
  30. package/dist/internal.d.ts.map +1 -1
  31. package/dist/lib/index.d.ts +10 -0
  32. package/dist/lib/index.d.ts.map +1 -0
  33. package/dist/lib/retry.d.ts +128 -0
  34. package/dist/lib/retry.d.ts.map +1 -0
  35. package/dist/nexus/sharing/index.d.ts +48 -2
  36. package/dist/nexus/sharing/index.d.ts.map +1 -1
  37. package/dist/sessions/session-enforcement.d.ts.map +1 -1
  38. package/dist/stats/index.d.ts +1 -0
  39. package/dist/stats/index.d.ts.map +1 -1
  40. package/dist/stats/workflow-telemetry.d.ts +89 -0
  41. package/dist/stats/workflow-telemetry.d.ts.map +1 -0
  42. package/dist/store/brain-schema.d.ts.map +1 -1
  43. package/dist/store/converters.d.ts.map +1 -1
  44. package/dist/store/cross-db-cleanup.d.ts +93 -0
  45. package/dist/store/cross-db-cleanup.d.ts.map +1 -0
  46. package/dist/store/db-helpers.d.ts.map +1 -1
  47. package/dist/store/migration-sqlite.d.ts.map +1 -1
  48. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  49. package/dist/store/sqlite.d.ts.map +1 -1
  50. package/dist/store/task-store.d.ts.map +1 -1
  51. package/dist/store/tasks-schema.d.ts +18 -3
  52. package/dist/store/tasks-schema.d.ts.map +1 -1
  53. package/dist/store/validation-schemas.d.ts +32 -0
  54. package/dist/store/validation-schemas.d.ts.map +1 -1
  55. package/dist/tasks/add.d.ts +10 -1
  56. package/dist/tasks/add.d.ts.map +1 -1
  57. package/dist/tasks/complete.d.ts.map +1 -1
  58. package/dist/tasks/enforcement.d.ts +22 -0
  59. package/dist/tasks/enforcement.d.ts.map +1 -0
  60. package/dist/tasks/epic-enforcement.d.ts +199 -0
  61. package/dist/tasks/epic-enforcement.d.ts.map +1 -0
  62. package/dist/tasks/index.d.ts +1 -1
  63. package/dist/tasks/index.d.ts.map +1 -1
  64. package/dist/tasks/pipeline-stage.d.ts +181 -0
  65. package/dist/tasks/pipeline-stage.d.ts.map +1 -0
  66. package/dist/tasks/update.d.ts +2 -0
  67. package/dist/tasks/update.d.ts.map +1 -1
  68. package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/migration.sql +12 -0
  69. package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/snapshot.json +1232 -0
  70. package/migrations/drizzle-tasks/20260321000000_t033-connection-health/migration.sql +518 -0
  71. package/migrations/drizzle-tasks/20260321000000_t033-connection-health/snapshot.json +4312 -0
  72. package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/migration.sql +82 -0
  73. package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/snapshot.json +9 -0
  74. package/package.json +5 -5
  75. package/schemas/config.schema.json +37 -1547
  76. package/src/__tests__/sharing.test.ts +24 -0
  77. package/src/agents/__tests__/agent-registry.test.ts +351 -0
  78. package/src/agents/__tests__/execution-learning.test.ts +684 -0
  79. package/src/agents/__tests__/health-monitor.test.ts +332 -0
  80. package/src/agents/__tests__/registry.test.ts +30 -2
  81. package/src/agents/agent-registry.ts +394 -0
  82. package/src/agents/agent-schema.ts +5 -0
  83. package/src/agents/execution-learning.ts +675 -0
  84. package/src/agents/health-monitor.ts +279 -0
  85. package/src/agents/index.ts +37 -1
  86. package/src/agents/retry.ts +57 -4
  87. package/src/backfill/index.ts +309 -0
  88. package/src/bootstrap.ts +1 -1
  89. package/src/config.ts +126 -0
  90. package/src/index.ts +8 -1
  91. package/src/intelligence/__tests__/adaptive-validation.test.ts +694 -0
  92. package/src/intelligence/__tests__/impact.test.ts +165 -1
  93. package/src/intelligence/adaptive-validation.ts +764 -0
  94. package/src/intelligence/impact.ts +203 -0
  95. package/src/intelligence/index.ts +19 -0
  96. package/src/intelligence/types.ts +76 -0
  97. package/src/internal.ts +39 -0
  98. package/src/lib/__tests__/retry.test.ts +321 -0
  99. package/src/lib/index.ts +16 -0
  100. package/src/lib/retry.ts +224 -0
  101. package/src/lifecycle/__tests__/chain-store.test.ts +7 -0
  102. package/src/lifecycle/__tests__/tessera-engine.test.ts +52 -0
  103. package/src/nexus/sharing/index.ts +142 -2
  104. package/src/sessions/__tests__/session-edge-cases.test.ts +24 -1
  105. package/src/sessions/session-enforcement.ts +13 -2
  106. package/src/stats/index.ts +7 -0
  107. package/src/stats/workflow-telemetry.ts +502 -0
  108. package/src/store/__tests__/migration-safety.test.ts +3 -0
  109. package/src/store/__tests__/session-store.test.ts +132 -1
  110. package/src/store/__tests__/task-store.test.ts +22 -1
  111. package/src/store/__tests__/test-db-helper.ts +29 -2
  112. package/src/store/brain-schema.ts +4 -1
  113. package/src/store/converters.ts +2 -0
  114. package/src/store/cross-db-cleanup.ts +192 -0
  115. package/src/store/db-helpers.ts +2 -0
  116. package/src/store/migration-sqlite.ts +6 -0
  117. package/src/store/sqlite-data-accessor.ts +20 -28
  118. package/src/store/sqlite.ts +14 -2
  119. package/src/store/task-store.ts +6 -0
  120. package/src/store/tasks-schema.ts +59 -20
  121. package/src/tasks/__tests__/add.test.ts +16 -0
  122. package/src/tasks/__tests__/complete-unblocks.test.ts +10 -1
  123. package/src/tasks/__tests__/complete.test.ts +11 -2
  124. package/src/tasks/__tests__/epic-enforcement.test.ts +909 -0
  125. package/src/tasks/__tests__/minimal-test.test.ts +28 -0
  126. package/src/tasks/__tests__/pipeline-stage.test.ts +403 -0
  127. package/src/tasks/__tests__/update.test.ts +40 -6
  128. package/src/tasks/add.ts +128 -2
  129. package/src/tasks/complete.ts +29 -17
  130. package/src/tasks/enforcement.ts +127 -0
  131. package/src/tasks/epic-enforcement.ts +364 -0
  132. package/src/tasks/index.ts +1 -0
  133. package/src/tasks/pipeline-stage.ts +293 -0
  134. package/src/tasks/update.ts +62 -0
  135. package/templates/config.template.json +34 -111
  136. package/templates/global-config.template.json +24 -40
@@ -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
 
@@ -6,7 +6,7 @@
6
6
  * @epic T4498
7
7
  */
8
8
 
9
- import { mkdir, mkdtemp, rm } from 'node:fs/promises';
9
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -28,6 +28,15 @@ beforeEach(async () => {
28
28
  cleoDir = join(tempDir, '.cleo');
29
29
  await mkdir(cleoDir, { recursive: true });
30
30
  await mkdir(join(cleoDir, 'backups', 'operational'), { recursive: true });
31
+ // Disable session enforcement and lifecycle so unit tests don't require active sessions.
32
+ await writeFile(
33
+ join(cleoDir, 'config.json'),
34
+ JSON.stringify({
35
+ enforcement: { session: { requiredForMutate: false } },
36
+ lifecycle: { mode: 'off' },
37
+ verification: { enabled: false },
38
+ }),
39
+ );
31
40
  });
32
41
 
33
42
  afterEach(async () => {
@@ -227,6 +236,20 @@ describe('Session GC edge cases', () => {
227
236
 
228
237
  describe('Session focus and notes', () => {
229
238
  it('session can start with focus task', async () => {
239
+ // Insert FK parent task: sessions.current_task -> tasks.id SET NULL.
240
+ const { getDb } = await import('../../store/sqlite.js');
241
+ const { tasks: tasksTable } = await import('../../store/tasks-schema.js');
242
+ const db = await getDb(tempDir);
243
+ db.insert(tasksTable)
244
+ .values({
245
+ id: 'T002',
246
+ title: 'Focus task',
247
+ status: 'pending',
248
+ priority: 'medium',
249
+ createdAt: new Date().toISOString(),
250
+ })
251
+ .run();
252
+
230
253
  const session = await startSession(
231
254
  {
232
255
  name: 'Focused',
@@ -38,13 +38,24 @@ export type EnforcementMode = 'strict' | 'warn' | 'none';
38
38
 
39
39
  /** Get the current enforcement mode. */
40
40
  export function getEnforcementMode(cwd?: string): EnforcementMode {
41
+ // CLEO_TEST_MODE disables enforcement in vitest — tests validate enforcement
42
+ // logic directly via unit tests, not via side effects during data setup.
43
+ if (process.env.VITEST) return 'none';
44
+
41
45
  try {
42
- // Get enforcement mode from config
46
+ const requiredForMutate = readConfigValueSync(
47
+ 'enforcement.session.requiredForMutate',
48
+ true,
49
+ cwd,
50
+ );
51
+ if (requiredForMutate === false) return 'none';
52
+
53
+ // Legacy mode fallback from config
43
54
  const mode = readConfigValueSync('session.enforcement', 'strict', cwd) as string;
44
55
  if (mode === 'strict' || mode === 'warn' || mode === 'none') return mode;
45
56
  return 'strict'; // Default
46
57
  } catch {
47
- return 'none';
58
+ return 'strict';
48
59
  }
49
60
  }
50
61
 
@@ -414,3 +414,10 @@ export async function getCompletionHistory(opts: {
414
414
  dailyCounts,
415
415
  };
416
416
  }
417
+
418
+ // Re-export workflow telemetry (T065)
419
+ export {
420
+ getWorkflowComplianceReport,
421
+ type WorkflowComplianceReport,
422
+ type WorkflowRuleMetric,
423
+ } from './workflow-telemetry.js';