@dtlabs/ai-toolkit 0.12.0 → 0.14.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 (50) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/bin/cli.js +851 -9
  3. package/docs/task-executor-bootstrap.md +195 -0
  4. package/docs/task-executor-cli.md +319 -0
  5. package/docs/workflow-control-plane.md +95 -0
  6. package/lib/agent-registry.js +1018 -0
  7. package/lib/execution-ledger.js +283 -7
  8. package/lib/task-executor/claude-process.js +563 -0
  9. package/lib/task-executor/git.js +1362 -0
  10. package/lib/task-executor/index.js +3729 -0
  11. package/lib/task-executor/ownership.js +590 -0
  12. package/lib/task-executor/plan.js +725 -0
  13. package/lib/task-executor/store.js +1164 -0
  14. package/lib/workflow-artifacts.js +102 -0
  15. package/lib/workflow-control.js +219 -0
  16. package/lib/workflow-runner.js +39 -0
  17. package/package.json +1 -1
  18. package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
  19. package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +1 -1
  20. package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
  21. package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
  22. package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
  23. package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
  24. package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
  25. package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
  26. package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
  27. package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
  28. package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
  29. package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
  30. package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
  31. package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
  32. package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
  33. package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +10 -10
  34. package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
  35. package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
  36. package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
  37. package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
  38. package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
  39. package/src/claude/commands/next-task.md +4 -4
  40. package/src/claude/skills/assess-codebase/SKILL.md +9 -103
  41. package/src/claude/skills/define-feature/SKILL.md +2 -2
  42. package/src/claude/skills/hi-gaia/SKILL.md +1 -1
  43. package/src/claude/skills/implement-feature/SKILL.md +131 -38
  44. package/src/claude/skills/init-agents/SKILL.md +2 -2
  45. package/src/claude/skills/install-toolkit/SKILL.md +2 -2
  46. package/src/claude/workflows/am-phase1.js +43 -294
  47. package/src/claude/workflows/am-phase2.js +10 -164
  48. package/src/claude/workflows/pm-phase1.js +45 -287
  49. package/src/claude/workflows/pm-phase2.js +51 -511
  50. package/src/claude/workflows/pm-phase3.js +5 -767
@@ -33,7 +33,8 @@ function _readLedger(ledgerPath) {
33
33
  }
34
34
  throw parseErr;
35
35
  }
36
- return { entries: Array.isArray(parsed) ? parsed : [] };
36
+ if (!Array.isArray(parsed)) throw new Error('Invalid ledger: expected JSON array');
37
+ return { entries: parsed };
37
38
  }
38
39
 
