@dtlabs/ai-toolkit 0.11.0 → 0.13.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/bin/cli.js +757 -68
  3. package/lib/agent-registry.js +1018 -0
  4. package/lib/execution-ledger.js +609 -0
  5. package/package.json +1 -1
  6. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  7. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +37 -61
  8. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  9. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  10. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  11. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  12. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  13. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  14. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  15. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  16. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  17. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  18. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  19. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  20. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  21. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +1 -1
  22. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  23. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  24. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  25. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  26. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  27. package/src/claude/commands/next-task.md +4 -4
  28. package/src/claude/scripts/wb-render.js +101 -2
  29. package/src/claude/scripts/wb-validate.js +32 -2
  30. package/src/claude/skills/assess-codebase/SKILL.md +1 -1
  31. package/src/claude/skills/define-feature/SKILL.md +2 -2
  32. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  33. package/src/claude/skills/implement-feature/SKILL.md +190 -10
  34. package/src/claude/skills/init-agents/SKILL.md +2 -2
  35. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  36. package/src/claude/workflows/pm-phase1.js +94 -73
  37. package/src/claude/workflows/pm-phase2.js +118 -108
  38. package/src/claude/workflows/pm-phase3.js +219 -75
@@ -0,0 +1,609 @@
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
+ // ── Metadata whitelist and reserved-field protection (INFRA-TASK-BE-03) ─────
248
+
249
+ const METADATA_WHITELIST = new Set([
250
+ 'agentId',
251
+ 'nativeAgentName',
252
+ 'platform',
253
+ 'toolkitVersion',
254
+ 'resolutionScope',
255
+ 'definitionHash',
256
+ ]);
257
+
258
+ const RESERVED_FIELDS = new Set([
259
+ 'operation_id',
260
+ 'agent',
261
+ 'phase',
262
+ 'model',
263
+ 'status',
264
+ 'started_at',
265
+ 'completed_at',
266
+ 'phase_delta_tokens',
267
+ 'error',
268
+ ]);
269
+
270
+ function _validateMetadata(metadata) {
271
+ for (const key of Object.keys(metadata)) {
272
+ if (RESERVED_FIELDS.has(key)) {
273
+ const err = new Error(
274
+ 'open: metadata key "' + key + '" is a reserved field and cannot be set via metadata'
275
+ );
276
+ err.code = 'METADATA_RESERVED_FIELD';
277
+ err.key = key;
278
+ throw err;
279
+ }
280
+ if (!METADATA_WHITELIST.has(key)) {
281
+ const err = new Error(
282
+ 'open: metadata key "' + key + '" is not in the whitelist; ' +
283
+ 'allowed keys: ' + Array.from(METADATA_WHITELIST).join(', ')
284
+ );
285
+ err.code = 'METADATA_UNKNOWN_KEY';
286
+ err.key = key;
287
+ throw err;
288
+ }
289
+ }
290
+ }
291
+
292
+ function _extractStoredMetadata(entry) {
293
+ const result = {};
294
+ for (const key of METADATA_WHITELIST) {
295
+ if (Object.prototype.hasOwnProperty.call(entry, key)) {
296
+ result[key] = entry[key];
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+
302
+ function _metadataEqual(storedEntry, newMetadata) {
303
+ const stored = _extractStoredMetadata(storedEntry);
304
+ const storedKeys = Object.keys(stored).sort();
305
+ const newKeys = Object.keys(newMetadata).sort();
306
+
307
+ if (storedKeys.length !== newKeys.length) return false;
308
+ for (var i = 0; i < newKeys.length; i++) {
309
+ if (stored[newKeys[i]] !== newMetadata[newKeys[i]]) return false;
310
+ }
311
+ return true;
312
+ }
313
+
314
+ // ── Public API (stubs — implemented by INFRA-TASK-BE-02 and US-0x tasks) ────
315
+
316
+ function computeOperationId(prefix, agent, attempt) {
317
+ const input = JSON.stringify([prefix, agent, attempt]);
318
+ return crypto.createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 32);
319
+ }
320
+
321
+ function open(dir, prefix, agent, phase, model, attempt, metadata) {
322
+ if (attempt == null) attempt = 1;
323
+
324
+ // Validate metadata keys BEFORE any lock or I/O (fail-closed).
325
+ if (metadata != null) {
326
+ _validateMetadata(metadata);
327
+ }
328
+
329
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
330
+ const lockPath = ledgerPath + '.lock';
331
+
332
+ const lockHandle = _acquireLock(lockPath);
333
+ try {
334
+ const { entries } = _readLedger(ledgerPath);
335
+ const operation_id = computeOperationId(prefix, agent, attempt);
336
+ const started_at = new Date().toISOString();
337
+
338
+ const existingIndex = entries.findIndex(function (e) {
339
+ return e.operation_id === operation_id;
340
+ });
341
+
342
+ if (existingIndex !== -1) {
343
+ // Idempotent re-open: check metadata conflict BEFORE writing anything.
344
+ if (metadata != null) {
345
+ if (!_metadataEqual(entries[existingIndex], metadata)) {
346
+ const err = new Error(
347
+ 'open: metadata conflict for operation_id ' + operation_id +
348
+ ' — supplied metadata differs from stored metadata; re-open rejected (fail-closed)'
349
+ );
350
+ err.code = 'METADATA_CONFLICT';
351
+ err.operation_id = operation_id;
352
+ throw err;
353
+ }
354
+ }
355
+ // Resume: idempotently preserve original started_at and any positive
356
+ // phase_delta_tokens — only reset status to running.
357
+ entries[existingIndex] = Object.assign({}, entries[existingIndex], { status: 'running' });
358
+ } else {
359
+ // First open: store standard fields, then spread validated metadata fields.
360
+ const newEntry = {
361
+ operation_id: operation_id,
362
+ agent: agent,
363
+ phase: phase,
364
+ model: model,
365
+ status: 'running',
366
+ started_at: started_at,
367
+ completed_at: null,
368
+ phase_delta_tokens: null,
369
+ };
370
+ if (metadata != null) {
371
+ Object.assign(newEntry, metadata);
372
+ }
373
+ entries.push(newEntry);
374
+ }
375
+
376
+ _writeLedger(ledgerPath, entries);
377
+
378
+ const resultEntry = existingIndex !== -1
379
+ ? entries[existingIndex]
380
+ : entries[entries.length - 1];
381
+
382
+ return { status: 'ok', operation_id: operation_id, entry: resultEntry };
383
+ } finally {
384
+ _releaseLock(lockPath, lockHandle);
385
+ }
386
+ }
387
+
388
+ function close(dir, prefix, agent, tokens, attempt) {
389
+ if (attempt == null) attempt = 1;
390
+
391
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
392
+ const lockPath = ledgerPath + '.lock';
393
+
394
+ const lockHandle = _acquireLock(lockPath);
395
+ try {
396
+ const { entries } = _readLedger(ledgerPath);
397
+ const operation_id = computeOperationId(prefix, agent, attempt);
398
+
399
+ // Primary lookup: by operation_id (deterministic, unambiguous)
400
+ let idx = entries.findIndex(function (e) {
401
+ return e.operation_id === operation_id;
402
+ });
403
+
404
+ // Fallback: by agent name — only when exactly one entry matches (AC-12)
405
+ if (idx === -1) {
406
+ const agentIndexes = [];
407
+ for (var i = 0; i < entries.length; i++) {
408
+ if (entries[i].agent === agent) agentIndexes.push(i);
409
+ }
410
+ if (agentIndexes.length === 0) {
411
+ throw new Error(
412
+ 'close: no entry found for operation_id ' + operation_id +
413
+ ' and agent "' + agent + '" in ' + ledgerPath
414
+ );
415
+ }
416
+ if (agentIndexes.length > 1) {
417
+ throw new Error(
418
+ 'close: ambiguous agent fallback — ' + agentIndexes.length +
419
+ ' entries match agent "' + agent + '" in ' + ledgerPath
420
+ );
421
+ }
422
+ idx = agentIndexes[0];
423
+ }
424
+
425
+ const existing = entries[idx];
426
+ const completed_at = new Date().toISOString();
427
+
428
+ // Token preservation (AC-10):
429
+ // - positive integer supplied → record it
430
+ // - null/undefined supplied → preserve whatever positive value is already stored;
431
+ // null must never clobber an existing positive phase_delta_tokens
432
+ var phase_delta_tokens = existing.phase_delta_tokens;
433
+ if (tokens !== null && tokens !== undefined) {
434
+ phase_delta_tokens = tokens;
435
+ }
436
+ // When tokens is null/undefined, phase_delta_tokens keeps its existing value
437
+ // (positive preserved; null/non-positive also left unchanged for this task)
438
+
439
+ // Merge update: Object.assign preserves any unknown/legacy fields verbatim (AC-12)
440
+ entries[idx] = Object.assign({}, existing, {
441
+ status: 'done',
442
+ completed_at: completed_at,
443
+ phase_delta_tokens: phase_delta_tokens,
444
+ });
445
+
446
+ _writeLedger(ledgerPath, entries);
447
+
448
+ // Return same shape as open() — operation_id comes from the entry when present,
449
+ // otherwise from the computed value (legacy entry without one, per AC-12)
450
+ const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
451
+ return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
452
+ } finally {
453
+ _releaseLock(lockPath, lockHandle);
454
+ }
455
+ }
456
+
457
+ function _sanitizeError(err) {
458
+ return String(err).replace(/[\r\n\t]+/g, ' ').slice(0, 500);
459
+ }
460
+
461
+ function fail(dir, prefix, agent, error, attempt) {
462
+ if (attempt == null) attempt = 1;
463
+
464
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
465
+ const lockPath = ledgerPath + '.lock';
466
+
467
+ const lockHandle = _acquireLock(lockPath);
468
+ try {
469
+ const { entries } = _readLedger(ledgerPath);
470
+ const operation_id = computeOperationId(prefix, agent, attempt);
471
+
472
+ // Primary lookup: by operation_id (deterministic, unambiguous)
473
+ let idx = entries.findIndex(function (e) {
474
+ return e.operation_id === operation_id;
475
+ });
476
+
477
+ // Fallback: by agent name — only when exactly one entry matches
478
+ if (idx === -1) {
479
+ const agentIndexes = [];
480
+ for (var i = 0; i < entries.length; i++) {
481
+ if (entries[i].agent === agent) agentIndexes.push(i);
482
+ }
483
+ if (agentIndexes.length === 0) {
484
+ throw new Error(
485
+ 'fail: no entry found for operation_id ' + operation_id +
486
+ ' and agent "' + agent + '" in ' + ledgerPath +
487
+ ' — operation was never opened'
488
+ );
489
+ }
490
+ if (agentIndexes.length > 1) {
491
+ throw new Error(
492
+ 'fail: ambiguous agent fallback — ' + agentIndexes.length +
493
+ ' entries match agent "' + agent + '" in ' + ledgerPath
494
+ );
495
+ }
496
+ idx = agentIndexes[0];
497
+ }
498
+
499
+ const existing = entries[idx];
500
+ const completed_at = new Date().toISOString();
501
+
502
+ const update = {
503
+ status: 'failed',
504
+ completed_at: completed_at,
505
+ };
506
+ if (error) {
507
+ update.error = _sanitizeError(error);
508
+ }
509
+
510
+ entries[idx] = Object.assign({}, existing, update);
511
+
512
+ _writeLedger(ledgerPath, entries);
513
+
514
+ const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
515
+ return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
516
+ } finally {
517
+ _releaseLock(lockPath, lockHandle);
518
+ }
519
+ }
520
+
521
+ function skip(dir, prefix, agent, phase, model, attempt) {
522
+ if (attempt == null) attempt = 1;
523
+
524
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
525
+ const lockPath = ledgerPath + '.lock';
526
+
527
+ const lockHandle = _acquireLock(lockPath);
528
+ try {
529
+ const { entries } = _readLedger(ledgerPath);
530
+ const operation_id = computeOperationId(prefix, agent, attempt);
531
+
532
+ // Primary lookup: by operation_id (deterministic, unambiguous)
533
+ let idx = entries.findIndex(function (e) {
534
+ return e.operation_id === operation_id;
535
+ });
536
+
537
+ // Fallback: by agent name — only when no exact operation_id match
538
+ if (idx === -1) {
539
+ const agentIndexes = [];
540
+ for (var i = 0; i < entries.length; i++) {
541
+ if (entries[i].agent === agent) agentIndexes.push(i);
542
+ }
543
+ if (agentIndexes.length > 1) {
544
+ throw new Error(
545
+ 'skip: ambiguous agent fallback — ' + agentIndexes.length +
546
+ ' entries match agent "' + agent + '" in ' + ledgerPath
547
+ );
548
+ }
549
+ if (agentIndexes.length === 1) {
550
+ idx = agentIndexes[0];
551
+ }
552
+ }
553
+
554
+ if (idx === -1) {
555
+ // Case 1: NO existing match — atomically create a new terminal skipped entry.
556
+ // started_at and completed_at are set to the same timestamp (AC-23).
557
+ const ts = new Date().toISOString();
558
+ entries.push({
559
+ operation_id: operation_id,
560
+ agent: agent,
561
+ phase: phase,
562
+ model: model,
563
+ status: 'skipped',
564
+ started_at: ts,
565
+ completed_at: ts,
566
+ phase_delta_tokens: null,
567
+ });
568
+
569
+ _writeLedger(ledgerPath, entries);
570
+
571
+ return { status: 'ok', operation_id: operation_id, entry: entries[entries.length - 1] };
572
+ }
573
+
574
+ // Case 2: EXACTLY ONE match — update in place, preserving started_at and all other fields.
575
+ const existing = entries[idx];
576
+ const completed_at = new Date().toISOString();
577
+
578
+ entries[idx] = Object.assign({}, existing, {
579
+ status: 'skipped',
580
+ completed_at: completed_at,
581
+ });
582
+
583
+ _writeLedger(ledgerPath, entries);
584
+
585
+ const returnedOpId = existing.operation_id != null ? existing.operation_id : operation_id;
586
+ return { status: 'ok', operation_id: returnedOpId, entry: entries[idx] };
587
+ } finally {
588
+ _releaseLock(lockPath, lockHandle);
589
+ }
590
+ }
591
+
592
+ module.exports = {
593
+ open,
594
+ close,
595
+ fail,
596
+ skip,
597
+ computeOperationId,
598
+ _readLedger,
599
+ _writeLedger,
600
+ _backupCorruptFile,
601
+ _acquireLock,
602
+ _releaseLock,
603
+ _isLockStale,
604
+ _validateMetadata,
605
+ _extractStoredMetadata,
606
+ _metadataEqual,
607
+ METADATA_WHITELIST,
608
+ RESERVED_FIELDS,
609
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtlabs/ai-toolkit",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "AI Toolkit — reusable agents and procedures for developers to build AI-powered applications.",
5
5
  "files": [
6
6
  "docs/",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: concurrency-safety-assessment
2
+ name: gaia-concurrency-safety-assessment
3
3
  description: "Concurrency Safety Assessment — identifies race conditions, shared mutable state corruption, and non-deterministic behaviour in concurrent or event-driven systems. Language-agnostic; produces structured findings with severity, evidence, and mitigation patterns. Output: {ASSESS_PREFIX}-Concurrency-Assessment.md"
4
4
  tools: Read, Grep, Glob, Bash, Write
5
5
  model: haiku