@cleocode/core 2026.3.61 → 2026.3.63
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 +103 -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/brain-sqlite.d.ts.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/store/brain-sqlite.ts +26 -0
- package/src/system/backup.ts +52 -1
- package/src/tasks/__tests__/add.test.ts +3 -1
- package/src/tasks/add.ts +66 -5
|
@@ -469,6 +469,13 @@ const CAPABILITY_MATRIX: OperationCapability[] = [
|
|
|
469
469
|
mode: 'native',
|
|
470
470
|
preferredChannel: 'either',
|
|
471
471
|
},
|
|
472
|
+
{
|
|
473
|
+
domain: 'admin',
|
|
474
|
+
operation: 'backup',
|
|
475
|
+
gateway: 'query',
|
|
476
|
+
mode: 'native',
|
|
477
|
+
preferredChannel: 'either',
|
|
478
|
+
},
|
|
472
479
|
{
|
|
473
480
|
domain: 'admin',
|
|
474
481
|
operation: 'backup',
|
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;
|
|
@@ -20,6 +20,7 @@ import { readMigrationFiles } from 'drizzle-orm/migrator';
|
|
|
20
20
|
import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite';
|
|
21
21
|
import { drizzle } from 'drizzle-orm/node-sqlite';
|
|
22
22
|
import { migrate } from 'drizzle-orm/node-sqlite/migrator';
|
|
23
|
+
import { getLogger } from '../logger.js';
|
|
23
24
|
import { getCleoDirAbsolute } from '../paths.js';
|
|
24
25
|
import * as brainSchema from './brain-schema.js';
|
|
25
26
|
import { isSqliteBusy, openNativeDatabase } from './sqlite.js';
|
|
@@ -119,6 +120,31 @@ function runBrainMigrations(
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
// Fix #65: Reconcile stale migration journal entries from older CLEO versions.
|
|
124
|
+
// Same pattern as tasks.db fix in sqlite.ts — see #63 for details.
|
|
125
|
+
if (tableExists(nativeDb, '__drizzle_migrations') && tableExists(nativeDb, 'brain_decisions')) {
|
|
126
|
+
const localMigrations = readMigrationFiles({ migrationsFolder });
|
|
127
|
+
const localHashes = new Set(localMigrations.map((m) => m.hash));
|
|
128
|
+
const dbEntries = nativeDb.prepare('SELECT hash FROM "__drizzle_migrations"').all() as Array<{
|
|
129
|
+
hash: string;
|
|
130
|
+
}>;
|
|
131
|
+
const hasOrphanedEntries = dbEntries.some((e) => !localHashes.has(e.hash));
|
|
132
|
+
|
|
133
|
+
if (hasOrphanedEntries) {
|
|
134
|
+
const log = getLogger('brain');
|
|
135
|
+
log.warn(
|
|
136
|
+
{ orphaned: dbEntries.filter((e) => !localHashes.has(e.hash)).length },
|
|
137
|
+
'Detected stale migration journal entries from a previous CLEO version. Reconciling brain.db.',
|
|
138
|
+
);
|
|
139
|
+
nativeDb.exec('DELETE FROM "__drizzle_migrations"');
|
|
140
|
+
for (const m of localMigrations) {
|
|
141
|
+
nativeDb.exec(
|
|
142
|
+
`INSERT INTO "__drizzle_migrations" ("hash", "created_at") VALUES ('${m.hash}', ${m.folderMillis})`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
// Run pending migrations via drizzle-orm/node-sqlite/migrator (synchronous).
|
|
123
149
|
const MAX_RETRIES = 5;
|
|
124
150
|
const BASE_DELAY_MS = 100;
|
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)
|