agentxchain 2.155.18 → 2.155.20

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.155.18",
3
+ "version": "2.155.20",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@ import { loadProjectContext, loadProjectState } from '../lib/config.js';
21
21
  import {
22
22
  initializeGovernedRun,
23
23
  assignGovernedTurn,
24
+ reissueTurn,
24
25
  deriveAfterDispatchHookRecoveryAction,
25
26
  markRunBlocked,
26
27
  getActiveTurns,
@@ -44,6 +45,7 @@ import { runHooks } from '../lib/hook-runner.js';
44
45
  import { summarizeRunProvenance } from '../lib/run-provenance.js';
45
46
  import { consumeNextApprovedIntent } from '../lib/intake.js';
46
47
  import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
48
+ import { resolveGovernedRole } from '../lib/role-resolution.js';
47
49
 
48
50
  function hasStandingPendingExitGate(state, config) {
49
51
  const phase = state?.phase;
@@ -442,12 +444,6 @@ export async function resumeCommand(opts) {
442
444
  process.exit(1);
443
445
  }
444
446
 
445
- printResumeRunContext({ root, state, config });
446
- console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
447
- console.log(` Role: ${retainedTurn.assigned_role}`);
448
- console.log(` Attempt: ${retainedTurn.attempt}`);
449
- console.log('');
450
-
451
447
  const reactivated = reactivateGovernedRun(root, state, { via: turnResumeVia, notificationConfig: config });
452
448
  if (!reactivated.ok) {
453
449
  console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
@@ -461,6 +457,38 @@ export async function resumeCommand(opts) {
461
457
  console.log(chalk.yellow(reactivated.phantom_notice));
462
458
  }
463
459
 
460
+ const materializationResolution = resolveGovernedRole({ state, config });
461
+ if (
462
+ state.charter_materialization_pending
463
+ && state.phase === 'planning'
464
+ && materializationResolution.roleId
465
+ && materializationResolution.roleId !== retainedTurn.assigned_role
466
+ && !opts.turn
467
+ ) {
468
+ const reason = `charter materialization pending superseded stale retained ${retainedTurn.assigned_role} turn`;
469
+ const reissued = reissueTurn(root, config, {
470
+ turnId: retainedTurn.turn_id,
471
+ roleId: materializationResolution.roleId,
472
+ reason,
473
+ });
474
+ if (!reissued.ok) {
475
+ console.log(chalk.red(`Failed to reissue retained turn for materialization: ${reissued.error}`));
476
+ process.exit(1);
477
+ }
478
+ state = reissued.state;
479
+ retainedTurn = reissued.newTurn;
480
+ console.log(chalk.yellow(`Reissued retained turn for charter materialization: ${retainedTurn.turn_id}`));
481
+ console.log(` Role: ${retainedTurn.assigned_role}`);
482
+ console.log(` Reason: ${reason}`);
483
+ console.log('');
484
+ } else {
485
+ printResumeRunContext({ root, state, config });
486
+ console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
487
+ console.log(` Role: ${retainedTurn.assigned_role}`);
488
+ console.log(` Attempt: ${retainedTurn.attempt}`);
489
+ console.log('');
490
+ }
491
+
464
492
  const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
465
493
  if (!bundleResult.ok) {
466
494
  console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
@@ -709,38 +737,24 @@ function printResumeRunContext({ root, state, config }) {
709
737
  }
710
738
 
711
739
  function resolveTargetRole(opts, state, config) {
712
- const phase = state.phase;
713
- const routing = config.routing?.[phase];
714
-
715
- if (opts.role) {
716
- // Validate the override
717
- if (!config.roles?.[opts.role]) {
718
- console.log(chalk.red(`Unknown role: "${opts.role}"`));
719
- console.log(chalk.dim(`Available roles: ${Object.keys(config.roles || {}).join(', ')}`));
720
- return null;
721
- }
722
- if (routing?.allowed_next_roles && !routing.allowed_next_roles.includes(opts.role) && opts.role !== 'human') {
723
- console.log(chalk.yellow(`Warning: role "${opts.role}" is not in allowed_next_roles for phase "${phase}".`));
724
- console.log(chalk.dim(`Allowed: ${routing.allowed_next_roles.join(', ')}`));
725
- // Allow it as an override, but warn
740
+ const resolved = resolveGovernedRole({ override: opts.role || null, state, config });
741
+ if (resolved.error) {
742
+ console.log(chalk.red(resolved.error));
743
+ if (resolved.availableRoles.length) {
744
+ console.log(chalk.dim(`Available roles: ${resolved.availableRoles.join(', ')}`));
726
745
  }
727
- return opts.role;
746
+ return null;
728
747
  }
729
748
 
730
- // Default: use the phase's entry_role
731
- if (routing?.entry_role) {
732
- return routing.entry_role;
749
+ if (!opts.role && state.next_recommended_role && resolved.roleId === state.next_recommended_role) {
750
+ console.log(chalk.dim(`Using recommended role: ${resolved.roleId} (from previous turn)`));
733
751
  }
734
752
 
735
- // Fallback: first role in config
736
- const roles = Object.keys(config.roles || {});
737
- if (roles.length > 0) {
738
- console.log(chalk.yellow(`No entry_role for phase "${phase}". Defaulting to "${roles[0]}".`));
739
- return roles[0];
753
+ for (const warning of resolved.warnings) {
754
+ console.log(chalk.yellow(`Warning: ${warning}`));
740
755
  }
741
756
 
742
- console.log(chalk.red('No roles defined in config.'));
743
- return null;
757
+ return resolved.roleId;
744
758
  }
745
759
 
746
760
  function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
@@ -30,6 +30,7 @@ import {
30
30
  assignGovernedTurn,
31
31
  acceptGovernedTurn,
32
32
  deriveAfterDispatchHookRecoveryAction,
33
+ reissueTurn,
33
34
  rejectGovernedTurn,
34
35
  markRunBlocked,
35
36
  getActiveTurnCount,
@@ -99,6 +100,35 @@ export async function stepCommand(opts) {
99
100
  process.exit(1);
100
101
  }
101
102
 
103
+ if (opts.resume && !opts.turn) {
104
+ const activeTurnsBeforeStaleCheck = getActiveTurns(state);
105
+ const activeTurnListBeforeStaleCheck = Object.values(activeTurnsBeforeStaleCheck);
106
+ const retainedTurn = activeTurnListBeforeStaleCheck.length === 1 ? activeTurnListBeforeStaleCheck[0] : null;
107
+ const materializationResolution = resolveGovernedRole({ state, config });
108
+ if (
109
+ retainedTurn
110
+ && state.status === 'active'
111
+ && state.charter_materialization_pending
112
+ && state.phase === 'planning'
113
+ && materializationResolution.roleId
114
+ && materializationResolution.roleId !== retainedTurn.assigned_role
115
+ ) {
116
+ const reason = `charter materialization pending superseded stale active ${retainedTurn.assigned_role} turn`;
117
+ const reissued = reissueTurn(root, config, {
118
+ turnId: retainedTurn.turn_id,
119
+ roleId: materializationResolution.roleId,
120
+ reason,
121
+ });
122
+ if (!reissued.ok) {
123
+ console.log(chalk.red(`Failed to reissue active turn for materialization: ${reissued.error}`));
124
+ process.exit(1);
125
+ }
126
+ state = reissued.state;
127
+ console.log(chalk.yellow(`Reissued active turn for charter materialization: ${reissued.newTurn.turn_id}`));
128
+ console.log(` Role: ${reissued.newTurn.assigned_role}`);
129
+ }
130
+ }
131
+
102
132
  const staleReconciliation = reconcileStaleTurns(root, state, config);
103
133
  state = staleReconciliation.state || state;
104
134
  if (staleReconciliation.ghost_turns.length > 0) {
@@ -251,6 +281,30 @@ export async function stepCommand(opts) {
251
281
  state = reactivated.state;
252
282
  skipAssignment = true;
253
283
 
284
+ const materializationResolution = resolveGovernedRole({ state, config });
285
+ if (
286
+ state.charter_materialization_pending
287
+ && state.phase === 'planning'
288
+ && materializationResolution.roleId
289
+ && materializationResolution.roleId !== targetTurn.assigned_role
290
+ && !opts.turn
291
+ ) {
292
+ const reason = `charter materialization pending superseded stale retained ${targetTurn.assigned_role} turn`;
293
+ const reissued = reissueTurn(root, config, {
294
+ turnId: targetTurn.turn_id,
295
+ roleId: materializationResolution.roleId,
296
+ reason,
297
+ });
298
+ if (!reissued.ok) {
299
+ console.log(chalk.red(`Failed to reissue retained turn for materialization: ${reissued.error}`));
300
+ process.exit(1);
301
+ }
302
+ state = reissued.state;
303
+ targetTurn = reissued.newTurn;
304
+ console.log(chalk.yellow(`Reissued retained turn for charter materialization: ${targetTurn.turn_id}`));
305
+ console.log(` Role: ${targetTurn.assigned_role}`);
306
+ }
307
+
254
308
  // BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
255
309
  refreshTurnBaselineSnapshot(root, targetTurn.turn_id);
256
310
  state = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8'));
@@ -3712,6 +3712,7 @@ export function refreshTurnBaselineSnapshot(root, turnId) {
3712
3712
  * @param {object} config - normalized config
3713
3713
  * @param {object} opts
3714
3714
  * @param {string} [opts.turnId] - specific turn to reissue
3715
+ * @param {string} [opts.roleId] - replacement role; defaults to original turn role
3715
3716
  * @param {string} [opts.reason] - reason for reissue
3716
3717
  * @returns {{ ok: boolean, state?: object, newTurn?: object, baselineDelta?: object, error?: string }}
3717
3718
  */
@@ -3769,7 +3770,8 @@ export function reissueTurn(root, config, opts = {}) {
3769
3770
  const oldTurn = activeTurns[turnId];
3770
3771
  if (!oldTurn) return { ok: false, error: `Turn ${turnId} not found in active turns` };
3771
3772
 
3772
- const roleId = oldTurn.assigned_role;
3773
+ const oldRoleId = oldTurn.assigned_role;
3774
+ const roleId = opts.roleId || oldRoleId;
3773
3775
  const role = config.roles?.[roleId];
3774
3776
  if (!role) return { ok: false, error: `Role "${roleId}" not found in config` };
3775
3777
 
@@ -3795,7 +3797,7 @@ export function reissueTurn(root, config, opts = {}) {
3795
3797
  appendJsonl(root, HISTORY_PATH, {
3796
3798
  turn_id: oldTurn.turn_id,
3797
3799
  run_id: state.run_id,
3798
- role: roleId,
3800
+ role: oldRoleId,
3799
3801
  phase: state.phase,
3800
3802
  status: 'reissued',
3801
3803
  summary: `Turn reissued: ${reason}`,
@@ -3811,7 +3813,8 @@ export function reissueTurn(root, config, opts = {}) {
3811
3813
  timestamp: now,
3812
3814
  decision: 'turn_reissued',
3813
3815
  turn_id: oldTurn.turn_id,
3814
- role: roleId,
3816
+ role: oldRoleId,
3817
+ new_role: roleId,
3815
3818
  phase: state.phase,
3816
3819
  reason,
3817
3820
  old_baseline: {
@@ -3895,6 +3898,8 @@ export function reissueTurn(root, config, opts = {}) {
3895
3898
  new_head: newBaseline.head_ref,
3896
3899
  old_runtime: oldRuntimeId,
3897
3900
  new_runtime: currentRuntimeId,
3901
+ old_role: oldRoleId,
3902
+ new_role: roleId,
3898
3903
  },
3899
3904
  });
3900
3905