@cleocode/core 2026.3.61 → 2026.3.62

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 (40) hide show
  1. package/dist/cleo.js +36 -1
  2. package/dist/cleo.js.map +1 -1
  3. package/dist/index.js +83 -25
  4. package/dist/index.js.map +3 -3
  5. package/dist/internal.d.ts +5 -3
  6. package/dist/internal.d.ts.map +1 -1
  7. package/dist/internal.js +4 -2
  8. package/dist/internal.js.map +1 -1
  9. package/dist/phases/deps.d.ts +1 -1
  10. package/dist/phases/deps.d.ts.map +1 -1
  11. package/dist/phases/deps.js +5 -2
  12. package/dist/phases/deps.js.map +1 -1
  13. package/dist/repair.js +43 -2
  14. package/dist/repair.js.map +1 -1
  15. package/dist/routing/capability-matrix.d.ts.map +1 -1
  16. package/dist/routing/capability-matrix.js +7 -0
  17. package/dist/routing/capability-matrix.js.map +1 -1
  18. package/dist/sequence/index.js +1 -1
  19. package/dist/sequence/index.js.map +1 -1
  20. package/dist/stats/index.d.ts.map +1 -1
  21. package/dist/stats/index.js +4 -2
  22. package/dist/stats/index.js.map +1 -1
  23. package/dist/store/sqlite.js +59 -5
  24. package/dist/store/sqlite.js.map +1 -1
  25. package/dist/system/backup.d.ts +15 -0
  26. package/dist/system/backup.d.ts.map +1 -1
  27. package/dist/system/backup.js +43 -1
  28. package/dist/system/backup.js.map +1 -1
  29. package/dist/tasks/add.d.ts.map +1 -1
  30. package/dist/tasks/add.js +66 -4
  31. package/dist/tasks/add.js.map +1 -1
  32. package/package.json +5 -5
  33. package/src/internal.ts +6 -3
  34. package/src/phases/deps.ts +5 -3
  35. package/src/routing/capability-matrix.ts +7 -0
  36. package/src/sequence/index.ts +1 -1
  37. package/src/stats/index.ts +4 -2
  38. package/src/system/backup.ts +52 -1
  39. package/src/tasks/__tests__/add.test.ts +3 -1
  40. package/src/tasks/add.ts +66 -5
@@ -168,7 +168,7 @@ export async function showSequence(cwd?: string): Promise<Record<string, unknown
168
168
  counter: seq.counter,
169
169
  lastId: seq.lastId,
170
170
  checksum: seq.checksum,
171
- nextId: `T${seq.counter + 1}`,
171
+ nextId: `T${String(seq.counter + 1).padStart(3, '0')}`,
172
172
  };
173
173
  }
174
174
 
@@ -149,8 +149,10 @@ export async function getProjectStats(
149
149
  )
150
150
  .get();
151
151
  archivedCompleted = archivedDoneRow?.c ?? 0;
