@dtlabs/ai-toolkit 0.10.1 → 0.12.0
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/CHANGELOG.md +108 -0
- package/bin/cli.js +1457 -143
- package/docs/installation.md +74 -8
- package/docs/reference.md +60 -12
- package/lib/asset-catalog.js +52 -0
- package/lib/execution-ledger.js +515 -0
- package/package.json +9 -7
- package/{.claude → src/claude}/agents/define-feature.md +36 -60
- package/{.claude → src/claude}/agents/install-toolkit.md +30 -103
- package/{.claude → src/claude}/scripts/wb-render.js +101 -2
- package/{.claude → src/claude}/scripts/wb-validate.js +32 -2
- package/{.claude → src/claude}/skills/implement-feature/SKILL.md +4 -2
- package/{.claude → src/claude}/workflows/am-phase1.js +9 -12
- package/{.claude → src/claude}/workflows/pm-phase1.js +25 -75
- package/{.claude → src/claude}/workflows/pm-phase2.js +57 -108
- package/{.claude → src/claude}/workflows/pm-phase3.js +162 -75
- package/.claude/scripts/.gitkeep +0 -0
- /package/{.claude → src/claude}/agents/concurrency-safety-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/dependency-injection-refactoring.md +0 -0
- /package/{.claude → src/claude}/agents/dependency-supply-chain-security.md +0 -0
- /package/{.claude → src/claude}/agents/developer-backend.md +0 -0
- /package/{.claude → src/claude}/agents/developer-frontend.md +0 -0
- /package/{.claude → src/claude}/agents/developer-testing.md +0 -0
- /package/{.claude → src/claude}/agents/domain-model-refactoring.md +0 -0
- /package/{.claude → src/claude}/agents/generate-requirements.md +0 -0
- /package/{.claude → src/claude}/agents/generate-tech-spec.md +0 -0
- /package/{.claude → src/claude}/agents/generate-work-breakdown.md +0 -0
- /package/{.claude → src/claude}/agents/generic-software-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/god-class-decomposition.md +0 -0
- /package/{.claude → src/claude}/agents/init-agents-md.md +0 -0
- /package/{.claude → src/claude}/agents/intervention-documentation-standard.md +0 -0
- /package/{.claude → src/claude}/agents/layered-architecture-assessment.md +0 -0
- /package/{.claude → src/claude}/agents/review-solution.md +0 -0
- /package/{.claude → src/claude}/agents/security-hardening.md +0 -0
- /package/{.claude → src/claude}/agents/validate-feature-docs.md +0 -0
- /package/{.claude → src/claude}/agents/validate-work-breakdown-semantic.md +0 -0
- /package/{.claude → src/claude}/commands/assessment-status.md +0 -0
- /package/{.claude → src/claude}/commands/check-docs.md +0 -0
- /package/{.claude → src/claude}/commands/check-interventions.md +0 -0
- /package/{.claude → src/claude}/commands/feature-status.md +0 -0
- /package/{.claude → src/claude}/commands/next-intervention.md +0 -0
- /package/{.claude → src/claude}/commands/next-task.md +0 -0
- /package/{.claude → src/claude}/commands/pr-description.md +0 -0
- /package/{.claude → src/claude}/skills/assess-codebase/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/define-feature/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/hi-gaia/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/init-agents/SKILL.md +0 -0
- /package/{.claude → src/claude}/skills/install-toolkit/SKILL.md +0 -0
- /package/{.claude → src/claude}/workflows/am-phase2.js +0 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// ── Internal I/O helpers (stubs — implemented by INFRA-TASK-BE-03) ──────────
|
|
9
|
+
|
|
10
|
+
function _readLedger(ledgerPath) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = fs.readFileSync(ledgerPath, 'utf8');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err.code === 'ENOENT') {
|
|
16
|
+
return { entries: [] };
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
} catch (parseErr) {
|
|
24
|
+
if (raw.length > 0) {
|
|
25
|
+
_backupCorruptFile(ledgerPath);
|
|
26
|
+
const err = new Error(
|
|
27
|
+
'corrupt ledger at ' + ledgerPath + ': ' + parseErr.message +
|
|
28
|
+
' — original preserved unchanged, corrupt content backed up to sidecar'
|
|
29
|
+
);
|
|
30
|
+
err.code = 'CORRUPT_LEDGER';
|
|
31
|
+
err.ledgerPath = ledgerPath;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
throw parseErr;
|
|
35
|
+
}
|
|
36
|
+
return { entries: Array.isArray(parsed) ? parsed : [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _writeLedger(ledgerPath, entries) {
|
|
40
|
+
const dir = path.dirname(ledgerPath);
|
|
41
|
+
const tmpName = '.' + path.basename(ledgerPath) + '.tmp-' + process.pid + '-' + Date.now();
|
|
42
|
+
const tmpPath = path.join(dir, tmpName);
|
|
43
|
+
const jsonStr = JSON.stringify(entries, null, 2);
|
|
44
|
+
|
|
45
|
+
let fd;
|
|
46
|
+
try {
|
|
47
|
+
fd = fs.openSync(tmpPath, 'w');
|
|
48
|
+
fs.writeSync(fd, jsonStr);
|
|
49
|
+
fs.fsyncSync(fd);
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
fd = undefined;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (fd !== undefined) {
|
|
54
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
55
|
+
}
|
|
56
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
fs.renameSync(tmpPath, ledgerPath);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
64
|
+
try { fs.unlinkSync(ledgerPath); } catch (_) {}
|
|
65
|
+
fs.renameSync(tmpPath, ledgerPath);
|
|
66
|
+
} else {
|
|
67
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _backupCorruptFile(ledgerPath) {
|
|
74
|
+
const dir = path.dirname(ledgerPath);
|
|
75
|
+
const base = path.basename(ledgerPath);
|
|
76
|
+
const sidecarName = base + '.backup-corrupt-' + Date.now();
|
|
77
|
+
const sidecarPath = path.join(dir, sidecarName);
|
|
78
|
+
const content = fs.readFileSync(ledgerPath);
|
|
79
|
+
fs.writeFileSync(sidecarPath, content);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Internal lock helpers ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function _sleepSync(ms) {
|
|
85
|
+
const end = Date.now() + ms;
|
|
86
|
+
while (Date.now() < end) { /* busy-wait for synchronous cross-process backoff */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _acquireLock(lockPath, opts) {
|
|
90
|
+
const deadlineMs = (opts && opts.deadlineMs != null) ? opts.deadlineMs : 5000;
|
|
91
|
+
const retryIntervalMs = (opts && opts.retryIntervalMs != null) ? opts.retryIntervalMs : 100;
|
|
92
|
+
const deadline = Date.now() + deadlineMs;
|
|
93
|
+
|
|
94
|
+
for (;;) {
|
|
95
|
+
let fd;
|
|
96
|
+
try {
|
|
97
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
98
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
99
|
+
const startedAt = new Date().toISOString();
|
|
100
|
+
const ownerToken = { pid: process.pid, startedAt, nonce };
|
|
101
|
+
fs.writeSync(fd, JSON.stringify(ownerToken));
|
|
102
|
+
fs.fsyncSync(fd);
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
return { nonce, lockPath, pid: process.pid, startedAt };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (fd !== undefined) {
|
|
107
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
108
|
+
if (err.code !== 'EEXIST') {
|
|
109
|
+
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (err.code !== 'EEXIST') {
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ABA-guarded staleness check + reclaim (US-10 / AC-08 / AC-26)
|
|
117
|
+
try {
|
|
118
|
+
let content1, stat1;
|
|
119
|
+
try {
|
|
120
|
+
content1 = fs.readFileSync(lockPath, 'utf8');
|
|
121
|
+
stat1 = fs.statSync(lockPath);
|
|
122
|
+
} catch (readErr) {
|
|
123
|
+
if (readErr.code === 'ENOENT') {
|
|
124
|
+
continue; // lock vanished between EEXIST and our read — retry O_EXCL immediately
|
|
125
|
+
}
|
|
126
|
+
throw readErr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const state = _isLockStale(content1, stat1);
|
|
130
|
+
|
|
131
|
+
if (state === 'reclaimable') {
|
|
132
|
+
// ABA guard: re-read and re-stat before unlinking to ensure no other
|
|
133
|
+
// writer replaced or refreshed the lock between classify and reclaim.
|
|
134
|
+
let content2, stat2;
|
|
135
|
+
try {
|
|
136
|
+
content2 = fs.readFileSync(lockPath, 'utf8');
|
|
137
|
+
stat2 = fs.statSync(lockPath);
|
|
138
|
+
} catch (abaErr) {
|
|
139
|
+
if (abaErr.code === 'ENOENT') {
|
|
140
|
+
continue; // lock vanished during ABA check — retry O_EXCL immediately
|
|
141
|
+
}
|
|
142
|
+
throw abaErr;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (content2 === content1 && stat2.mtimeMs === stat1.mtimeMs) {
|
|
146
|
+
// Content and mtime are identical — safe to unlink and reclaim
|
|
147
|
+
try {
|
|
148
|
+
fs.unlinkSync(lockPath);
|
|
149
|
+
} catch (unlinkErr) {
|
|
150
|
+
if (unlinkErr.code !== 'ENOENT') throw unlinkErr;
|
|
151
|
+
// ENOENT: another process reclaimed it first — still retry O_EXCL
|
|
152
|
+
}
|
|
153
|
+
continue; // retry O_EXCL create immediately, no sleep
|
|
154
|
+
}
|
|
155
|
+
// ABA check failed: content or mtime changed — another writer refreshed
|
|
156
|
+
// the lock between classify and reclaim; treat as 'wait' this iteration
|
|
157
|
+
}
|
|
158
|
+
// 'live', 'wait', or ABA-failed 'reclaimable': fall through to deadline + sleep
|
|
159
|
+
} catch (checkErr) {
|
|
160
|
+
if (checkErr.code === 'ENOENT') {
|
|
161
|
+
continue; // lock vanished during stale check — retry O_EXCL immediately
|
|
162
|
+
}
|
|
163
|
+
// Any other unexpected error during the staleness check: absorb and fall
|
|
164
|
+
// through to the normal wait/sleep path so acquisition is never aborted
|
|
165
|
+
// by a transient read error.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Existing behavior for live / wait / ABA-failed locks
|
|
169
|
+
if (Date.now() >= deadline) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
'Failed to acquire lock ' + lockPath + ': deadline of ' + deadlineMs + 'ms exceeded'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const remaining = deadline - Date.now();
|
|
175
|
+
if (remaining > 0) {
|
|
176
|
+
_sleepSync(Math.min(retryIntervalMs, remaining));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _releaseLock(lockPath, handle) {
|
|
183
|
+
let content;
|
|
184
|
+
try {
|
|
185
|
+
content = fs.readFileSync(lockPath, 'utf8');
|
|
186
|
+
} catch (_) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let ownerToken;
|
|
190
|
+
try {
|
|
191
|
+
ownerToken = JSON.parse(content);
|
|
192
|
+
} catch (_) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!ownerToken || ownerToken.nonce !== handle.nonce) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
fs.unlinkSync(lockPath);
|
|
200
|
+
} catch (_) {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const LOCK_STALE_THRESHOLD_MS = 30000;
|
|
204
|
+
const LOCK_ORPHAN_THRESHOLD_MS = 30000;
|
|
205
|
+
|
|
206
|
+
function _isLockStale(content, stat) {
|
|
207
|
+
let token = null;
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(content);
|
|
210
|
+
if (parsed !== null && typeof parsed === 'object' && typeof parsed.pid === 'number') {
|
|
211
|
+
token = parsed;
|
|
212
|
+
}
|
|
213
|
+
} catch (_) {}
|
|
214
|
+
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
|
|
217
|
+
if (token !== null) {
|
|
218
|
+
let ownerAlive = false;
|
|
219
|
+
try {
|
|
220
|
+
process.kill(token.pid, 0);
|
|
221
|
+
ownerAlive = true;
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err.code === 'EPERM') {
|
|
224
|
+
ownerAlive = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (ownerAlive) {
|
|
229
|
+
return 'live';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let age;
|
|
233
|
+
const startedAtMs = token.startedAt ? Date.parse(token.startedAt) : NaN;
|
|
234
|
+
if (!isNaN(startedAtMs)) {
|
|
235
|
+
age = now - startedAtMs;
|
|
236
|
+
} else {
|
|
237
|
+
age = now - stat.mtimeMs;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return age > LOCK_STALE_THRESHOLD_MS ? 'reclaimable' : 'wait';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const mtimeAge = now - stat.mtimeMs;
|
|
244
|
+
return mtimeAge > LOCK_ORPHAN_THRESHOLD_MS ? 'reclaimable' : 'wait';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Public API (stubs — implemented by INFRA-TASK-BE-02 and US-0x tasks) ────
|
|
248
|
+
|
|
249
|
+
function computeOperationId(prefix, agent, attempt) {
|
|
250
|
+
const input = JSON.stringify([prefix, agent, attempt]);
|
|
251
|
+
return crypto.createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 32);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function open(dir, prefix, agent, phase, model, attempt) {
|
|
255
|
+
if (attempt == null) attempt = 1;
|
|
256
|
+
|
|
257
|
+
const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
|
|
258
|
+
const lockPath = ledgerPath + '.lock';
|
|
259
|
+
|
|
260
|
+
const lockHandle = _acquireLock(lockPath);
|
|
261
|
+
try {
|
|
262
|
+
const { entries } = _readLedger(ledgerPath);
|
|
263
|
+
const operation_id = computeOperationId(prefix, agent, attempt);
|
|
264
|
+
const started_at = new Date().toISOString();
|
|
265
|
+
|
|
266
|
+
const existingIndex = entries.findIndex(function (e) {
|
|
267
|
+
return e.operation_id === operation_id;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (existingIndex !== -1) {
|
|
271
|
+
// Resume: idempotently preserve original started_at and any positive
|
|
272
|
+
// phase_delta_tokens — only reset status to running.
|
|
273
|
+
entries[existingIndex] = Object.assign({}, entries[existingIndex], { status: 'running' });
|
|
274
|
+
} else {
|
|
275
|
+
entries.push({
|
|
276
|
+
operation_id: operation_id,
|
|
277
|
+
agent: agent,
|
|
278
|
+
phase: phase,
|
|
279
|
+
model: model,
|
|
280
|
+
status: 'running',
|
|
281
|
+
started_at: started_at,
|
|
282
|
+
completed_at: null,
|
|
283
|
+
phase_delta_tokens: null,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_writeLedger(ledgerPath, entries);
|
|
288
|
+
|
|
289
|
+
const resultEntry = existingIndex !== -1
|
|
290
|
+
? entries[existingIndex]
|
|
291
|
+
: entries[entries.length - 1];
|
|
292
|
+
|
|
293
|
+
return { status: 'ok', operation_id: operation_id, entry: resultEntry };
|
|
294
|
+
} finally {
|
|
295
|
+
_releaseLock(lockPath, lockHandle);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function close(dir, prefix, agent, tokens, attempt) {
|
|
300
|
+
if (attempt == null) attempt = 1;
|
|
301
|
+
|
|
302
|
+
const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
|
|
303
|
+
const lockPath = ledgerPath + '.lock';
|
|
304
|
+
|
|
305
|
+
const lockHandle = _acquireLock(lockPath);
|
|
306
|
+
try {
|
|
307
|
+
const { entries } = _readLedger(ledgerPath);
|
|
308
|
+
const operation_id = computeOperationId(prefix, agent, attempt);
|
|
309
|
+
|
|
310
|
+
// Primary lookup: by operation_id (deterministic, unambiguous)
|
|
311
|
+
let idx = entries.findIndex(function (e) {
|
|
312
|
+
return e.operation_id === operation_id;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Fallback: by agent name — only when exactly one entry matches (AC-12)
|
|
316
|
+
if (idx === -1) {
|
|
317
|
+
const agentIndexes = [];
|
|
318
|
+
for (var i = 0; i < entries.length; i++) {
|
|
319
|
+
if (entries[i].agent === agent) agentIndexes.push(i);
|
|
320
|
+
}
|
|
321
|
+
if (agentIndexes.length === 0) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
'close: no entry found for operation_id ' + operation_id +
|
|
324
|
+
' and agent "' + agent + '" in ' + ledgerPath
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (agentIndexes.length > 1) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
'close: ambiguous agent fallback — ' + agentIndexes.length +
|
|
330
|
+
' entries match agent "' + agent + '" in ' + ledgerPath
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
idx = agentIndexes[0];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const existing = entries[idx];
|
|
337
|
+
const completed_at = new Date().toISOString();
|
|
338
|
+
|
|
339
|
+
// Token preservation (AC-10):
|
|
340
|
+
// - positive integer supplied → record it
|
|
341
|
+
// - null/undefined supplied → preserve whatever positive value is already stored;
|
|
342
|
+
// null must never clobber an existing positive phase_delta_tokens
|
|
343
|
+
var phase_delta_tokens = existing.phase_delta_tokens;
|
|
344
|
+
if (tokens !== null && tokens !== undefined) {
|
|
345
|
+
phase_delta_tokens = tokens;
|
|
346
|
+
}
|
|
347
|
+
// When tokens is null/undefined, phase_delta_tokens keeps its existing value
|
|
348
|
+
// (positive preserved; null/non-positive also left unchanged for this task)
|
|
349
|
+
|
|
350
|
+
// Merge update: Object.assign preserves any unknown/legacy fields verbatim (AC-12)
|
|
351
|
+
entries[idx] = Object.assign({}, existing, {
|
|
352
|
+
status: 'done',
|
|
353
|
+
completed_at: completed_at,
|
|
354
|
+
phase_delta_tokens: phase_delta_tokens,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
_writeLedger(ledgerPath, entries);
|
|
358
|
+
|
|
359
|
+
// Return same shape as open() — operation_id comes from the entry when present,
|
|
360
|
+
// otherwise from the computed value (legacy entry without one, per AC-12)
|
|
361
|
+
const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
|
|
362
|
+
return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
|
|
363
|
+
} finally {
|
|
364
|
+
_releaseLock(lockPath, lockHandle);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function _sanitizeError(err) {
|
|
369
|
+
return String(err).replace(/[\r\n\t]+/g, ' ').slice(0, 500);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function fail(dir, prefix, agent, error, attempt) {
|
|
373
|
+
if (attempt == null) attempt = 1;
|
|
374
|
+
|
|
375
|
+
const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
|
|
376
|
+
const lockPath = ledgerPath + '.lock';
|
|
377
|
+
|
|
378
|
+
const lockHandle = _acquireLock(lockPath);
|
|
379
|
+
try {
|
|
380
|
+
const { entries } = _readLedger(ledgerPath);
|
|
381
|
+
const operation_id = computeOperationId(prefix, agent, attempt);
|
|
382
|
+
|
|
383
|
+
// Primary lookup: by operation_id (deterministic, unambiguous)
|
|
384
|
+
let idx = entries.findIndex(function (e) {
|
|
385
|
+
return e.operation_id === operation_id;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Fallback: by agent name — only when exactly one entry matches
|
|
389
|
+
if (idx === -1) {
|
|
390
|
+
const agentIndexes = [];
|
|
391
|
+
for (var i = 0; i < entries.length; i++) {
|
|
392
|
+
if (entries[i].agent === agent) agentIndexes.push(i);
|
|
393
|
+
}
|
|
394
|
+
if (agentIndexes.length === 0) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
'fail: no entry found for operation_id ' + operation_id +
|
|
397
|
+
' and agent "' + agent + '" in ' + ledgerPath +
|
|
398
|
+
' — operation was never opened'
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (agentIndexes.length > 1) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
'fail: ambiguous agent fallback — ' + agentIndexes.length +
|
|
404
|
+
' entries match agent "' + agent + '" in ' + ledgerPath
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
idx = agentIndexes[0];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const existing = entries[idx];
|
|
411
|
+
const completed_at = new Date().toISOString();
|
|
412
|
+
|
|
413
|
+
const update = {
|
|
414
|
+
status: 'failed',
|
|
415
|
+
completed_at: completed_at,
|
|
416
|
+
};
|
|
417
|
+
if (error) {
|
|
418
|
+
update.error = _sanitizeError(error);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
entries[idx] = Object.assign({}, existing, update);
|
|
422
|
+
|
|
423
|
+
_writeLedger(ledgerPath, entries);
|
|
424
|
+
|
|
425
|
+
const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
|
|
426
|
+
return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
|
|
427
|
+
} finally {
|
|
428
|
+
_releaseLock(lockPath, lockHandle);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function skip(dir, prefix, agent, phase, model, attempt) {
|
|
433
|
+
if (attempt == null) attempt = 1;
|
|
434
|
+
|
|
435
|
+
const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
|
|
436
|
+
const lockPath = ledgerPath + '.lock';
|
|
437
|
+
|
|
438
|
+
const lockHandle = _acquireLock(lockPath);
|
|
439
|
+
try {
|
|
440
|
+
const { entries } = _readLedger(ledgerPath);
|
|
441
|
+
const operation_id = computeOperationId(prefix, agent, attempt);
|
|
442
|
+
|
|
443
|
+
// Primary lookup: by operation_id (deterministic, unambiguous)
|
|
444
|
+
let idx = entries.findIndex(function (e) {
|
|
445
|
+
return e.operation_id === operation_id;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Fallback: by agent name — only when no exact operation_id match
|
|
449
|
+
if (idx === -1) {
|
|
450
|
+
const agentIndexes = [];
|
|
451
|
+
for (var i = 0; i < entries.length; i++) {
|
|
452
|
+
if (entries[i].agent === agent) agentIndexes.push(i);
|
|
453
|
+
}
|
|
454
|
+
if (agentIndexes.length > 1) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
'skip: ambiguous agent fallback — ' + agentIndexes.length +
|
|
457
|
+
' entries match agent "' + agent + '" in ' + ledgerPath
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (agentIndexes.length === 1) {
|
|
461
|
+
idx = agentIndexes[0];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (idx === -1) {
|
|
466
|
+
// Case 1: NO existing match — atomically create a new terminal skipped entry.
|
|
467
|
+
// started_at and completed_at are set to the same timestamp (AC-23).
|
|
468
|
+
const ts = new Date().toISOString();
|
|
469
|
+
entries.push({
|
|
470
|
+
operation_id: operation_id,
|
|
471
|
+
agent: agent,
|
|
472
|
+
phase: phase,
|
|
473
|
+
model: model,
|
|
474
|
+
status: 'skipped',
|
|
475
|
+
started_at: ts,
|
|
476
|
+
completed_at: ts,
|
|
477
|
+
phase_delta_tokens: null,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
_writeLedger(ledgerPath, entries);
|
|
481
|
+
|
|
482
|
+
return { status: 'ok', operation_id: operation_id, entry: entries[entries.length - 1] };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Case 2: EXACTLY ONE match — update in place, preserving started_at and all other fields.
|
|
486
|
+
const existing = entries[idx];
|
|
487
|
+
const completed_at = new Date().toISOString();
|
|
488
|
+
|
|
489
|
+
entries[idx] = Object.assign({}, existing, {
|
|
490
|
+
status: 'skipped',
|
|
491
|
+
completed_at: completed_at,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
_writeLedger(ledgerPath, entries);
|
|
495
|
+
|
|
496
|
+
const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
|
|
497
|
+
return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
|
|
498
|
+
} finally {
|
|
499
|
+
_releaseLock(lockPath, lockHandle);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = {
|
|
504
|
+
open,
|
|
505
|
+
close,
|
|
506
|
+
fail,
|
|
507
|
+
skip,
|
|
508
|
+
computeOperationId,
|
|
509
|
+
_readLedger,
|
|
510
|
+
_writeLedger,
|
|
511
|
+
_backupCorruptFile,
|
|
512
|
+
_acquireLock,
|
|
513
|
+
_releaseLock,
|
|
514
|
+
_isLockStale,
|
|
515
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtlabs/ai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
|
|
5
5
|
"files": [
|
|
6
6
|
"docs/",
|
|
@@ -8,12 +8,8 @@
|
|
|
8
8
|
"CHANGELOG.md",
|
|
9
9
|
"README.md",
|
|
10
10
|
"CLAUDE.global.md",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
".claude/workflows",
|
|
14
|
-
".claude/skills",
|
|
15
|
-
".claude/scripts/*.js",
|
|
16
|
-
".claude/scripts/.gitkeep",
|
|
11
|
+
"src/claude",
|
|
12
|
+
"lib",
|
|
17
13
|
"bin"
|
|
18
14
|
],
|
|
19
15
|
"bin": {
|
|
@@ -21,7 +17,13 @@
|
|
|
21
17
|
},
|
|
22
18
|
"scripts": {
|
|
23
19
|
"prepublishOnly": "npm pack --dry-run",
|
|
20
|
+
"prepack": "node bin/cli.js validate-purity",
|
|
21
|
+
"toolkit:validate-purity": "node bin/cli.js validate-purity",
|
|
22
|
+
"toolkit:dev-install-global": "node bin/cli.js --global --dry-run && node bin/cli.js --global && node bin/cli.js doctor resolution && node bin/cli.js verify-install",
|
|
24
23
|
"test": "jest --bail",
|
|
24
|
+
"test:cli": "jest --bail --testPathPattern=tests/cli/",
|
|
25
|
+
"test:frontmatter": "jest --bail --testPathPattern=tests/frontmatter/",
|
|
26
|
+
"test:regression": "jest --bail --testPathPattern=tests/regression/",
|
|
25
27
|
"test:coverage": "jest --coverage"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|