@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.
- package/dist/cleo.js +36 -1
- package/dist/cleo.js.map +1 -1
- package/dist/index.js +83 -25
- package/dist/index.js.map +3 -3
- package/dist/internal.d.ts +5 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -2
- package/dist/internal.js.map +1 -1
- package/dist/phases/deps.d.ts +1 -1
- package/dist/phases/deps.d.ts.map +1 -1
- package/dist/phases/deps.js +5 -2
- package/dist/phases/deps.js.map +1 -1
- package/dist/repair.js +43 -2
- package/dist/repair.js.map +1 -1
- package/dist/routing/capability-matrix.d.ts.map +1 -1
- package/dist/routing/capability-matrix.js +7 -0
- package/dist/routing/capability-matrix.js.map +1 -1
- package/dist/sequence/index.js +1 -1
- package/dist/sequence/index.js.map +1 -1
- package/dist/stats/index.d.ts.map +1 -1
- package/dist/stats/index.js +4 -2
- package/dist/stats/index.js.map +1 -1
- package/dist/store/sqlite.js +59 -5
- package/dist/store/sqlite.js.map +1 -1
- package/dist/system/backup.d.ts +15 -0
- package/dist/system/backup.d.ts.map +1 -1
- package/dist/system/backup.js +43 -1
- package/dist/system/backup.js.map +1 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/add.js +66 -4
- package/dist/tasks/add.js.map +1 -1
- package/package.json +5 -5
- package/src/internal.ts +6 -3
- package/src/phases/deps.ts +5 -3
- package/src/routing/capability-matrix.ts +7 -0
- package/src/sequence/index.ts +1 -1
- package/src/stats/index.ts +4 -2
- package/src/system/backup.ts +52 -1
- package/src/tasks/__tests__/add.test.ts +3 -1
- package/src/tasks/add.ts +66 -5
package/src/sequence/index.ts
CHANGED
|
@@ -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
|
|
package/src/stats/index.ts
CHANGED
|
@@ -149,8 +149,10 @@ export async function getProjectStats(
|
|
|
149
149
|
)
|
|
150
150
|
.get();
|
|
151
151
|
archivedCompleted = archivedDoneRow?.c ?? 0;
|
|
152
|
-
// totalCompleted
|
|
153
|
-
|
|
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;
|
package/src/system/backup.ts
CHANGED
|
@@ -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
|
-
|
|
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)
|