agentxchain 2.128.0 → 2.130.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/README.md +2 -0
  2. package/bin/agentxchain.js +38 -4
  3. package/package.json +1 -1
  4. package/scripts/verify-post-publish.sh +55 -5
  5. package/src/commands/accept-turn.js +14 -0
  6. package/src/commands/checkpoint-turn.js +35 -0
  7. package/src/commands/connector.js +17 -2
  8. package/src/commands/doctor.js +151 -1
  9. package/src/commands/events.js +7 -1
  10. package/src/commands/init.js +42 -11
  11. package/src/commands/inject.js +1 -1
  12. package/src/commands/mission.js +803 -7
  13. package/src/commands/reissue-turn.js +122 -0
  14. package/src/commands/reject-turn.js +60 -6
  15. package/src/commands/restart.js +81 -10
  16. package/src/commands/resume.js +20 -9
  17. package/src/commands/run.js +13 -0
  18. package/src/commands/status.js +58 -4
  19. package/src/commands/step.js +49 -10
  20. package/src/commands/validate.js +78 -20
  21. package/src/lib/cli-version.js +106 -0
  22. package/src/lib/connector-probe.js +146 -5
  23. package/src/lib/continuous-run.js +22 -87
  24. package/src/lib/coordinator-dispatch.js +25 -0
  25. package/src/lib/dispatch-bundle.js +39 -0
  26. package/src/lib/governed-state.js +624 -11
  27. package/src/lib/governed-templates.js +1 -0
  28. package/src/lib/intake.js +233 -77
  29. package/src/lib/mission-plans.js +510 -6
  30. package/src/lib/missions.js +65 -6
  31. package/src/lib/normalized-config.js +50 -15
  32. package/src/lib/repo-observer.js +8 -2
  33. package/src/lib/run-events.js +5 -0
  34. package/src/lib/run-loop.js +25 -0
  35. package/src/lib/runner-interface.js +2 -0
  36. package/src/lib/session-checkpoint.js +18 -2
  37. package/src/lib/turn-checkpoint.js +221 -0
  38. package/src/templates/governed/full-local-cli.json +71 -0
@@ -12,6 +12,7 @@ export const VALID_GOVERNED_TEMPLATE_IDS = Object.freeze([
12
12
  'cli-tool',
13
13
  'library',
14
14
  'web-app',
15
+ 'full-local-cli',
15
16
  'enterprise-app',
16
17
  ]);
17
18
 
package/src/lib/intake.js CHANGED
@@ -27,6 +27,8 @@ const INTENT_ID_RE = /^intent_\d+_[0-9a-f]{4}$/;
27
27
  // intent files, but current first-party intake writers do not transition into it.
28
28
  const S1_STATES = new Set(['detected', 'triaged', 'approved', 'planned', 'executing', 'blocked', 'completed', 'failed', 'suppressed', 'rejected']);
29
29
  const TERMINAL_STATES = new Set(['suppressed', 'rejected', 'completed', 'failed']);
30
+ const DISPATCHABLE_STATUSES = new Set(['planned', 'approved']);
31
+ const PRIORITY_RANK = { p0: 0, p1: 1, p2: 2, p3: 3 };
30
32
 
