@cleocode/core 2026.3.58 → 2026.3.60
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/agents/agent-registry.d.ts +206 -0
- package/dist/agents/agent-registry.d.ts.map +1 -0
- package/dist/agents/agent-registry.js +288 -0
- package/dist/agents/agent-registry.js.map +1 -0
- package/dist/agents/agent-schema.js +5 -0
- package/dist/agents/agent-schema.js.map +1 -1
- package/dist/agents/execution-learning.js +474 -0
- package/dist/agents/execution-learning.js.map +1 -0
- package/dist/agents/health-monitor.d.ts +161 -0
- package/dist/agents/health-monitor.d.ts.map +1 -0
- package/dist/agents/health-monitor.js +217 -0
- package/dist/agents/health-monitor.js.map +1 -0
- package/dist/agents/index.d.ts +3 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +9 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/retry.d.ts +57 -4
- package/dist/agents/retry.d.ts.map +1 -1
- package/dist/agents/retry.js +57 -4
- package/dist/agents/retry.js.map +1 -1
- package/dist/backfill/index.d.ts +27 -0
- package/dist/backfill/index.d.ts.map +1 -1
- package/dist/backfill/index.js +229 -0
- package/dist/backfill/index.js.map +1 -0
- package/dist/bootstrap.d.ts +2 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +135 -28
- package/dist/bootstrap.js.map +1 -1
- package/dist/cleo.d.ts +40 -0
- package/dist/cleo.d.ts.map +1 -1
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1036 -536
- package/dist/index.js.map +4 -4
- package/dist/intelligence/adaptive-validation.js +497 -0
- package/dist/intelligence/adaptive-validation.js.map +1 -0
- package/dist/intelligence/impact.d.ts +34 -1
- package/dist/intelligence/impact.d.ts.map +1 -1
- package/dist/intelligence/impact.js +176 -0
- package/dist/intelligence/impact.js.map +1 -1
- package/dist/intelligence/index.d.ts +2 -2
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/index.js +6 -1
- package/dist/intelligence/index.js.map +1 -1
- package/dist/intelligence/types.d.ts +60 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/internal.d.ts +5 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +11 -2
- package/dist/internal.js.map +1 -1
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/retry.d.ts +128 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/retry.js +152 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/nexus/sharing/index.d.ts +48 -2
- package/dist/nexus/sharing/index.d.ts.map +1 -1
- package/dist/nexus/sharing/index.js +110 -1
- package/dist/nexus/sharing/index.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +22 -2
- package/dist/scaffold.js.map +1 -1
- package/dist/sessions/session-enforcement.js +4 -0
- package/dist/sessions/session-enforcement.js.map +1 -1
- package/dist/stats/index.js +2 -0
- package/dist/stats/index.js.map +1 -1
- package/dist/stats/workflow-telemetry.d.ts +15 -0
- package/dist/stats/workflow-telemetry.d.ts.map +1 -1
- package/dist/stats/workflow-telemetry.js +400 -0
- package/dist/stats/workflow-telemetry.js.map +1 -0
- package/dist/store/brain-schema.js +4 -1
- package/dist/store/brain-schema.js.map +1 -1
- package/dist/store/converters.js +2 -0
- package/dist/store/converters.js.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +35 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -1
- package/dist/store/cross-db-cleanup.js +169 -0
- package/dist/store/cross-db-cleanup.js.map +1 -0
- package/dist/store/db-helpers.js +2 -0
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/migration-sqlite.js +5 -0
- package/dist/store/migration-sqlite.js.map +1 -1
- package/dist/store/sqlite-data-accessor.js +20 -28
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sqlite.js +13 -2
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/task-store.js +4 -0
- package/dist/store/task-store.js.map +1 -1
- package/dist/store/tasks-schema.js +50 -20
- package/dist/store/tasks-schema.js.map +1 -1
- package/dist/tasks/add.js +87 -3
- package/dist/tasks/add.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +15 -4
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/enforcement.d.ts.map +1 -1
- package/dist/tasks/enforcement.js +8 -1
- package/dist/tasks/enforcement.js.map +1 -1
- package/dist/tasks/epic-enforcement.d.ts +61 -0
- package/dist/tasks/epic-enforcement.d.ts.map +1 -1
- package/dist/tasks/epic-enforcement.js +294 -0
- package/dist/tasks/epic-enforcement.js.map +1 -0
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/pipeline-stage.d.ts +70 -1
- package/dist/tasks/pipeline-stage.d.ts.map +1 -1
- package/dist/tasks/pipeline-stage.js +248 -0
- package/dist/tasks/pipeline-stage.js.map +1 -0
- package/dist/tasks/update.js +28 -0
- package/dist/tasks/update.js.map +1 -1
- package/package.json +5 -5
- package/schemas/config.schema.json +37 -1547
- package/src/__tests__/sharing.test.ts +24 -0
- package/src/agents/__tests__/agent-registry.test.ts +351 -0
- package/src/agents/__tests__/health-monitor.test.ts +332 -0
- package/src/agents/agent-registry.ts +394 -0
- package/src/agents/health-monitor.ts +279 -0
- package/src/agents/index.ts +24 -1
- package/src/agents/retry.ts +57 -4
- package/src/backfill/index.ts +27 -0
- package/src/bootstrap.ts +171 -30
- package/src/cleo.ts +103 -2
- package/src/config.ts +3 -3
- package/src/index.ts +1 -0
- package/src/intelligence/__tests__/impact.test.ts +165 -1
- package/src/intelligence/impact.ts +203 -0
- package/src/intelligence/index.ts +3 -0
- package/src/intelligence/types.ts +76 -0
- package/src/internal.ts +20 -0
- package/src/lib/__tests__/retry.test.ts +321 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/retry.ts +224 -0
- package/src/nexus/sharing/index.ts +142 -2
- package/src/scaffold.ts +24 -2
- package/src/stats/workflow-telemetry.ts +15 -0
- package/src/store/__tests__/session-store.test.ts +43 -7
- package/src/store/__tests__/task-store.test.ts +1 -1
- package/src/store/__tests__/test-db-helper.ts +7 -3
- package/src/store/cross-db-cleanup.ts +35 -0
- package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
- package/src/tasks/__tests__/minimal-test.test.ts +2 -2
- package/src/tasks/__tests__/update.test.ts +25 -25
- package/src/tasks/complete.ts +11 -6
- package/src/tasks/enforcement.ts +6 -3
- package/src/tasks/epic-enforcement.ts +61 -0
- package/src/tasks/pipeline-stage.ts +70 -1
- package/templates/config.template.json +5 -116
- package/templates/global-config.template.json +2 -44
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
package/src/scaffold.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { execFile } from 'node:child_process';
|
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { existsSync, constants as fsConstants, readFileSync, statSync } from 'node:fs';
|
|
16
16
|
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
17
|
+
import { homedir as getHomedir } from 'node:os';
|
|
17
18
|
import { dirname, join, resolve } from 'node:path';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
19
20
|
import { promisify } from 'node:util';
|
|
@@ -1278,12 +1279,33 @@ export function checkGlobalTemplates(): CheckResult {
|
|
|
1278
1279
|
};
|
|
1279
1280
|
}
|
|
1280
1281
|
|
|
1282
|
+
// Check version sync between XDG and legacy paths
|
|
1283
|
+
const xdgContent = readFileSync(injectionPath, 'utf-8');
|
|
1284
|
+
const xdgVersion = xdgContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
|
|
1285
|
+
const home = getHomedir();
|
|
1286
|
+
const legacyPath = join(home, '.cleo', 'templates', 'CLEO-INJECTION.md');
|
|
1287
|
+
|
|
1288
|
+
if (existsSync(legacyPath)) {
|
|
1289
|
+
const legacyContent = readFileSync(legacyPath, 'utf-8');
|
|
1290
|
+
const legacyVersion = legacyContent.match(/^Version:\s*(.+)$/m)?.[1]?.trim();
|
|
1291
|
+
if (legacyVersion && xdgVersion && legacyVersion !== xdgVersion) {
|
|
1292
|
+
return {
|
|
1293
|
+
id: 'global_templates',
|
|
1294
|
+
category: 'global',
|
|
1295
|
+
status: 'warning',
|
|
1296
|
+
message: `Legacy template version (${legacyVersion}) out of sync with XDG (${xdgVersion})`,
|
|
1297
|
+
details: { path: injectionPath, exists: true, xdgVersion, legacyVersion, legacyPath },
|
|
1298
|
+
fix: 'npm install -g @cleocode/cleo (reinstall syncs both paths)',
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1281
1303
|
return {
|
|
1282
1304
|
id: 'global_templates',
|
|
1283
1305
|
category: 'global',
|
|
1284
1306
|
status: 'passed',
|
|
1285
|
-
message:
|
|
1286
|
-
details: { path: injectionPath, exists: true },
|
|
1307
|
+
message: `Global injection template present (v${xdgVersion ?? 'unknown'})`,
|
|
1308
|
+
details: { path: injectionPath, exists: true, version: xdgVersion },
|
|
1287
1309
|
fix: null,
|
|
1288
1310
|
};
|
|
1289
1311
|
}
|
|
@@ -229,6 +229,21 @@ function gradeFromScore(score: number): string {
|
|
|
229
229
|
* WF-003: Completed tasks SHOULD have verification gates set (T061)
|
|
230
230
|
* WF-004: Tasks with verification SHOULD have all 3 gates set
|
|
231
231
|
* WF-005: Tasks MUST have session binding on creation (non-epic)
|
|
232
|
+
*
|
|
233
|
+
* @remarks
|
|
234
|
+
* Derives all metrics from existing audit_log and tasks tables — no new
|
|
235
|
+
* tracking infrastructure is required.
|
|
236
|
+
*
|
|
237
|
+
* @param opts - Report options
|
|
238
|
+
* @param opts.since - ISO 8601 date string to filter metrics from
|
|
239
|
+
* @param opts.cwd - Working directory for database resolution
|
|
240
|
+
* @returns Compliance report with per-rule pass/fail counts and overall rate
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* const report = await getWorkflowComplianceReport({ cwd: '/my/project' });
|
|
245
|
+
* console.log(report.overall.passRate); // e.g. 0.85
|
|
246
|
+
* ```
|
|
232
247
|
*/
|
|
233
248
|
export async function getWorkflowComplianceReport(opts: {
|
|
234
249
|
since?: string;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @epic T4638
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import type { Session } from '@cleocode/contracts';
|
|
@@ -404,9 +404,27 @@ describe('SQLite session-store', () => {
|
|
|
404
404
|
const db = await getDb();
|
|
405
405
|
db.insert(tasksTable)
|
|
406
406
|
.values([
|
|
407
|
-
{
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
{
|
|
408
|
+
id: 'T001',
|
|
409
|
+
title: 'Task T001',
|
|
410
|
+
status: 'pending',
|
|
411
|
+
priority: 'medium',
|
|
412
|
+
createdAt: new Date().toISOString(),
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: 'T002',
|
|
416
|
+
title: 'Task T002',
|
|
417
|
+
status: 'pending',
|
|
418
|
+
priority: 'medium',
|
|
419
|
+
createdAt: new Date().toISOString(),
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: 'T003',
|
|
423
|
+
title: 'Task T003',
|
|
424
|
+
status: 'pending',
|
|
425
|
+
priority: 'medium',
|
|
426
|
+
createdAt: new Date().toISOString(),
|
|
427
|
+
},
|
|
410
428
|
])
|
|
411
429
|
.run();
|
|
412
430
|
await createSession(makeSession({ id: 'sess-001' }));
|
|
@@ -430,9 +448,27 @@ describe('SQLite session-store', () => {
|
|
|
430
448
|
const db = await getDb();
|
|
431
449
|
db.insert(tasksTable)
|
|
432
450
|
.values([
|
|
433
|
-
{
|
|
434
|
-
|
|
435
|
-
|
|
451
|
+
{
|
|
452
|
+
id: 'T001',
|
|
453
|
+
title: 'Task T001',
|
|
454
|
+
status: 'pending',
|
|
455
|
+
priority: 'medium',
|
|
456
|
+
createdAt: new Date().toISOString(),
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: 'T002',
|
|
460
|
+
title: 'Task T002',
|
|
461
|
+
status: 'pending',
|
|
462
|
+
priority: 'medium',
|
|
463
|
+
createdAt: new Date().toISOString(),
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: 'T003',
|
|
467
|
+
title: 'Task T003',
|
|
468
|
+
status: 'pending',
|
|
469
|
+
priority: 'medium',
|
|
470
|
+
createdAt: new Date().toISOString(),
|
|
471
|
+
},
|
|
436
472
|
])
|
|
437
473
|
.run();
|
|
438
474
|
await createSession(makeSession({ id: 'sess-001' }));
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @epic T4638
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import type { Task } from '@cleocode/contracts';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @task T5244
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import type { Task } from '@cleocode/contracts';
|
|
@@ -57,7 +57,9 @@ export async function createTestDb(): Promise<TestDbEnv> {
|
|
|
57
57
|
const { readdirSync } = await import('node:fs');
|
|
58
58
|
const contents = readdirSync(cleoDir);
|
|
59
59
|
if (!contents.includes('config.json')) {
|
|
60
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
`createTestDb: config.json not found in ${cleoDir} after write (contents: ${JSON.stringify(contents)})`,
|
|
62
|
+
);
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const accessor = await createSqliteDataAccessor(tempDir);
|
|
@@ -66,7 +68,9 @@ export async function createTestDb(): Promise<TestDbEnv> {
|
|
|
66
68
|
const { readdirSync: readdirSync2 } = await import('node:fs');
|
|
67
69
|
const contentsAfterDb = readdirSync2(cleoDir);
|
|
68
70
|
if (!contentsAfterDb.includes('config.json')) {
|
|
69
|
-
throw new Error(
|
|
71
|
+
throw new Error(
|
|
72
|
+
`createTestDb: config.json DELETED by createSqliteDataAccessor! ${cleoDir}: ${JSON.stringify(contentsAfterDb)}`,
|
|
73
|
+
);
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
return {
|
|
@@ -26,8 +26,17 @@ import { getBrainDb } from './brain-sqlite.js';
|
|
|
26
26
|
* This is a best-effort cleanup — brain.db is a cognitive store and minor
|
|
27
27
|
* staleness is preferable to failing task deletions due to brain.db errors.
|
|
28
28
|
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Best-effort: failures in brain.db cleanup do not propagate to the caller.
|
|
31
|
+
* A background reconciliation pass can clean up any residual stale refs.
|
|
32
|
+
*
|
|
29
33
|
* @param taskId - The ID of the task being deleted from tasks.db
|
|
30
34
|
* @param cwd - Optional working directory
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* await cleanupBrainRefsOnTaskDelete('T042');
|
|
39
|
+
* ```
|
|
31
40
|
*/
|
|
32
41
|
export async function cleanupBrainRefsOnTaskDelete(taskId: string, cwd?: string): Promise<void> {
|
|
33
42
|
let brainDb: Awaited<ReturnType<typeof getBrainDb>> | null = null;
|
|
@@ -84,8 +93,16 @@ export async function cleanupBrainRefsOnTaskDelete(taskId: string, cwd?: string)
|
|
|
84
93
|
* Handles:
|
|
85
94
|
* - XFKB-004: Nullify brain_observations.source_session_id where it matches
|
|
86
95
|
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* Best-effort: failures do not propagate. brain.db may not be initialised.
|
|
98
|
+
*
|
|
87
99
|
* @param sessionId - The ID of the session being deleted from tasks.db
|
|
88
100
|
* @param cwd - Optional working directory
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* await cleanupBrainRefsOnSessionDelete('ses_20260321_abc');
|
|
105
|
+
* ```
|
|
89
106
|
*/
|
|
90
107
|
export async function cleanupBrainRefsOnSessionDelete(
|
|
91
108
|
sessionId: string,
|
|
@@ -116,8 +133,17 @@ export async function cleanupBrainRefsOnSessionDelete(
|
|
|
116
133
|
*
|
|
117
134
|
* Provides write-guard for XFKB-001/002/003 on brain.db insert.
|
|
118
135
|
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Used as a write-guard before inserting cross-DB references into brain.db.
|
|
138
|
+
*
|
|
119
139
|
* @param taskId - Task ID to verify
|
|
120
140
|
* @param tasksDb - The tasks.db drizzle instance
|
|
141
|
+
* @returns True if the task exists in tasks.db
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* if (await taskExistsInTasksDb('T042', db)) { /* safe to reference *\/ }
|
|
146
|
+
* ```
|
|
121
147
|
*/
|
|
122
148
|
export async function taskExistsInTasksDb(
|
|
123
149
|
taskId: string,
|
|
@@ -139,8 +165,17 @@ export async function taskExistsInTasksDb(
|
|
|
139
165
|
*
|
|
140
166
|
* Provides write-guard for XFKB-004 on brain.db insert.
|
|
141
167
|
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* Used as a write-guard before inserting cross-DB references into brain.db.
|
|
170
|
+
*
|
|
142
171
|
* @param sessionId - Session ID to verify
|
|
143
172
|
* @param tasksDb - The tasks.db drizzle instance
|
|
173
|
+
* @returns True if the session exists in tasks.db
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* if (await sessionExistsInTasksDb('ses_abc', db)) { /* safe to reference *\/ }
|
|
178
|
+
* ```
|
|
144
179
|
*/
|
|
145
180
|
export async function sessionExistsInTasksDb(
|
|
146
181
|
sessionId: string,
|
|
@@ -19,8 +19,13 @@ import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-help
|
|
|
19
19
|
|
|
20
20
|
// Epic enforcement tests NEED enforcement active — temporarily clear VITEST
|
|
21
21
|
const savedVitest = process.env.VITEST;
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
delete process.env.VITEST;
|
|
24
|
+
});
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
if (savedVitest) process.env.VITEST = savedVitest;
|
|
27
|
+
});
|
|
28
|
+
|
|
24
29
|
import type { DataAccessor } from '../../store/data-accessor.js';
|
|
25
30
|
import { addTask } from '../add.js';
|
|
26
31
|
import {
|
|
@@ -380,7 +385,7 @@ describe('validateEpicStageAdvancement (strict)', () => {
|
|
|
380
385
|
id: 'T002',
|
|
381
386
|
title: 'Child',
|
|
382
387
|
description: 'Child',
|
|
383
|
-
status: '
|
|
388
|
+
status: 'active',
|
|
384
389
|
priority: 'medium',
|
|
385
390
|
type: 'task',
|
|
386
391
|
parentId: 'T001',
|
|
@@ -416,7 +421,7 @@ describe('validateEpicStageAdvancement (strict)', () => {
|
|
|
416
421
|
id: 'T002',
|
|
417
422
|
title: 'Child',
|
|
418
423
|
description: 'Child',
|
|
419
|
-
status: '
|
|
424
|
+
status: 'active',
|
|
420
425
|
priority: 'medium',
|
|
421
426
|
type: 'task',
|
|
422
427
|
parentId: 'T001',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
|
|
6
6
|
describe('minimal repro', () => {
|
|
7
7
|
let tempDir: string;
|
|
@@ -231,11 +231,11 @@ describe('updateTask', () => {
|
|
|
231
231
|
await writeFile(
|
|
232
232
|
join(env.cleoDir, 'config.json'),
|
|
233
233
|
JSON.stringify({
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
235
|
+
lifecycle: { mode: 'off' },
|
|
236
|
+
verification: { enabled: false },
|
|
237
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
238
|
+
}),
|
|
239
239
|
);
|
|
240
240
|
|
|
241
241
|
const result = await updateTask({ taskId: 'T002', parentId: 'T001' }, env.tempDir, accessor);
|
|
@@ -266,11 +266,11 @@ describe('updateTask', () => {
|
|
|
266
266
|
await writeFile(
|
|
267
267
|
join(env.cleoDir, 'config.json'),
|
|
268
268
|
JSON.stringify({
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
270
|
+
lifecycle: { mode: 'off' },
|
|
271
|
+
verification: { enabled: false },
|
|
272
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
273
|
+
}),
|
|
274
274
|
);
|
|
275
275
|
|
|
276
276
|
const result = await updateTask({ taskId: 'T002', parentId: null }, env.tempDir, accessor);
|
|
@@ -301,11 +301,11 @@ describe('updateTask', () => {
|
|
|
301
301
|
await writeFile(
|
|
302
302
|
join(env.cleoDir, 'config.json'),
|
|
303
303
|
JSON.stringify({
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
305
|
+
lifecycle: { mode: 'off' },
|
|
306
|
+
verification: { enabled: false },
|
|
307
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
308
|
+
}),
|
|
309
309
|
);
|
|
310
310
|
|
|
311
311
|
const result = await updateTask({ taskId: 'T002', parentId: '' }, env.tempDir, accessor);
|
|
@@ -336,11 +336,11 @@ describe('updateTask', () => {
|
|
|
336
336
|
await writeFile(
|
|
337
337
|
join(env.cleoDir, 'config.json'),
|
|
338
338
|
JSON.stringify({
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
340
|
+
lifecycle: { mode: 'off' },
|
|
341
|
+
verification: { enabled: false },
|
|
342
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
343
|
+
}),
|
|
344
344
|
);
|
|
345
345
|
|
|
346
346
|
await expect(
|
|
@@ -370,11 +370,11 @@ describe('updateTask', () => {
|
|
|
370
370
|
await writeFile(
|
|
371
371
|
join(env.cleoDir, 'config.json'),
|
|
372
372
|
JSON.stringify({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
374
|
+
lifecycle: { mode: 'off' },
|
|
375
|
+
verification: { enabled: false },
|
|
376
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
377
|
+
}),
|
|
378
378
|
);
|
|
379
379
|
|
|
380
380
|
const result = await updateTask(
|
package/src/tasks/complete.ts
CHANGED
|
@@ -76,15 +76,18 @@ async function loadCompletionEnforcement(cwd?: string): Promise<CompletionEnforc
|
|
|
76
76
|
const acceptanceMode =
|
|
77
77
|
modeRaw === 'off' || modeRaw === 'warn' || modeRaw === 'block'
|
|
78
78
|
? modeRaw
|
|
79
|
-
: isTest
|
|
79
|
+
: isTest
|
|
80
|
+
? 'off'
|
|
81
|
+
: 'block';
|
|
80
82
|
|
|
81
83
|
const acceptanceRequiredForPriorities = Array.isArray(prioritiesRaw)
|
|
82
84
|
? prioritiesRaw.filter((p): p is string => typeof p === 'string')
|
|
83
|
-
: isTest
|
|
85
|
+
: isTest
|
|
86
|
+
? []
|
|
87
|
+
: ['critical', 'high', 'medium', 'low'];
|
|
84
88
|
|
|
85
|
-
const verificationEnabled =
|
|
86
|
-
: verificationEnabledRaw === false ? false
|
|
87
|
-
: !isTest;
|
|
89
|
+
const verificationEnabled =
|
|
90
|
+
verificationEnabledRaw === true ? true : verificationEnabledRaw === false ? false : !isTest;
|
|
88
91
|
|
|
89
92
|
const verificationRequiredGates = Array.isArray(verificationRequiredGatesRaw)
|
|
90
93
|
? verificationRequiredGatesRaw
|
|
@@ -104,7 +107,9 @@ async function loadCompletionEnforcement(cwd?: string): Promise<CompletionEnforc
|
|
|
104
107
|
lifecycleModeRaw === 'none' ||
|
|
105
108
|
lifecycleModeRaw === 'off'
|
|
106
109
|
? lifecycleModeRaw
|
|
107
|
-
: isTest
|
|
110
|
+
: isTest
|
|
111
|
+
? 'off'
|
|
112
|
+
: 'strict';
|
|
108
113
|
|
|
109
114
|
return {
|
|
110
115
|
acceptanceMode,
|
package/src/tasks/enforcement.ts
CHANGED
|
@@ -41,9 +41,12 @@ export async function createAcceptanceEnforcement(cwd?: string): Promise<Accepta
|
|
|
41
41
|
const minCriteriaRaw = await getRawConfigValue('enforcement.acceptance.minimumCriteria', cwd);
|
|
42
42
|
const defaultPriorityRaw = await getRawConfigValue('defaults.priority', cwd);
|
|
43
43
|
|
|
44
|
-
const mode =
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const mode =
|
|
45
|
+
modeRaw === 'off' || modeRaw === 'warn' || modeRaw === 'block'
|
|
46
|
+
? modeRaw
|
|
47
|
+
: isTest
|
|
48
|
+
? 'off'
|
|
49
|
+
: 'block';
|
|
47
50
|
|
|
48
51
|
const requiredForPriorities = Array.isArray(prioritiesRaw)
|
|
49
52
|
? prioritiesRaw.filter((p): p is string => typeof p === 'string')
|