152
- // totalCompleted = currently done (not yet archived) + archived-as-completed
153
- totalCompleted = (statusMap['done'] ?? 0) + archivedCompleted;
152
+ // totalCompleted: use audit log as SSoT (same source as completedInPeriod) to ensure
153
+ // the two metrics are consistent. DB-based status counts under-count because they miss
154
+ // tasks that were completed then cancelled, deleted, or archived with a non-default reason.
155
+ totalCompleted = entries.filter(isComplete).length;
154
156
  } catch {
155
157
  // fallback to audit_log counts if DB unavailable
156
158
  totalCreated = entries.filter(isCreate).length;
@@ -3,7 +3,7 @@
3
3
  * @task T4783
4
4
  */
5
5
 
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { ExitCode } from '@cleocode/contracts';
9
9
  import { CleoError } from '../errors.js';
@@ -80,6 +80,57 @@ export function createBackup(
80
80
  return { backupId, path: backupDir, timestamp, type: btype, files: backedUp };
81
81
  }
82
82
 
83
+ /** A single backup entry returned by listSystemBackups. */
84
+ export interface BackupEntry {
85
+ backupId: string;
86
+ type: string;
87
+ timestamp: string;
88
+ note?: string;
89
+ files: string[];
90
+ }
91
+
92
+ /**
93
+ * List all available system backups (snapshot, safety, migration types).
94
+ * Reads `.meta.json` sidecar files written by createBackup.
95
+ * This is a pure read operation — it does not modify any files.
96
+ * @task T4783
97
+ */
98
+ export function listSystemBackups(projectRoot: string): BackupEntry[] {
99
+ const cleoDir = join(projectRoot, '.cleo');
100
+ const backupTypes = ['snapshot', 'safety', 'migration'];
101
+ const entries: BackupEntry[] = [];
102
+
103
+ for (const btype of backupTypes) {
104
+ const backupDir = join(cleoDir, 'backups', btype);
105
+ if (!existsSync(backupDir)) continue;
106
+ try {
107
+ const files = readdirSync(backupDir).filter((f) => f.endsWith('.meta.json'));
108
+ for (const metaFile of files) {
109
+ try {
110
+ const raw = readFileSync(join(backupDir, metaFile), 'utf-8');
111
+ const meta = JSON.parse(raw) as Partial<BackupEntry>;
112
+ if (meta.backupId && meta.timestamp) {
113
+ entries.push({
114
+ backupId: meta.backupId,
115
+ type: meta.type ?? btype,
116
+ timestamp: meta.timestamp,
117
+ note: meta.note,
118
+ files: meta.files ?? [],
119
+ });
120
+ }
121
+ } catch {
122
+ // skip malformed meta files
123
+ }
124
+ }
125
+ } catch {
126
+ // skip unreadable backup directories
127
+ }
128
+ }
129
+
130
+ // Sort newest first
131
+ return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
132
+ }
133
+
83
134
  /** Restore from a backup. */
84
135
  export function restoreBackup(
85
136
  projectRoot: string,
@@ -286,7 +286,9 @@ describe('addTask (integration)', () => {
286
286
  accessor,
287
287
  );
288
288
  expect(result.dryRun).toBe(true);
289
- expect(result.task.id).toBe('T001');
289
+ // Dry run does not allocate a real sequence ID — the task is a preview only
290
+ expect(result.task.id).toBe('T???');
291
+ expect(result.task.title).toBe('Dry run task');
290
292
  });
291
293
 
292
294
  it('validates parent hierarchy', async () => {
package/src/tasks/add.ts CHANGED
@@ -645,6 +645,72 @@ export async function addTask(
645
645
  return { task: duplicate, duplicate: true };
646
646
  }
647
647
 
648
+ // Dry run: build a preview task without allocating a sequence ID or writing to the DB.
649
+ // Must be checked before allocateNextTaskId to avoid advancing the counter on no-op runs.
650
+ if (options.dryRun) {
651
+ const previewNow = new Date().toISOString();
652
+
653
+ // Resolve pipeline stage for the preview without any DB writes
654
+ let previewParentForStage: import('./pipeline-stage.js').ResolvedParent | null = null;
655
+ if (parentId) {
656
+ const previewParentTask = await dataAccessor.loadSingleTask(parentId);
657
+ previewParentForStage = previewParentTask
658
+ ? { pipelineStage: previewParentTask.pipelineStage, type: previewParentTask.type }
659
+ : null;
660
+ }
661
+ const previewPipelineStage = resolveDefaultPipelineStage({
662
+ explicitStage: options.pipelineStage,
663
+ taskType: taskType ?? null,
664
+ parentTask: previewParentForStage,
665
+ });
666
+ const previewPosition =
667
+ options.position !== undefined
668
+ ? options.position
669
+ : await dataAccessor.getNextPosition(parentId);
670
+
671
+ const previewTask: Task = {
672
+ id: 'T???',
673
+ title: options.title,
674
+ description: options.description,
675
+ status,
676
+ priority,
677
+ type: taskType,
678
+ parentId: parentId || null,
679
+ position: previewPosition,
680
+ positionVersion: 0,
681
+ size,
682
+ pipelineStage: previewPipelineStage,
683
+ createdAt: previewNow,
684
+ updatedAt: previewNow,
685
+ };
686
+ if (phase) previewTask.phase = phase;
687
+ if (options.labels?.length) previewTask.labels = options.labels.map((l) => l.trim());
688
+ if (options.files?.length) previewTask.files = options.files.map((f) => f.trim());
689
+ if (options.acceptance?.length)
690
+ previewTask.acceptance = options.acceptance.map((a) => a.trim());
691
+ if (options.depends?.length) previewTask.depends = options.depends.map((d) => d.trim());
692
+ if (options.notes) {
693
+ const previewNote = `${new Date()
694
+ .toISOString()
695
+ .replace('T', ' ')
696
+ .replace(/\.\d+Z$/, ' UTC')}: ${options.notes}`;
697
+ previewTask.notes = [previewNote];
698
+ }
699
+ if (status === 'blocked' && options.description) {
700
+ previewTask.blockedBy = options.description;
701
+ }
702
+ if (status === 'done') {
703
+ previewTask.completedAt = previewNow;
704
+ }
705
+ if (taskType !== 'epic') {
706
+ const verificationEnabledRaw = await getRawConfigValue('verification.enabled', cwd);
707
+ if (verificationEnabledRaw === true) {
708
+ previewTask.verification = buildDefaultVerification(previewNow);
709
+ }
710
+ }
711
+ return { task: previewTask, dryRun: true };
712
+ }
713
+
648
714
  const taskId = await allocateNextTaskId(cwd);
649
715
 
650
716
  const now = new Date().toISOString();
@@ -741,11 +807,6 @@ export async function addTask(
741
807
  }
742
808
  }
743
809
 
744
- // Dry run
745
- if (options.dryRun) {
746
- return { task, dryRun: true };
747
- }
748
-
749
810
  // Wrap all writes in a transaction for TOCTOU safety (T023)
750
811
  await dataAccessor.transaction(async (tx: TransactionAccessor) => {
751
812
  // Position shuffling via bulk SQL update (T025)