31
33
  const VALID_TRANSITIONS = {
32
34
  detected: ['triaged', 'suppressed'],
@@ -502,6 +504,196 @@ export function intakeStatus(root, intentId) {
502
504
  return { ok: true, summary, exitCode: 0 };
503
505
  }
504
506
 
507
+ export function findNextDispatchableIntent(root) {
508
+ const dirs = intakeDirs(root);
509
+ if (!existsSync(dirs.intents)) {
510
+ return { ok: false, error: 'no intents directory' };
511
+ }
512
+
513
+ const intents = readJsonDir(dirs.intents)
514
+ .filter((intent) => intent && DISPATCHABLE_STATUSES.has(intent.status));
515
+
516
+ if (intents.length === 0) {
517
+ return { ok: false, error: 'no dispatchable intents' };
518
+ }
519
+
520
+ const approved = intents.filter((intent) => intent.status === 'approved');
521
+ const candidates = approved.length > 0 ? approved : intents;
522
+
523
+ candidates.sort((a, b) => {
524
+ const aPriority = PRIORITY_RANK[a.priority] ?? Number.MAX_SAFE_INTEGER;
525
+ const bPriority = PRIORITY_RANK[b.priority] ?? Number.MAX_SAFE_INTEGER;
526
+ if (aPriority !== bPriority) return aPriority - bPriority;
527
+
528
+ const aTime = Date.parse(a.approved_at || a.planned_at || a.created_at || a.updated_at || 0);
529
+ const bTime = Date.parse(b.approved_at || b.planned_at || b.created_at || b.updated_at || 0);
530
+ if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) {
531
+ return aTime - bTime;
532
+ }
533
+
534
+ return String(a.intent_id || '').localeCompare(String(b.intent_id || ''));
535
+ });
536
+
537
+ const intent = candidates[0];
538
+ return {
539
+ ok: true,
540
+ intentId: intent.intent_id,
541
+ status: intent.status,
542
+ priority: intent.priority || null,
543
+ charter: intent.charter || null,
544
+ acceptance_count: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract.length : 0,
545
+ intent,
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Return all approved-but-unconsumed intents sorted by priority (BUG-15).
551
+ * Used by `status` to surface the pending intent queue.
552
+ */
553
+ export function findPendingApprovedIntents(root) {
554
+ const dirs = intakeDirs(root);
555
+ if (!existsSync(dirs.intents)) return [];
556
+
557
+ return readJsonDir(dirs.intents)
558
+ .filter((intent) => intent && intent.status === 'approved')
559
+ .sort((a, b) => {
560
+ const aPriority = PRIORITY_RANK[a.priority] ?? Number.MAX_SAFE_INTEGER;
561
+ const bPriority = PRIORITY_RANK[b.priority] ?? Number.MAX_SAFE_INTEGER;
562
+ if (aPriority !== bPriority) return aPriority - bPriority;
563
+ const aTime = Date.parse(a.approved_at || a.created_at || 0);
564
+ const bTime = Date.parse(b.approved_at || b.created_at || 0);
565
+ if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) return aTime - bTime;
566
+ return String(a.intent_id || '').localeCompare(String(b.intent_id || ''));
567
+ })
568
+ .map((intent) => ({
569
+ intent_id: intent.intent_id,
570
+ priority: intent.priority || 'p0',
571
+ charter: intent.charter || intent.description || null,
572
+ acceptance_count: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract.length : 0,
573
+ approved_at: intent.approved_at || null,
574
+ }));
575
+ }
576
+
577
+ /**
578
+ * Unified intent consumption entry point (BUG-16).
579
+ * Both manual (resume/step --resume) and continuous/scheduler paths should call
580
+ * this single function to consume the next approved intent.
581
+ *
582
+ * @param {string} root
583
+ * @param {{ role?: string, writeDispatchBundle?: boolean, allowTerminalRestart?: boolean, provenance?: object }} options
584
+ * @returns {{ ok: boolean, intentId?: string, intent?: object, error?: string }}
585
+ */
586
+ export function consumeNextApprovedIntent(root, options = {}) {
587
+ const queued = findNextDispatchableIntent(root);
588
+ if (!queued.ok) {
589
+ return { ok: false, error: queued.error || 'no dispatchable intents' };
590
+ }
591
+
592
+ const prepared = prepareIntentForDispatch(root, queued.intentId, {
593
+ role: options.role,
594
+ writeDispatchBundle: options.writeDispatchBundle ?? false,
595
+ allowTerminalRestart: options.allowTerminalRestart ?? false,
596
+ provenance: options.provenance,
597
+ });
598
+
599
+ if (!prepared.ok) {
600
+ return { ok: false, error: prepared.error, intentId: queued.intentId };
601
+ }
602
+
603
+ return {
604
+ ok: true,
605
+ intentId: queued.intentId,
606
+ status: queued.status,
607
+ priority: queued.priority,
608
+ charter: queued.charter,
609
+ intent: prepared.intent,
610
+ run_id: prepared.run_id,
611
+ turn_id: prepared.turn_id,
612
+ role: prepared.role,
613
+ };
614
+ }
615
+
616
+ export function prepareIntentForDispatch(root, intentId, options = {}) {
617
+ const loadedIntent = readIntent(root, intentId);
618
+ if (!loadedIntent.ok) {
619
+ return loadedIntent;
620
+ }
621
+
622
+ const startingStatus = loadedIntent.intent.status;
623
+ let intent = loadedIntent.intent;
624
+ let planned = false;
625
+
626
+ if (intent.status === 'approved') {
627
+ const plannedResult = planIntent(root, intent.intent_id, {
628
+ projectName: options.projectName,
629
+ force: options.forcePlan === true,
630
+ });
631
+ if (!plannedResult.ok) {
632
+ return {
633
+ ok: false,
634
+ error: `plan failed: ${plannedResult.error}`,
635
+ intent_status: intent.status,
636
+ exitCode: plannedResult.exitCode || 1,
637
+ };
638
+ }
639
+ intent = plannedResult.intent;
640
+ planned = true;
641
+ }
642
+
643
+ if (intent.status === 'planned') {
644
+ const startResult = startIntent(root, intent.intent_id, {
645
+ allowTerminalRestart: options.allowTerminalRestart === true,
646
+ provenance: options.provenance,
647
+ role: options.role || undefined,
648
+ writeDispatchBundle: options.writeDispatchBundle,
649
+ });
650
+ if (!startResult.ok) {
651
+ return {
652
+ ok: false,
653
+ error: `start failed: ${startResult.error}`,
654
+ intent_status: intent.status,
655
+ exitCode: startResult.exitCode || 1,
656
+ };
657
+ }
658
+ return {
659
+ ok: true,
660
+ intent_id: intent.intent_id,
661
+ starting_status: startingStatus,
662
+ final_status: startResult.intent.status,
663
+ planned,
664
+ started: true,
665
+ run_id: startResult.run_id,
666
+ turn_id: startResult.turn_id,
667
+ role: startResult.role,
668
+ intent: startResult.intent,
669
+ exitCode: 0,
670
+ };
671
+ }
672
+
673
+ if (intent.status === 'executing') {
674
+ return {
675
+ ok: true,
676
+ intent_id: intent.intent_id,
677
+ starting_status: startingStatus,
678
+ final_status: intent.status,
679
+ planned,
680
+ started: false,
681
+ run_id: intent.target_run || null,
682
+ turn_id: intent.target_turn || null,
683
+ role: options.role || null,
684
+ intent,
685
+ exitCode: 0,
686
+ };
687
+ }
688
+
689
+ return {
690
+ ok: false,
691
+ error: `intent ${intentId} is in unsupported status "${intent.status}" for dispatch preparation`,
692
+ intent_status: intent.status,
693
+ exitCode: 1,
694
+ };
695
+ }
696
+
505
697
  // ---------------------------------------------------------------------------
506
698
  // Approve
507
699
  // ---------------------------------------------------------------------------
@@ -750,7 +942,9 @@ export function startIntent(root, intentId, options = {}) {
750
942
  }
751
943
 
752
944
  // Assign governed turn
753
- const assignResult = assignGovernedTurn(root, config, roleId.role);
945
+ const assignResult = assignGovernedTurn(root, config, roleId.role, {
946
+ intakeContext,
947
+ });
754
948
  if (!assignResult.ok) {
755
949
  return { ok: false, error: `turn assignment failed: ${assignResult.error}`, exitCode: 1 };
756
950
  }
@@ -763,24 +957,18 @@ export function startIntent(root, intentId, options = {}) {
763
957
  return { ok: false, error: 'turn assignment succeeded but turn not found in state', exitCode: 1 };
764
958
  }
765
959
 
766
- assignedTurn.intake_context = intakeContext;
767
- if (state.active_turns?.[assignedTurn.turn_id]) {
768
- state.active_turns[assignedTurn.turn_id].intake_context = intakeContext;
769
- safeWriteJson(statePath, state);
770
- }
960
+ if (options.writeDispatchBundle !== false) {
961
+ const bundleResult = writeDispatchBundle(root, state, config);
962
+ if (!bundleResult.ok) {
963
+ return { ok: false, error: `dispatch bundle failed: ${bundleResult.error}`, exitCode: 1 };
964
+ }
771
965
 
772
- // Write dispatch bundle
773
- const bundleResult = writeDispatchBundle(root, state, config);
774
- if (!bundleResult.ok) {
775
- return { ok: false, error: `dispatch bundle failed: ${bundleResult.error}`, exitCode: 1 };
966
+ finalizeDispatchManifest(root, assignedTurn.turn_id, {
967
+ run_id: state.run_id,
968
+ role: assignedTurn.assigned_role,
969
+ });
776
970
  }
777
971
 
778
- // Finalize dispatch manifest
779
- finalizeDispatchManifest(root, assignedTurn.turn_id, {
780
- run_id: state.run_id,
781
- role: assignedTurn.assigned_role,
782
- });
783
-
784
972
  // Update intent: planned → executing
785
973
  const now = nowISO();
786
974
  intent.status = 'executing';
@@ -967,6 +1155,18 @@ export function resolveIntent(root, intentId) {
967
1155
 
968
1156
  const { intent, intentPath, dirs } = loadedIntent;
969
1157
 
1158
+ if (intent.status === 'completed') {
1159
+ return {
1160
+ ok: true,
1161
+ intent,
1162
+ previous_status: 'completed',
1163
+ new_status: 'completed',
1164
+ run_outcome: 'completed',
1165
+ no_change: true,
1166
+ exitCode: 0,
1167
+ };
1168
+ }
1169
+
970
1170
  if (intent.status !== 'executing' && intent.status !== 'blocked') {
971
1171
  return {
972
1172
  ok: false,
@@ -1426,83 +1626,39 @@ export function consumePreemptionMarker(root, options = {}) {
1426
1626
  };
1427
1627
  }
1428
1628
 
1429
- const startingStatus = loadedIntent.intent.status;
1430
- let intent = loadedIntent.intent;
1431
- let planned = false;
1432
- let started = false;
1433
-
1434
- if (intent.status === 'approved') {
1435
- const plannedResult = planIntent(root, intent.intent_id, {
1436
- projectName: options.projectName,
1437
- force: options.forcePlan === true,
1438
- });
1439
- if (!plannedResult.ok) {
1440
- return {
1441
- ok: false,
1442
- error: `failed to plan injected intent ${intent.intent_id}: ${plannedResult.error}`,
1443
- marker,
1444
- intent_status: intent.status,
1445
- exitCode: plannedResult.exitCode || 1,
1446
- };
1447
- }
1448
- intent = plannedResult.intent;
1449
- planned = true;
1450
- }
1451
-
1452
- if (intent.status === 'planned') {
1453
- const startResult = startIntent(root, intent.intent_id, {
1454
- role: options.role || undefined,
1455
- });
1456
- if (!startResult.ok) {
1457
- return {
1458
- ok: false,
1459
- error: `failed to start injected intent ${intent.intent_id}: ${startResult.error}`,
1460
- marker,
1461
- intent_status: intent.status,
1462
- exitCode: startResult.exitCode || 1,
1463
- };
1464
- }
1465
- clearPreemptionMarker(root);
1629
+ const prepared = prepareIntentForDispatch(root, marker.intent_id, options);
1630
+ if (!prepared.ok) {
1466
1631
  return {
1467
- ok: true,
1632
+ ...prepared,
1633
+ error: `failed to prepare injected intent ${marker.intent_id}: ${prepared.error}`,
1468
1634
  marker,
1469
- intent_id: intent.intent_id,
1470
- starting_status: startingStatus,
1471
- final_status: startResult.intent.status,
1472
- planned,
1473
- started: true,
1474
- run_id: startResult.run_id,
1475
- turn_id: startResult.turn_id,
1476
- role: startResult.role,
1477
- intent: startResult.intent,
1478
- exitCode: 0,
1479
1635
  };
1480
1636
  }
1481
1637
 
1482
- if (intent.status === 'executing') {
1638
+ if (prepared.final_status === 'executing') {
1483
1639
  clearPreemptionMarker(root);
1484
1640
  return {
1485
1641
  ok: true,
1486
1642
  marker,
1487
- intent_id: intent.intent_id,
1488
- starting_status: startingStatus,
1489
- final_status: intent.status,
1490
- planned,
1491
- started: false,
1492
- run_id: intent.target_run || null,
1493
- turn_id: intent.target_turn || null,
1494
- role: options.role || null,
1495
- intent,
1643
+ intent_id: prepared.intent_id,
1644
+ starting_status: prepared.starting_status,
1645
+ final_status: prepared.final_status,
1646
+ planned: prepared.planned,
1647
+ started: prepared.started,
1648
+ run_id: prepared.run_id,
1649
+ turn_id: prepared.turn_id,
1650
+ role: prepared.role,
1651
+ intent: prepared.intent,
1496
1652
  exitCode: 0,
1497
1653
  };
1498
1654
  }
1499
1655
 
1500
1656
  return {
1501
1657
  ok: false,
1502
- error: `cannot consume preemption marker from intent status "${intent.status}"`,
1658
+ error: `cannot consume preemption marker from intent status "${prepared.final_status}"`,
1503
1659
  marker,
1504
- intent_id: intent.intent_id,
1505
- intent_status: intent.status,
1660
+ intent_id: prepared.intent_id,
1661
+ intent_status: prepared.final_status,
1506
1662
  exitCode: 1,
1507
1663
  };
1508
1664
  }