39
40
  function _writeLedger(ledgerPath, entries) {
@@ -244,6 +245,73 @@ function _isLockStale(content, stat) {
244
245
  return mtimeAge > LOCK_ORPHAN_THRESHOLD_MS ? 'reclaimable' : 'wait';
245
246
  }
246
247
 
248
+ // ── Metadata whitelist and reserved-field protection (INFRA-TASK-BE-03) ─────
249
+
250
+ const METADATA_WHITELIST = new Set([
251
+ 'agentId',
252
+ 'nativeAgentName',
253
+ 'platform',
254
+ 'toolkitVersion',
255
+ 'resolutionScope',
256
+ 'definitionHash',
257
+ ]);
258
+
259
+ const RESERVED_FIELDS = new Set([
260
+ 'operation_id',
261
+ 'agent',
262
+ 'phase',
263
+ 'model',
264
+ 'status',
265
+ 'started_at',
266
+ 'completed_at',
267
+ 'phase_delta_tokens',
268
+ 'error',
269
+ ]);
270
+
271
+ function _validateMetadata(metadata) {
272
+ for (const key of Object.keys(metadata)) {
273
+ if (RESERVED_FIELDS.has(key)) {
274
+ const err = new Error(
275
+ 'open: metadata key "' + key + '" is a reserved field and cannot be set via metadata'
276
+ );
277
+ err.code = 'METADATA_RESERVED_FIELD';
278
+ err.key = key;
279
+ throw err;
280
+ }
281
+ if (!METADATA_WHITELIST.has(key)) {
282
+ const err = new Error(
283
+ 'open: metadata key "' + key + '" is not in the whitelist; ' +
284
+ 'allowed keys: ' + Array.from(METADATA_WHITELIST).join(', ')
285
+ );
286
+ err.code = 'METADATA_UNKNOWN_KEY';
287
+ err.key = key;
288
+ throw err;
289
+ }
290
+ }
291
+ }
292
+
293
+ function _extractStoredMetadata(entry) {
294
+ const result = {};
295
+ for (const key of METADATA_WHITELIST) {
296
+ if (Object.prototype.hasOwnProperty.call(entry, key)) {
297
+ result[key] = entry[key];
298
+ }
299
+ }
300
+ return result;
301
+ }
302
+
303
+ function _metadataEqual(storedEntry, newMetadata) {
304
+ const stored = _extractStoredMetadata(storedEntry);
305
+ const storedKeys = Object.keys(stored).sort();
306
+ const newKeys = Object.keys(newMetadata).sort();
307
+
308
+ if (storedKeys.length !== newKeys.length) return false;
309
+ for (var i = 0; i < newKeys.length; i++) {
310
+ if (stored[newKeys[i]] !== newMetadata[newKeys[i]]) return false;
311
+ }
312
+ return true;
313
+ }
314
+
247
315
  // ── Public API (stubs — implemented by INFRA-TASK-BE-02 and US-0x tasks) ────
248
316
 
249
317
  function computeOperationId(prefix, agent, attempt) {
@@ -251,9 +319,14 @@ function computeOperationId(prefix, agent, attempt) {
251
319
  return crypto.createHash('sha256').update(input, 'utf8').digest('hex').slice(0, 32);
252
320
  }
253
321
 
254
- function open(dir, prefix, agent, phase, model, attempt) {
322
+ function open(dir, prefix, agent, phase, model, attempt, metadata) {
255
323
  if (attempt == null) attempt = 1;
256
324
 
325
+ // Validate metadata keys BEFORE any lock or I/O (fail-closed).
326
+ if (metadata != null) {
327
+ _validateMetadata(metadata);
328
+ }
329
+
257
330
  const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
258
331
  const lockPath = ledgerPath + '.lock';
259
332
 
@@ -268,11 +341,25 @@ function open(dir, prefix, agent, phase, model, attempt) {
268
341
  });
269
342
 
270
343
  if (existingIndex !== -1) {
344
+ _assertUnowned(entries[existingIndex]);
345
+ // Idempotent re-open: check metadata conflict BEFORE writing anything.
346
+ if (metadata != null) {
347
+ if (!_metadataEqual(entries[existingIndex], metadata)) {
348
+ const err = new Error(
349
+ 'open: metadata conflict for operation_id ' + operation_id +
350
+ ' — supplied metadata differs from stored metadata; re-open rejected (fail-closed)'
351
+ );
352
+ err.code = 'METADATA_CONFLICT';
353
+ err.operation_id = operation_id;
354
+ throw err;
355
+ }
356
+ }
271
357
  // Resume: idempotently preserve original started_at and any positive
272
358
  // phase_delta_tokens — only reset status to running.
273
359
  entries[existingIndex] = Object.assign({}, entries[existingIndex], { status: 'running' });
274
360
  } else {
275
- entries.push({
361
+ // First open: store standard fields, then spread validated metadata fields.
362
+ const newEntry = {
276
363
  operation_id: operation_id,
277
364
  agent: agent,
278
365
  phase: phase,
@@ -281,7 +368,11 @@ function open(dir, prefix, agent, phase, model, attempt) {
281
368
  started_at: started_at,
282
369
  completed_at: null,
283
370
  phase_delta_tokens: null,
284
- });
371
+ };
372
+ if (metadata != null) {
373
+ Object.assign(newEntry, metadata);
374
+ }
375
+ entries.push(newEntry);
285
376
  }
286
377
 
287
378
  _writeLedger(ledgerPath, entries);
@@ -333,7 +424,8 @@ function close(dir, prefix, agent, tokens, attempt) {
333
424
  idx = agentIndexes[0];
334
425
  }
335
426
 
336
- const existing = entries[idx];
427
+ const existing = entries[idx];
428
+ _assertUnowned(existing);
337
429
  const completed_at = new Date().toISOString();
338
430
 
339
431
  // Token preservation (AC-10):
@@ -407,7 +499,8 @@ function fail(dir, prefix, agent, error, attempt) {
407
499
  idx = agentIndexes[0];
408
500
  }
409
501
 
410
- const existing = entries[idx];
502
+ const existing = entries[idx];
503
+ _assertUnowned(existing);
411
504
  const completed_at = new Date().toISOString();
412
505
 
413
506
  const update = {
@@ -483,7 +576,8 @@ function skip(dir, prefix, agent, phase, model, attempt) {
483
576
  }
484
577
 
485
578
  // Case 2: EXACTLY ONE match — update in place, preserving started_at and all other fields.
486
- const existing = entries[idx];
579
+ const existing = entries[idx];
580
+ _assertUnowned(existing);
487
581
  const completed_at = new Date().toISOString();
488
582
 
489
583
  entries[idx] = Object.assign({}, existing, {
@@ -500,11 +594,188 @@ function skip(dir, prefix, agent, phase, model, attempt) {
500
594
  }
501
595
  }
502
596
 
597
+ // ── finalizeActivity (INFRA-TASK-BE-02) ──────────────────────────────────────
598
+ //
599
+ // Additive terminal-state API used by the executor. Unlike close()/fail()/skip(),
600
+ // it requires an exact operation_id match (no name-only agent fallback), can
601
+ // record tokens on ANY terminal status (including 'failed'), accepts an optional
602
+ // usage_reason (additive ledger field, NOT an open() metadata whitelist key),
603
+ // and is replay-safe: finalizing an already-terminal entry with identical data
604
+ // is a no-op that returns the existing entry unchanged, while conflicting data
605
+ // fails closed without writing. It does not introduce a second ledger file or
606
+ // a duplicate token counter — it reuses phase_delta_tokens like close()/fail().
607
+
608
+ const FINALIZE_STATUSES = new Set(['done', 'failed', 'skipped']);
609
+ const TERMINAL_STATUSES = new Set(['done', 'failed', 'skipped']);
610
+
611
+ function finalizeActivity(dir, prefix, agent, attempt, options) {
612
+ if (attempt == null) attempt = 1;
613
+ options = options || {};
614
+
615
+ const status = options.status;
616
+ const tokens = (options.tokens === undefined) ? null : options.tokens;
617
+ const reason = options.reason;
618
+ const completedAtArg = options.completedAt;
619
+
620
+ if (!FINALIZE_STATUSES.has(status)) {
621
+ const err = new Error(
622
+ 'finalizeActivity: status must be one of "done", "failed", "skipped"; got ' +
623
+ JSON.stringify(status)
624
+ );
625
+ err.code = 'INVALID_STATUS';
626
+ throw err;
627
+ }
628
+
629
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
630
+ const lockPath = ledgerPath + '.lock';
631
+
632
+ const lockHandle = _acquireLock(lockPath);
633
+ try {
634
+ const { entries } = _readLedger(ledgerPath);
635
+ const operation_id = computeOperationId(prefix, agent, attempt);
636
+
637
+ // Exact-ID lookup only — no name-only agent fallback (unlike close/fail/skip).
638
+ const idx = entries.findIndex(function (e) {
639
+ return e.operation_id === operation_id;
640
+ });
641
+
642
+ if (idx === -1) {
643
+ const err = new Error(
644
+ 'finalizeActivity: no entry found for operation_id ' + operation_id +
645
+ ' in ' + ledgerPath + ' — exact operation_id match required, ' +
646
+ 'no name-only fallback; operation was never opened'
647
+ );
648
+ err.code = 'ACTIVITY_NOT_FOUND';
649
+ err.operation_id = operation_id;
650
+ throw err;
651
+ }
652
+
653
+ const existing = entries[idx];
654
+ _assertUnowned(existing);
655
+
656
+ const suppliedTokens = (tokens === undefined) ? null : tokens;
657
+ const suppliedReason = (reason === undefined) ? null : reason;
658
+
659
+ if (TERMINAL_STATUSES.has(existing.status)) {
660
+ // Already finalized: this is either an idempotent replay of identical
661
+ // data (return unchanged, no write) or conflicting data (fail closed).
662
+ const existingTokens = (existing.phase_delta_tokens === undefined)
663
+ ? null : existing.phase_delta_tokens;
664
+ const existingReason = (existing.usage_reason === undefined)
665
+ ? null : existing.usage_reason;
666
+ const effectiveCompletedAt = (completedAtArg != null)
667
+ ? completedAtArg : existing.completed_at;
668
+
669
+ const isIdentical =
670
+ existing.status === status &&
671
+ existingTokens === suppliedTokens &&
672
+ existingReason === suppliedReason &&
673
+ existing.completed_at === effectiveCompletedAt;
674
+
675
+ if (isIdentical) {
676
+ return { status: 'ok', operation_id: operation_id, entry: existing, replay: true };
677
+ }
678
+
679
+ const err = new Error(
680
+ 'finalizeActivity: conflicting terminal data for operation_id ' + operation_id +
681
+ ' in ' + ledgerPath + ' — existing terminal entry differs from the supplied ' +
682
+ 'status/tokens/reason/completedAt; refusing to overwrite (fail-closed)'
683
+ );
684
+ err.code = 'TERMINAL_CONFLICT';
685
+ err.operation_id = operation_id;
686
+ throw err;
687
+ }
688
+
689
+ // Not yet terminal: finalize it. Tokens are recorded even when status is
690
+ // 'failed' — first finalization records known tokens, or null plus an
691
+ // optional usage_reason explaining why usage is unavailable. Matching the
692
+ // close() token-preservation invariant: a null/undefined supplied value
693
+ // never clobbers an already-stored positive phase_delta_tokens.
694
+ const completed_at = (completedAtArg != null) ? completedAtArg : new Date().toISOString();
695
+
696
+ let phase_delta_tokens = existing.phase_delta_tokens;
697
+ if (suppliedTokens !== null) {
698
+ phase_delta_tokens = suppliedTokens;
699
+ }
700
+
701
+ const update = {
702
+ status: status,
703
+ completed_at: completed_at,
704
+ phase_delta_tokens: phase_delta_tokens,
705
+ };
706
+ if (reason !== undefined) {
707
+ update.usage_reason = reason;
708
+ }
709
+
710
+ // Merge update: Object.assign preserves unknown/legacy fields and metadata verbatim.
711
+ entries[idx] = Object.assign({}, existing, update);
712
+
713
+ _writeLedger(ledgerPath, entries);
714
+
715
+ return { status: 'ok', operation_id: operation_id, entry: entries[idx] };
716
+ } finally {
717
+ _releaseLock(lockPath, lockHandle);
718
+ }
719
+ }
720
+
721
+ // Workflow-owned entries cannot be finalized by worker/legacy CLI calls.
722
+ function _assertUnowned(entry) {
723
+ if (entry && entry.workflow_owner) {
724
+ const error = new Error('Workflow entry requires its owner capability');
725
+ error.code = 'LEDGER_OWNER_REQUIRED';
726
+ throw error;
727
+ }
728
+ }
729
+
730
+ function openOwned(dir, prefix, agent, phase, model, owner, metadata) {
731
+ if (typeof owner !== 'string' || !owner) throw new Error('Workflow owner required');
732
+ if (metadata) _validateMetadata(metadata);
733
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
734
+ const lockPath = ledgerPath + '.lock';
735
+ const handle = _acquireLock(lockPath);
736
+ try {
737
+ const { entries } = _readLedger(ledgerPath);
738
+ const operation_id = computeOperationId(prefix, agent, 1);
739
+ if (entries.some(e => e.operation_id === operation_id)) throw new Error('Owned operation already opened');
740
+ const entry = { operation_id, agent, phase, model, workflow_owner: owner,
741
+ status: 'running', started_at: new Date().toISOString(), completed_at: null, phase_delta_tokens: null };
742
+ if (metadata) Object.assign(entry, metadata);
743
+ entries.push(entry);
744
+ _writeLedger(ledgerPath, entries);
745
+ return entry;
746
+ } finally { _releaseLock(lockPath, handle); }
747
+ }
748
+
749
+ function finalizeOwned(dir, prefix, agent, owner, status, error, tokens) {
750
+ if (!['done', 'failed', 'skipped'].includes(status)) throw new Error('Invalid owned terminal status');
751
+ const ledgerPath = path.join(dir, prefix + '-token-ledger.json');
752
+ const lockPath = ledgerPath + '.lock';
753
+ const handle = _acquireLock(lockPath);
754
+ try {
755
+ const { entries } = _readLedger(ledgerPath);
756
+ const id = computeOperationId(prefix, agent, 1);
757
+ const entry = entries.find(e => e.operation_id === id);
758
+ if (!entry || !owner || entry.workflow_owner !== owner) throw new Error('Ledger ownership mismatch');
759
+ if (entry.status !== 'running') throw new Error('Owned terminal transition conflict');
760
+ if (tokens != null && (!Number.isInteger(tokens) || tokens <= 0)) throw new Error('Invalid owned token measurement');
761
+ if (tokens != null) entry.phase_delta_tokens = tokens;
762
+ entry.status = status;
763
+ entry.completed_at = new Date().toISOString();
764
+ if (error) entry.error = _sanitizeError(error);
765
+ _writeLedger(ledgerPath, entries);
766
+ return entry;
767
+ } finally { _releaseLock(lockPath, handle); }
768
+ }
769
+
770
+
503
771
  module.exports = {
772
+ openOwned,
773
+ finalizeOwned,
504
774
  open,
505
775
  close,
506
776
  fail,
507
777
  skip,
778
+ finalizeActivity,
508
779
  computeOperationId,
509
780
  _readLedger,
510
781
  _writeLedger,
@@ -512,4 +783,9 @@ module.exports = {
512
783
  _acquireLock,
513
784
  _releaseLock,
514
785
  _isLockStale,
786
+ _validateMetadata,
787
+ _extractStoredMetadata,
788
+ _metadataEqual,
789
+ METADATA_WHITELIST,
790
+ RESERVED_FIELDS,
515
791
  };