@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
@@ -0,0 +1,28 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ describe('minimal repro', () => {
7
+ let tempDir: string;
8
+ let cleoDir: string;
9
+
10
+ beforeEach(() => {
11
+ tempDir = mkdtempSync(join(tmpdir(), 'cleo-test-'));
12
+ cleoDir = join(tempDir, '.cleo');
13
+ mkdirSync(cleoDir, { recursive: true });
14
+ writeFileSync(join(cleoDir, 'config.json'), JSON.stringify({ test: true }));
15
+ process.env['CLEO_DIR'] = cleoDir;
16
+ });
17
+
18
+ afterEach(() => {
19
+ delete process.env['CLEO_DIR'];
20
+ });
21
+
22
+ it('config exists in test', () => {
23
+ const contents = readdirSync(cleoDir);
24
+ console.log('contents:', contents);
25
+ console.log('CLEO_DIR:', process.env['CLEO_DIR']);
26
+ expect(existsSync(join(cleoDir, 'config.json'))).toBe(true);
27
+ });
28
+ });
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Tests for pipeline stage binding (T060).
3
+ *
4
+ * Covers:
5
+ * - Stage validation
6
+ * - Auto-assignment on task creation (standalone, epic, under-parent)
7
+ * - Forward-only transition enforcement on update
8
+ * - Stage persistence through rowToTask / taskToRow round-trip
9
+ *
10
+ * @task T060
11
+ * @epic T056
12
+ */
13
+
14
+ import { writeFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+ import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
18
+ import type { DataAccessor } from '../../store/data-accessor.js';
19
+ import { addTask } from '../add.js';
20
+
21
+ /** Config that disables session, acceptance, and lifecycle enforcement for test isolation. */
22
+ const NO_SESSION_CONFIG = JSON.stringify({
23
+ lifecycle: { mode: 'off' },
24
+ enforcement: {
25
+ session: { requiredForMutate: false },
26
+ acceptance: { mode: 'off' },
27
+ },
28
+ verification: { enabled: false },
29
+ });
30
+
31
+ import {
32
+ getPipelineStageOrder,
33
+ isPipelineTransitionForward,
34
+ isValidPipelineStage,
35
+ resolveDefaultPipelineStage,
36
+ TASK_PIPELINE_STAGES,
37
+ validatePipelineStage,
38
+ validatePipelineTransition,
39
+ } from '../pipeline-stage.js';
40
+ import { updateTask } from '../update.js';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Unit tests — pure functions
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('isValidPipelineStage', () => {
47
+ it('returns true for all canonical stages', () => {
48
+ for (const stage of TASK_PIPELINE_STAGES) {
49
+ expect(isValidPipelineStage(stage)).toBe(true);
50
+ }
51
+ });
52
+
53
+ it('returns false for unknown strings', () => {
54
+ expect(isValidPipelineStage('unknown')).toBe(false);
55
+ expect(isValidPipelineStage('')).toBe(false);
56
+ expect(isValidPipelineStage('RESEARCH')).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('validatePipelineStage', () => {
61
+ it('does not throw for valid stages', () => {
62
+ expect(() => validatePipelineStage('research')).not.toThrow();
63
+ expect(() => validatePipelineStage('implementation')).not.toThrow();
64
+ expect(() => validatePipelineStage('contribution')).not.toThrow();
65
+ });
66
+
67
+ it('throws CleoError for invalid stage', () => {
68
+ expect(() => validatePipelineStage('invalid')).toThrow('Invalid pipeline stage');
69
+ });
70
+ });
71
+
72
+ describe('getPipelineStageOrder', () => {
73
+ it('returns expected order values', () => {
74
+ expect(getPipelineStageOrder('research')).toBe(1);
75
+ expect(getPipelineStageOrder('implementation')).toBe(6);
76
+ expect(getPipelineStageOrder('release')).toBe(9);
77
+ expect(getPipelineStageOrder('contribution')).toBe(10);
78
+ });
79
+
80
+ it('returns -1 for unknown stage', () => {
81
+ expect(getPipelineStageOrder('unknown')).toBe(-1);
82
+ });
83
+ });
84
+
85
+ describe('isPipelineTransitionForward', () => {
86
+ it('allows same stage (no-op)', () => {
87
+ expect(isPipelineTransitionForward('research', 'research')).toBe(true);
88
+ expect(isPipelineTransitionForward('implementation', 'implementation')).toBe(true);
89
+ });
90
+
91
+ it('allows forward transitions', () => {
92
+ expect(isPipelineTransitionForward('research', 'consensus')).toBe(true);
93
+ expect(isPipelineTransitionForward('research', 'implementation')).toBe(true);
94
+ expect(isPipelineTransitionForward('implementation', 'release')).toBe(true);
95
+ });
96
+
97
+ it('rejects backward transitions', () => {
98
+ expect(isPipelineTransitionForward('implementation', 'research')).toBe(false);
99
+ expect(isPipelineTransitionForward('testing', 'specification')).toBe(false);
100
+ expect(isPipelineTransitionForward('release', 'implementation')).toBe(false);
101
+ });
102
+
103
+ it('allows transitions with unknown stages (defensive)', () => {
104
+ expect(isPipelineTransitionForward('unknown', 'research')).toBe(true);
105
+ expect(isPipelineTransitionForward('research', 'unknown')).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe('validatePipelineTransition', () => {
110
+ it('allows forward transitions', () => {
111
+ expect(() => validatePipelineTransition('research', 'implementation')).not.toThrow();
112
+ expect(() => validatePipelineTransition('implementation', 'testing')).not.toThrow();
113
+ });
114
+
115
+ it('allows same-stage (no-op)', () => {
116
+ expect(() => validatePipelineTransition('implementation', 'implementation')).not.toThrow();
117
+ });
118
+
119
+ it('allows any transition from null/undefined current stage', () => {
120
+ expect(() => validatePipelineTransition(null, 'implementation')).not.toThrow();
121
+ expect(() => validatePipelineTransition(undefined, 'research')).not.toThrow();
122
+ });
123
+
124
+ it('throws for backward transitions', () => {
125
+ expect(() => validatePipelineTransition('implementation', 'research')).toThrow(
126
+ 'cannot move backward',
127
+ );
128
+ expect(() => validatePipelineTransition('testing', 'specification')).toThrow(
129
+ 'cannot move backward',
130
+ );
131
+ });
132
+
133
+ it('throws for invalid new stage', () => {
134
+ expect(() => validatePipelineTransition(null, 'invalid')).toThrow('Invalid pipeline stage');
135
+ });
136
+ });
137
+
138
+ describe('resolveDefaultPipelineStage', () => {
139
+ it('returns explicit stage when provided and valid', () => {
140
+ expect(resolveDefaultPipelineStage({ explicitStage: 'testing' })).toBe('testing');
141
+ });
142
+
143
+ it('inherits from parent pipeline stage', () => {
144
+ const result = resolveDefaultPipelineStage({
145
+ parentTask: { pipelineStage: 'specification', type: 'epic' },
146
+ });
147
+ expect(result).toBe('specification');
148
+ });
149
+
150
+ it('defaults to research for epics', () => {
151
+ expect(resolveDefaultPipelineStage({ taskType: 'epic' })).toBe('research');
152
+ });
153
+
154
+ it('defaults to implementation for standalone tasks', () => {
155
+ expect(resolveDefaultPipelineStage({ taskType: 'task' })).toBe('implementation');
156
+ expect(resolveDefaultPipelineStage({})).toBe('implementation');
157
+ });
158
+
159
+ it('explicit stage takes priority over parent inheritance', () => {
160
+ const result = resolveDefaultPipelineStage({
161
+ explicitStage: 'testing',
162
+ parentTask: { pipelineStage: 'research', type: 'epic' },
163
+ });
164
+ expect(result).toBe('testing');
165
+ });
166
+
167
+ it('parent inheritance takes priority over type default', () => {
168
+ const result = resolveDefaultPipelineStage({
169
+ taskType: 'task', // would default to implementation
170
+ parentTask: { pipelineStage: 'specification', type: 'epic' }, // but parent overrides
171
+ });
172
+ expect(result).toBe('specification');
173
+ });
174
+
175
+ it('ignores parent with invalid pipelineStage', () => {
176
+ const result = resolveDefaultPipelineStage({
177
+ taskType: 'task',
178
+ parentTask: { pipelineStage: 'invalid_stage', type: 'epic' },
179
+ });
180
+ // Falls through to task default
181
+ expect(result).toBe('implementation');
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Integration tests — addTask + updateTask with real SQLite
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe('addTask pipeline stage auto-assignment', () => {
190
+ let env: TestDbEnv;
191
+ let accessor: DataAccessor;
192
+
193
+ beforeEach(async () => {
194
+ env = await createTestDb();
195
+ accessor = env.accessor;
196
+ // Disable session enforcement so tests run without an active session
197
+ await writeFile(join(env.cleoDir, 'config.json'), NO_SESSION_CONFIG);
198
+ });
199
+
200
+ afterEach(async () => {
201
+ await env.cleanup();
202
+ });
203
+
204
+ it('assigns implementation to a standalone task by default', async () => {
205
+ const result = await addTask(
206
+ { title: 'Standalone task', description: 'A standalone task without explicit stage' },
207
+ env.tempDir,
208
+ accessor,
209
+ );
210
+ expect(result.task.pipelineStage).toBe('implementation');
211
+ });
212
+
213
+ it('assigns research to an epic by default', async () => {
214
+ const result = await addTask(
215
+ {
216
+ title: 'My epic',
217
+ description: 'An epic with default stage',
218
+ type: 'epic',
219
+ },
220
+ env.tempDir,
221
+ accessor,
222
+ );
223
+ expect(result.task.pipelineStage).toBe('research');
224
+ });
225
+
226
+ it('respects explicit pipelineStage on creation', async () => {
227
+ const result = await addTask(
228
+ {
229
+ title: 'Testing task',
230
+ description: 'Task at testing stage',
231
+ pipelineStage: 'testing',
232
+ },
233
+ env.tempDir,
234
+ accessor,
235
+ );
236
+ expect(result.task.pipelineStage).toBe('testing');
237
+ });
238
+
239
+ it('inherits parent pipeline stage when creating child task', async () => {
240
+ // Create epic at specification stage
241
+ const epicResult = await addTask(
242
+ {
243
+ title: 'Parent epic',
244
+ description: 'Epic at specification stage',
245
+ type: 'epic',
246
+ pipelineStage: 'specification',
247
+ },
248
+ env.tempDir,
249
+ accessor,
250
+ );
251
+ const epicId = epicResult.task.id;
252
+
253
+ // Create child task under that epic — should inherit specification
254
+ const childResult = await addTask(
255
+ {
256
+ title: 'Child task',
257
+ description: 'Child task inheriting parent stage',
258
+ parentId: epicId,
259
+ },
260
+ env.tempDir,
261
+ accessor,
262
+ );
263
+ expect(childResult.task.pipelineStage).toBe('specification');
264
+ });
265
+
266
+ it('rejects invalid pipelineStage on creation', async () => {
267
+ await expect(
268
+ addTask(
269
+ {
270
+ title: 'Bad stage',
271
+ description: 'Task with invalid stage',
272
+ pipelineStage: 'not_a_real_stage',
273
+ },
274
+ env.tempDir,
275
+ accessor,
276
+ ),
277
+ ).rejects.toThrow('Invalid pipeline stage');
278
+ });
279
+
280
+ it('persists pipelineStage through round-trip', async () => {
281
+ await addTask(
282
+ {
283
+ title: 'Persist test',
284
+ description: 'Test that stage persists to DB',
285
+ pipelineStage: 'validation',
286
+ },
287
+ env.tempDir,
288
+ accessor,
289
+ );
290
+
291
+ // Load back from DB
292
+ const loaded = await accessor.loadSingleTask('T001');
293
+ expect(loaded?.pipelineStage).toBe('validation');
294
+ });
295
+ });
296
+
297
+ describe('updateTask pipeline stage transitions', () => {
298
+ let env: TestDbEnv;
299
+ let accessor: DataAccessor;
300
+
301
+ beforeEach(async () => {
302
+ env = await createTestDb();
303
+ accessor = env.accessor;
304
+ // Disable session enforcement so tests run without an active session
305
+ await writeFile(join(env.cleoDir, 'config.json'), NO_SESSION_CONFIG);
306
+ });
307
+
308
+ afterEach(async () => {
309
+ await env.cleanup();
310
+ });
311
+
312
+ it('allows forward stage transition', async () => {
313
+ // Create at implementation stage
314
+ await addTask(
315
+ {
316
+ title: 'Transition test',
317
+ description: 'Task for transition testing',
318
+ pipelineStage: 'implementation',
319
+ },
320
+ env.tempDir,
321
+ accessor,
322
+ );
323
+
324
+ // Move forward to validation
325
+ const result = await updateTask(
326
+ { taskId: 'T001', pipelineStage: 'validation' },
327
+ env.tempDir,
328
+ accessor,
329
+ );
330
+ expect(result.task.pipelineStage).toBe('validation');
331
+ expect(result.changes).toContain('pipelineStage');
332
+ });
333
+
334
+ it('allows same-stage transition (no-op)', async () => {
335
+ await addTask(
336
+ {
337
+ title: 'Same stage test',
338
+ description: 'Task for same-stage transition test',
339
+ pipelineStage: 'implementation',
340
+ },
341
+ env.tempDir,
342
+ accessor,
343
+ );
344
+
345
+ const result = await updateTask(
346
+ { taskId: 'T001', pipelineStage: 'implementation' },
347
+ env.tempDir,
348
+ accessor,
349
+ );
350
+ expect(result.task.pipelineStage).toBe('implementation');
351
+ });
352
+
353
+ it('rejects backward stage transition', async () => {
354
+ // Create at testing stage
355
+ await addTask(
356
+ {
357
+ title: 'Backward test',
358
+ description: 'Task that should not go backward',
359
+ pipelineStage: 'testing',
360
+ },
361
+ env.tempDir,
362
+ accessor,
363
+ );
364
+
365
+ // Attempt to move backward to implementation
366
+ await expect(
367
+ updateTask({ taskId: 'T001', pipelineStage: 'implementation' }, env.tempDir, accessor),
368
+ ).rejects.toThrow('cannot move backward');
369
+ });
370
+
371
+ it('rejects invalid stage on update', async () => {
372
+ await addTask(
373
+ {
374
+ title: 'Invalid update',
375
+ description: 'Task with upcoming invalid update',
376
+ pipelineStage: 'implementation',
377
+ },
378
+ env.tempDir,
379
+ accessor,
380
+ );
381
+
382
+ await expect(
383
+ updateTask({ taskId: 'T001', pipelineStage: 'not_real' }, env.tempDir, accessor),
384
+ ).rejects.toThrow('Invalid pipeline stage');
385
+ });
386
+
387
+ it('persists updated stage to DB', async () => {
388
+ await addTask(
389
+ {
390
+ title: 'Persist update',
391
+ description: 'Task for update persistence test',
392
+ pipelineStage: 'implementation',
393
+ },
394
+ env.tempDir,
395
+ accessor,
396
+ );
397
+
398
+ await updateTask({ taskId: 'T001', pipelineStage: 'validation' }, env.tempDir, accessor);
399
+
400
+ const loaded = await accessor.loadSingleTask('T001');
401
+ expect(loaded?.pipelineStage).toBe('validation');
402
+ });
403
+ });
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
10
  import { createTestDb, seedTasks, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
11
11
  import type { DataAccessor } from '../../store/data-accessor.js';
12
+ import { resetDbState } from '../../store/sqlite.js';
12
13
  import { updateTask } from '../update.js';
13
14
 
14
15
  describe('updateTask', () => {
@@ -18,13 +19,21 @@ describe('updateTask', () => {
18
19
  beforeEach(async () => {
19
20
  env = await createTestDb();
20
21
  accessor = env.accessor;
22
+ // Pin CLEO_DIR so concurrent workers cannot contaminate path resolution
23
+ process.env['CLEO_DIR'] = env.cleoDir;
21
24
  await writeFile(
22
25
  join(env.cleoDir, 'config.json'),
23
- JSON.stringify({ verification: { enabled: false } }),
26
+ JSON.stringify({
27
+ enforcement: { session: { requiredForMutate: false } },
28
+ lifecycle: { mode: 'off' },
29
+ verification: { enabled: false },
30
+ }),
24
31
  );
25
32
  });
26
33
 
27
34
  afterEach(async () => {
35
+ delete process.env['CLEO_DIR'];
36
+ resetDbState();
28
37
  await env.cleanup();
29
38
  });
30
39
 
@@ -221,7 +230,12 @@ describe('updateTask', () => {
221
230
  ]);
222
231
  await writeFile(
223
232
  join(env.cleoDir, 'config.json'),
224
- JSON.stringify({ hierarchy: { maxDepth: 3, maxSiblings: 20 } }),
233
+ JSON.stringify({
234
+ enforcement: { session: { requiredForMutate: false } },
235
+ lifecycle: { mode: 'off' },
236
+ verification: { enabled: false },
237
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
238
+ }),
225
239
  );
226
240
 
227
241
  const result = await updateTask({ taskId: 'T002', parentId: 'T001' }, env.tempDir, accessor);
@@ -251,7 +265,12 @@ describe('updateTask', () => {
251
265
  ]);
252
266
  await writeFile(
253
267
  join(env.cleoDir, 'config.json'),
254
- JSON.stringify({ hierarchy: { maxDepth: 3, maxSiblings: 20 } }),
268
+ JSON.stringify({
269
+ enforcement: { session: { requiredForMutate: false } },
270
+ lifecycle: { mode: 'off' },
271
+ verification: { enabled: false },
272
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
273
+ }),
255
274
  );
256
275
 
257
276
  const result = await updateTask({ taskId: 'T002', parentId: null }, env.tempDir, accessor);
@@ -281,7 +300,12 @@ describe('updateTask', () => {
281
300
  ]);
282
301
  await writeFile(
283
302
  join(env.cleoDir, 'config.json'),
284
- JSON.stringify({ hierarchy: { maxDepth: 3, maxSiblings: 20 } }),
303
+ JSON.stringify({
304
+ enforcement: { session: { requiredForMutate: false } },
305
+ lifecycle: { mode: 'off' },
306
+ verification: { enabled: false },
307
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
308
+ }),
285
309
  );
286
310
 
287
311
  const result = await updateTask({ taskId: 'T002', parentId: '' }, env.tempDir, accessor);
@@ -311,7 +335,12 @@ describe('updateTask', () => {
311
335
  ]);
312
336
  await writeFile(
313
337
  join(env.cleoDir, 'config.json'),
314
- JSON.stringify({ hierarchy: { maxDepth: 3, maxSiblings: 20 } }),
338
+ JSON.stringify({
339
+ enforcement: { session: { requiredForMutate: false } },
340
+ lifecycle: { mode: 'off' },
341
+ verification: { enabled: false },
342
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
343
+ }),
315
344
  );
316
345
 
317
346
  await expect(
@@ -340,7 +369,12 @@ describe('updateTask', () => {
340
369
  ]);
341
370
  await writeFile(
342
371
  join(env.cleoDir, 'config.json'),
343
- JSON.stringify({ hierarchy: { maxDepth: 3, maxSiblings: 20 } }),
372
+ JSON.stringify({
373
+ enforcement: { session: { requiredForMutate: false } },
374
+ lifecycle: { mode: 'off' },
375
+ verification: { enabled: false },
376
+ hierarchy: { maxDepth: 3, maxSiblings: 20 },
377
+ }),
344
378
  );
345
379
 
346
380
  const result = await updateTask(