agentxchain 2.151.0 → 2.152.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.151.0",
3
+ "version": "2.152.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -94,6 +94,7 @@ export async function resumeCommand(opts) {
94
94
  const activeTurns = getActiveTurns(state);
95
95
  const resumeVia = opts?._via || 'resume';
96
96
  const turnResumeVia = opts?._via || 'resume --turn';
97
+ let skipRetainedRedispatch = false;
97
98
 
98
99
  if (state.status === 'active' && activeCount > 0) {
99
100
  if (activeCount === 1) {
@@ -142,7 +143,53 @@ export async function resumeCommand(opts) {
142
143
  // patched defensively) once the schema citation + migration citation are
143
144
  // documented in code and the coverage matrix.
144
145
 
145
- if (state.status === 'blocked' && activeCount > 0) {
146
+ if (state.status === 'blocked' && activeCount > 0 && resumeVia === 'operator_unblock') {
147
+ const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
148
+ if (!reactivated.ok) {
149
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
150
+ process.exit(1);
151
+ }
152
+ state = reactivated.state;
153
+ console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
154
+ if (reactivated.migration_notice) {
155
+ console.log(chalk.yellow(reactivated.migration_notice));
156
+ }
157
+ if (reactivated.phantom_notice) {
158
+ console.log(chalk.yellow(reactivated.phantom_notice));
159
+ }
160
+
161
+ const phaseReconciliation = reconcilePhaseAdvanceBeforeDispatch(root, config, state, {
162
+ allow_active_turn_cleanup: true,
163
+ allow_standing_gate: true,
164
+ });
165
+ if (!phaseReconciliation.ok && !phaseReconciliation.state) {
166
+ console.log(chalk.red(`Failed to reconcile phase gate before dispatch: ${phaseReconciliation.error}`));
167
+ process.exit(1);
168
+ }
169
+ state = phaseReconciliation.state || state;
170
+ if (phaseReconciliation.advanced) {
171
+ console.log(chalk.green(`Advanced phase before dispatch: ${phaseReconciliation.from_phase} → ${phaseReconciliation.to_phase}`));
172
+ skipRetainedRedispatch = true;
173
+ } else {
174
+ markRunBlocked(root, {
175
+ category: 'needs_human',
176
+ blockedOn: state.blocked_on || 'human:unblock_reconcile_failed',
177
+ recovery: {
178
+ typed_reason: 'needs_human',
179
+ owner: 'human',
180
+ recovery_action: 'agentxchain approve-transition or agentxchain gate show <gate>',
181
+ turn_retained: true,
182
+ detail: 'Operator unblock resolved the escalation, but no phase transition could be materialized from the current gate state.',
183
+ },
184
+ turnId: opts.turn || null,
185
+ notificationConfig: config,
186
+ });
187
+ console.log(chalk.red('Unblock did not materialize a phase transition; leaving the run blocked for manual recovery.'));
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ if (state.status === 'blocked' && activeCount > 0 && !skipRetainedRedispatch) {
146
193
  let retainedTurn = null;
147
194
  if (opts.turn) {
148
195
  retainedTurn = activeTurns[opts.turn];
@@ -1600,6 +1600,95 @@ function resolvePhaseTransitionSource(historyEntries, gateFailure, fallbackTurnI
1600
1600
  return requestedSource;
1601
1601
  }
1602
1602
 
1603
+ function buildStandingPhaseTransitionSource(state, config) {
1604
+ const phase = state?.phase;
1605
+ const routing = phase ? config?.routing?.[phase] : null;
1606
+ const gateId = routing?.exit_gate || null;
1607
+ const nextPhase = getNextPhase(phase, config?.routing || {});
1608
+ if (!phase || !gateId || !nextPhase) {
1609
+ return null;
1610
+ }
1611
+ if ((state?.phase_gate_status || {})[gateId] !== 'pending') {
1612
+ return null;
1613
+ }
1614
+ return {
1615
+ turn_id: state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
1616
+ run_id: state?.run_id || null,
1617
+ role: null,
1618
+ assigned_role: null,
1619
+ phase,
1620
+ status: 'completed',
1621
+ phase_transition_request: nextPhase,
1622
+ summary: `Synthetic ${gateId} transition source for operator-unblocked standing gate.`,
1623
+ verification: { status: 'pass' },
1624
+ };
1625
+ }
1626
+
1627
+ function getPhaseRoles(config, phase) {
1628
+ const routing = config?.routing?.[phase] || {};
1629
+ const roles = new Set();
1630
+ if (typeof routing.entry_role === 'string' && routing.entry_role) {
1631
+ roles.add(routing.entry_role);
1632
+ }
1633
+ if (Array.isArray(routing.allowed_next_roles)) {
1634
+ for (const role of routing.allowed_next_roles) {
1635
+ if (typeof role === 'string' && role) roles.add(role);
1636
+ }
1637
+ }
1638
+ return roles;
1639
+ }
1640
+
1641
+ function cleanupPhaseAdvanceArtifacts(root, state, config, fromPhase) {
1642
+ const phaseRoles = getPhaseRoles(config, fromPhase);
1643
+ const activeTurns = getActiveTurns(state);
1644
+ const removedTurnIds = [];
1645
+ const nextActiveTurns = {};
1646
+ for (const [turnId, turn] of Object.entries(activeTurns)) {
1647
+ const role = turn?.assigned_role || turn?.role_id || turn?.role || null;
1648
+ if (phaseRoles.has(role) && turn?.status !== 'accepted' && turn?.status !== 'completed') {
1649
+ removedTurnIds.push(turnId);
1650
+ continue;
1651
+ }
1652
+ nextActiveTurns[turnId] = turn;
1653
+ }
1654
+
1655
+ const nextReservations = { ...(state?.budget_reservations || {}) };
1656
+ const clearedBudgetTurnIds = [];
1657
+ for (const turnId of removedTurnIds) {
1658
+ if (Object.prototype.hasOwnProperty.call(nextReservations, turnId)) {
1659
+ delete nextReservations[turnId];
1660
+ clearedBudgetTurnIds.push(turnId);
1661
+ }
1662
+ }
1663
+
1664
+ const removedDispatchTurnIds = [];
1665
+ for (const turnId of removedTurnIds) {
1666
+ const dispatchDir = join(root, getDispatchTurnDir(turnId));
1667
+ if (existsSync(dispatchDir)) {
1668
+ try {
1669
+ rmSync(dispatchDir, { recursive: true, force: true });
1670
+ removedDispatchTurnIds.push(turnId);
1671
+ } catch {
1672
+ // Best-effort cleanup; state correctness must not depend on filesystem pruning.
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ return {
1678
+ state: {
1679
+ ...state,
1680
+ active_turns: nextActiveTurns,
1681
+ budget_reservations: nextReservations,
1682
+ },
1683
+ payload: {
1684
+ from_phase: fromPhase,
1685
+ removed_turn_ids: removedTurnIds,
1686
+ cleared_budget_turn_ids: clearedBudgetTurnIds,
1687
+ removed_dispatch_turn_ids: removedDispatchTurnIds,
1688
+ },
1689
+ };
1690
+ }
1691
+
1603
1692
  function buildBlockedReason({ category, recovery, turnId, blockedAt = new Date().toISOString() }) {
1604
1693
  return {
1605
1694
  category,
@@ -2355,7 +2444,7 @@ export function markRunBlocked(root, details) {
2355
2444
  blockedAt,
2356
2445
  });
2357
2446
 
2358
- const updatedState = {
2447
+ let updatedState = {
2359
2448
  ...state,
2360
2449
  status: 'blocked',
2361
2450
  blocked_on: details.blockedOn,
@@ -2607,13 +2696,15 @@ export function reactivateGovernedRun(root, state, details = {}) {
2607
2696
  };
2608
2697
  }
2609
2698
 
2610
- export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null) {
2699
+ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null, opts = {}) {
2611
2700
  const currentState = state && typeof state === 'object' ? state : readState(root);
2612
2701
  if (!currentState) {
2613
2702
  return { ok: false, error: 'No governed state.json found' };
2614
2703
  }
2615
2704
 
2616
- if (currentState.status !== 'active' || getActiveTurnCount(currentState) > 0) {
2705
+ const activeTurnCount = getActiveTurnCount(currentState);
2706
+ const allowActiveTurnCleanup = opts?.allow_active_turn_cleanup === true;
2707
+ if (currentState.status !== 'active' || (activeTurnCount > 0 && !allowActiveTurnCleanup)) {
2617
2708
  return {
2618
2709
  ok: true,
2619
2710
  state: attachLegacyCurrentTurnAlias(currentState),
@@ -2643,12 +2734,15 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2643
2734
  }
2644
2735
 
2645
2736
  const historyEntries = readJsonlEntries(root, HISTORY_PATH);
2646
- const phaseSource = resolvePhaseTransitionSource(
2737
+ let phaseSource = resolvePhaseTransitionSource(
2647
2738
  historyEntries,
2648
2739
  gateFailure,
2649
2740
  currentState.last_completed_turn_id || null,
2650
2741
  currentState.queued_phase_transition || null,
2651
2742
  );
2743
+ if (!phaseSource?.phase_transition_request && opts?.allow_standing_gate === true) {
2744
+ phaseSource = buildStandingPhaseTransitionSource(currentState, config);
2745
+ }
2652
2746
  if (!phaseSource?.phase_transition_request) {
2653
2747
  return {
2654
2748
  ok: true,
@@ -2685,7 +2779,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2685
2779
  if (approvalResult.action === 'auto_approve') {
2686
2780
  const now = new Date().toISOString();
2687
2781
  const prevPhase = currentState.phase;
2688
- const nextState = {
2782
+ let nextState = {
2689
2783
  ...currentState,
2690
2784
  phase: gateResult.next_phase,
2691
2785
  phase_entered_at: now,
@@ -2699,6 +2793,8 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2699
2793
  [gateResult.gate_id || 'no_gate']: 'passed',
2700
2794
  },
2701
2795
  };
2796
+ const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
2797
+ nextState = cleanup.state;
2702
2798
  writeState(root, nextState);
2703
2799
  appendJsonl(root, LEDGER_PATH, {
2704
2800
  type: 'approval_policy',
@@ -2738,6 +2834,17 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2738
2834
  trigger: 'auto_approved',
2739
2835
  },
2740
2836
  });
2837
+ emitRunEvent(root, 'phase_cleanup', {
2838
+ run_id: nextState.run_id,
2839
+ phase: nextState.phase,
2840
+ status: nextState.status,
2841
+ payload: {
2842
+ ...cleanup.payload,
2843
+ to_phase: gateResult.next_phase,
2844
+ gate_id: gateResult.gate_id || null,
2845
+ trigger: 'auto_approved',
2846
+ },
2847
+ });
2741
2848
  return {
2742
2849
  ok: true,
2743
2850
  state: attachLegacyCurrentTurnAlias(nextState),
@@ -2789,7 +2896,7 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2789
2896
 
2790
2897
  const now = new Date().toISOString();
2791
2898
  const prevPhase = currentState.phase;
2792
- const nextState = {
2899
+ let nextState = {
2793
2900
  ...currentState,
2794
2901
  phase: gateResult.next_phase,
2795
2902
  phase_entered_at: now,
@@ -2803,6 +2910,8 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2803
2910
  [gateResult.gate_id || 'no_gate']: 'passed',
2804
2911
  },
2805
2912
  };
2913
+ const cleanup = cleanupPhaseAdvanceArtifacts(root, nextState, config, prevPhase);
2914
+ nextState = cleanup.state;
2806
2915
 
2807
2916
  writeState(root, nextState);
2808
2917
  const retiredIntentIds = retireApprovedPhaseScopedIntents(root, nextState, config, prevPhase, now);
@@ -2832,6 +2941,18 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null)
2832
2941
  trigger: 'reconciled_before_dispatch',
2833
2942
  },
2834
2943
  });
2944
+ emitRunEvent(root, 'phase_cleanup', {
2945
+ run_id: nextState.run_id,
2946
+ phase: nextState.phase,
2947
+ status: nextState.status,
2948
+ turn: phaseSource.turn_id ? { turn_id: phaseSource.turn_id, role_id: phaseSource.role || phaseSource.assigned_role || null } : undefined,
2949
+ payload: {
2950
+ ...cleanup.payload,
2951
+ to_phase: gateResult.next_phase,
2952
+ gate_id: gateResult.gate_id || null,
2953
+ trigger: 'reconciled_before_dispatch',
2954
+ },
2955
+ });
2835
2956
 
2836
2957
  return {
2837
2958
  ok: true,
@@ -2876,7 +2997,7 @@ export function initializeGovernedRun(root, config, options = {}) {
2876
2997
  const now = new Date().toISOString();
2877
2998
  const provenance = buildDefaultRunProvenance(options.provenance);
2878
2999
  const repoDecisions = getActiveRepoDecisions(root);
2879
- const updatedState = {
3000
+ let updatedState = {
2880
3001
  ...state,
2881
3002
  run_id: runId,
2882
3003
  created_at: now,
@@ -4604,7 +4725,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4604
4725
  const remainingReservations = { ...(state.budget_reservations || {}) };
4605
4726
  delete remainingReservations[currentTurn.turn_id];
4606
4727
  const costUsd = turnResult.cost?.usd || 0;
4607
- const updatedState = {
4728
+ let updatedState = {
4608
4729
  ...state,
4609
4730
  turn_sequence: acceptedSequence,
4610
4731
  last_completed_turn_id: currentTurn.turn_id,
@@ -4946,6 +5067,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4946
5067
  [gateResult.gate_id || 'no_gate']: 'passed',
4947
5068
  };
4948
5069
  updatedState.queued_phase_transition = null;
5070
+ const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, prevPhase);
5071
+ updatedState = cleanup.state;
4949
5072
  const retiredIntentIds = retireApprovedPhaseScopedIntents(root, updatedState, config, prevPhase, now);
4950
5073
  if (retiredIntentIds.length > 0) {
4951
5074
  emitRunEvent(root, 'intent_retired_by_phase_advance', {
@@ -4973,6 +5096,18 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4973
5096
  trigger: 'auto',
4974
5097
  },
4975
5098
  });
5099
+ emitRunEvent(root, 'phase_cleanup', {
5100
+ run_id: updatedState.run_id,
5101
+ phase: updatedState.phase,
5102
+ status: updatedState.status,
5103
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
5104
+ payload: {
5105
+ ...cleanup.payload,
5106
+ to_phase: gateResult.next_phase,
5107
+ gate_id: gateResult.gate_id || null,
5108
+ trigger: 'auto',
5109
+ },
5110
+ });
4976
5111
  } else if (gateResult.action === 'awaiting_human_approval') {
4977
5112
  // Evaluate approval policy — may auto-approve
4978
5113
  const approvalResult = evaluateApprovalPolicy({
@@ -4992,6 +5127,8 @@ function _acceptGovernedTurnLocked(root, config, opts) {
4992
5127
  [gateResult.gate_id || 'no_gate']: 'passed',
4993
5128
  };
4994
5129
  updatedState.queued_phase_transition = null;
5130
+ const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, prevPhase);
5131
+ updatedState = cleanup.state;
4995
5132
  ledgerEntries.push({
4996
5133
  type: 'approval_policy',
4997
5134
  gate_type: 'phase_transition',
@@ -5030,6 +5167,18 @@ function _acceptGovernedTurnLocked(root, config, opts) {
5030
5167
  trigger: 'auto_approved',
5031
5168
  },
5032
5169
  });
5170
+ emitRunEvent(root, 'phase_cleanup', {
5171
+ run_id: updatedState.run_id,
5172
+ phase: updatedState.phase,
5173
+ status: updatedState.status,
5174
+ turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
5175
+ payload: {
5176
+ ...cleanup.payload,
5177
+ to_phase: gateResult.next_phase,
5178
+ gate_id: gateResult.gate_id || null,
5179
+ trigger: 'auto_approved',
5180
+ },
5181
+ });
5033
5182
  } else {
5034
5183
  updatedState.status = 'paused';
5035
5184
  updatedState.blocked_on = `human_approval:${gateResult.gate_id}`;
@@ -5975,7 +6124,7 @@ export function approvePhaseTransition(root, config, opts = {}) {
5975
6124
  appendJsonl(root, LEDGER_PATH, entry);
5976
6125
  }
5977
6126
 
5978
- const updatedState = {
6127
+ let updatedState = {
5979
6128
  ...state,
5980
6129
  phase: transition.to,
5981
6130
  phase_entered_at: new Date().toISOString(),
@@ -5989,6 +6138,8 @@ export function approvePhaseTransition(root, config, opts = {}) {
5989
6138
  [transition.gate]: 'passed',
5990
6139
  },
5991
6140
  };
6141
+ const cleanup = cleanupPhaseAdvanceArtifacts(root, updatedState, config, transition.from);
6142
+ updatedState = cleanup.state;
5992
6143
 
5993
6144
  writeState(root, updatedState);
5994
6145
  clearSlaReminders(root, 'pending_phase_transition');
@@ -6009,6 +6160,17 @@ export function approvePhaseTransition(root, config, opts = {}) {
6009
6160
  trigger: 'human_approved',
6010
6161
  },
6011
6162
  });
6163
+ emitRunEvent(root, 'phase_cleanup', {
6164
+ run_id: updatedState.run_id,
6165
+ phase: updatedState.phase,
6166
+ status: 'active',
6167
+ payload: {
6168
+ ...cleanup.payload,
6169
+ to_phase: transition.to,
6170
+ gate_id: transition.gate || null,
6171
+ trigger: 'human_approved',
6172
+ },
6173
+ });
6012
6174
 
6013
6175
  // Session checkpoint — non-fatal
6014
6176
  writeSessionCheckpoint(root, updatedState, 'phase_approved');
@@ -38,6 +38,7 @@ export const VALID_RUN_EVENTS = [
38
38
  'gate_pending',
39
39
  'gate_approved',
40
40
  'gate_failed',
41
+ 'phase_cleanup',
41
42
  'budget_exceeded_warn',
42
43
  'human_escalation_raised',
43
44
  'human_escalation_resolved',