agentxchain 2.154.9 → 2.154.11

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.154.9",
3
+ "version": "2.154.11",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@ import {
27
27
  getActiveTurnCount,
28
28
  reactivateGovernedRun,
29
29
  reconcilePhaseAdvanceBeforeDispatch,
30
+ reconcileRunCompletionBeforeDispatch,
30
31
  transitionActiveTurnLifecycle,
31
32
  STATE_PATH,
32
33
  } from '../lib/governed-state.js';
@@ -59,6 +60,13 @@ function getStandingPendingExitGate(state, config) {
59
60
  return config?.gates?.[gateId] || null;
60
61
  }
61
62
 
63
+ function isFinalPhase(state, config) {
64
+ const phase = state?.phase;
65
+ if (!phase) return false;
66
+ const phases = Object.keys(config?.routing || {});
67
+ return phases.length > 0 && phases[phases.length - 1] === phase;
68
+ }
69
+
62
70
  function entrySatisfiesSyntheticGateVerification(gate, entry) {
63
71
  if (!gate?.requires_verification_pass) return true;
64
72
  const verificationStatus = entry?.verification?.status;
@@ -122,7 +130,7 @@ function turnCanApproveHumanGateFromEscalation(root, state, config, entry, propo
122
130
 
123
131
  function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {}) {
124
132
  const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
125
- if (!turnId) return false;
133
+ if (!turnId || isFinalPhase(state, config)) return false;
126
134
  const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
127
135
  if (!existsSync(historyPath)) return false;
128
136
  const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
@@ -167,6 +175,35 @@ function latestCompletedTurnWantsPhaseContinuation(root, state, config, opts = {
167
175
  return false;
168
176
  }
169
177
 
178
+ function latestCompletedTurnWantsRunCompletion(root, state, config, opts = {}) {
179
+ const turnId = opts?.turnId || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
180
+ if (!turnId || !isFinalPhase(state, config)) return false;
181
+ const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
182
+ if (!existsSync(historyPath)) return false;
183
+ const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
184
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
185
+ let entry = null;
186
+ try {
187
+ entry = JSON.parse(lines[index]);
188
+ } catch {
189
+ continue;
190
+ }
191
+ if (entry?.turn_id !== turnId) continue;
192
+ if (entry.run_completion_request === true) return true;
193
+ const standingGate = getStandingPendingExitGate(state, config);
194
+ const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
195
+ if (
196
+ entry.status === 'needs_human'
197
+ && entrySatisfiesSyntheticGateVerification(standingGate, entry)
198
+ && turnCanApproveHumanGateFromEscalation(root, state, config, entry, proposed)
199
+ ) {
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+ return false;
205
+ }
206
+
170
207
  export async function resumeCommand(opts) {
171
208
  const context = loadProjectContext();
172
209
  if (!context) {
@@ -329,6 +366,57 @@ export async function resumeCommand(opts) {
329
366
  }
330
367
  }
331
368
 
369
+ if (
370
+ state.status === 'blocked'
371
+ && resumeVia === 'operator_unblock'
372
+ && hasStandingPendingExitGate(state, config)
373
+ && latestCompletedTurnWantsRunCompletion(root, state, config, { turnId: opts.turn || null })
374
+ ) {
375
+ const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
376
+ if (!reactivated.ok) {
377
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
378
+ process.exit(1);
379
+ }
380
+ state = reactivated.state;
381
+ console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
382
+ if (reactivated.migration_notice) {
383
+ console.log(chalk.yellow(reactivated.migration_notice));
384
+ }
385
+ if (reactivated.phantom_notice) {
386
+ console.log(chalk.yellow(reactivated.phantom_notice));
387
+ }
388
+
389
+ const completionReconciliation = reconcileRunCompletionBeforeDispatch(root, config, state, {
390
+ allow_active_turn_cleanup: true,
391
+ allow_standing_gate: true,
392
+ standing_gate_turn_id: opts.turn || null,
393
+ });
394
+ if (!completionReconciliation.ok && !completionReconciliation.state) {
395
+ console.log(chalk.red(`Failed to reconcile run completion before dispatch: ${completionReconciliation.error}`));
396
+ process.exit(1);
397
+ }
398
+ state = completionReconciliation.state || state;
399
+ if (completionReconciliation.completed) {
400
+ console.log(chalk.green(`Completed run before dispatch: ${state.run_id}`));
401
+ return;
402
+ }
403
+ markRunBlocked(root, {
404
+ category: 'needs_human',
405
+ blockedOn: state.blocked_on || 'human:unblock_completion_reconcile_failed',
406
+ recovery: {
407
+ typed_reason: 'needs_human',
408
+ owner: 'human',
409
+ recovery_action: 'agentxchain approve-completion',
410
+ turn_retained: true,
411
+ detail: 'Operator unblock resolved the escalation, but no run completion could be materialized from the current gate state.',
412
+ },
413
+ turnId: opts.turn || null,
414
+ notificationConfig: config,
415
+ });
416
+ console.log(chalk.red('Unblock did not materialize run completion; leaving the run blocked for manual recovery.'));
417
+ process.exit(1);
418
+ }
419
+
332
420
  if (state.status === 'blocked' && activeCount > 0 && !skipRetainedRedispatch) {
333
421
  let retainedTurn = null;
334
422
  if (opts.turn) {
@@ -790,11 +790,15 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
790
790
  }
791
791
 
792
792
  const activeGovernedState = loadProjectState(root, context.config);
793
+ const queuedIntent = session.current_run_id
794
+ ? findNextDispatchableIntent(root, { run_id: session.current_run_id })
795
+ : { ok: false };
793
796
  if (
794
797
  session.current_run_id
795
798
  && activeGovernedState?.status === 'active'
796
799
  && activeGovernedState.run_id === session.current_run_id
797
- && Object.keys(activeGovernedState.active_turns || {}).length > 0
800
+ && !contOpts.continueFrom
801
+ && !queuedIntent.ok
798
802
  ) {
799
803
  log('Continuing active governed run.');
800
804
  let execution;
@@ -855,7 +859,9 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
855
859
  }
856
860
 
857
861
  // Step 1: Check intake queue for pending work (BUG-34: scope to current run)
858
- const queued = findNextDispatchableIntent(root, { run_id: session.current_run_id });
862
+ const queued = queuedIntent.ok
863
+ ? queuedIntent
864
+ : findNextDispatchableIntent(root, { run_id: session.current_run_id });
859
865
  let targetIntentId = null;
860
866
  let visionObjective = null;
861
867
 
@@ -89,6 +89,7 @@ const TALK_PATH = 'TALK.md';
89
89
  const ACCEPTANCE_LOCK_PATH = '.agentxchain/locks/accept-turn.lock';
90
90
  const ACCEPTANCE_JOURNAL_DIR = '.agentxchain/transactions/accept';
91
91
  const INTAKE_INTENTS_DIR = '.agentxchain/intake/intents';
92
+ const CONTINUOUS_SESSION_PATH = '.agentxchain/continuous-session.json';
92
93
  const STALE_LOCK_TIMEOUT_MS = 30_000;
93
94
  const GOVERNED_SCHEMA_VERSION = '1.1';
94
95
 
@@ -1629,6 +1630,59 @@ function buildStandingPhaseTransitionSource(state, config, opts = {}) {
1629
1630
  };
1630
1631
  }
1631
1632
 
1633
+ function buildStandingRunCompletionSource(state, config, opts = {}) {
1634
+ const phase = state?.phase;
1635
+ const routing = phase ? config?.routing?.[phase] : null;
1636
+ const gateId = routing?.exit_gate || null;
1637
+ const nextPhase = getNextPhase(phase, config?.routing || {});
1638
+ if (!phase || !gateId || nextPhase) {
1639
+ return null;
1640
+ }
1641
+ if ((state?.phase_gate_status || {})[gateId] !== 'pending') {
1642
+ return null;
1643
+ }
1644
+ return {
1645
+ turn_id: opts?.turn_id || state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null,
1646
+ run_id: state?.run_id || null,
1647
+ role: null,
1648
+ assigned_role: null,
1649
+ phase,
1650
+ status: 'completed',
1651
+ run_completion_request: true,
1652
+ summary: `Synthetic ${gateId} completion source for operator-unblocked standing gate.`,
1653
+ verification: { status: 'pass' },
1654
+ };
1655
+ }
1656
+
1657
+ function syncPausedContinuousSessionAfterOutOfBandCompletion(root, completedState, opts = {}) {
1658
+ const sessionPath = join(root, CONTINUOUS_SESSION_PATH);
1659
+ if (!existsSync(sessionPath) || !completedState?.run_id) {
1660
+ return null;
1661
+ }
1662
+
1663
+ let session = null;
1664
+ try {
1665
+ session = JSON.parse(readFileSync(sessionPath, 'utf8'));
1666
+ } catch {
1667
+ return null;
1668
+ }
1669
+
1670
+ if (!session || session.status !== 'paused' || session.current_run_id !== completedState.run_id) {
1671
+ return session;
1672
+ }
1673
+
1674
+ const nextSession = {
1675
+ ...session,
1676
+ status: 'completed',
1677
+ runs_completed: Number.isInteger(session.runs_completed) ? session.runs_completed + 1 : 1,
1678
+ idle_cycles: 0,
1679
+ completed_at: completedState.completed_at || new Date().toISOString(),
1680
+ completed_via: opts.completed_via || 'out_of_band_run_completion',
1681
+ };
1682
+ safeWriteJson(sessionPath, nextSession);
1683
+ return nextSession;
1684
+ }
1685
+
1632
1686
  function getPhaseRoles(config, phase) {
1633
1687
  const routing = config?.routing?.[phase] || {};
1634
1688
  const roles = new Set();
@@ -2974,6 +3028,222 @@ export function reconcilePhaseAdvanceBeforeDispatch(root, config, state = null,
2974
3028
  };
2975
3029
  }
2976
3030
 
3031
+ export function reconcileRunCompletionBeforeDispatch(root, config, state = null, opts = {}) {
3032
+ const currentState = state && typeof state === 'object' ? state : readState(root);
3033
+ if (!currentState) {
3034
+ return { ok: false, error: 'No governed state.json found' };
3035
+ }
3036
+
3037
+ const activeTurnCount = getActiveTurnCount(currentState);
3038
+ const allowActiveTurnCleanup = opts?.allow_active_turn_cleanup === true;
3039
+ if (currentState.status !== 'active' || (activeTurnCount > 0 && !allowActiveTurnCleanup)) {
3040
+ return {
3041
+ ok: true,
3042
+ state: attachLegacyCurrentTurnAlias(currentState),
3043
+ completed: false,
3044
+ };
3045
+ }
3046
+
3047
+ const gateFailure = currentState.last_gate_failure;
3048
+ if (gateFailure && gateFailure.gate_type !== 'run_completion') {
3049
+ return {
3050
+ ok: true,
3051
+ state: attachLegacyCurrentTurnAlias(currentState),
3052
+ completed: false,
3053
+ };
3054
+ }
3055
+
3056
+ const historyEntries = readJsonlEntries(root, HISTORY_PATH);
3057
+ const requestedTurnId = gateFailure?.requested_by_turn
3058
+ || currentState.queued_run_completion?.requested_by_turn
3059
+ || currentState.last_completed_turn_id
3060
+ || null;
3061
+ let completionSource = findHistoryTurnRequest(historyEntries, requestedTurnId, 'run_completion');
3062
+ if (!completionSource?.run_completion_request && opts?.allow_standing_gate === true) {
3063
+ completionSource = buildStandingRunCompletionSource(currentState, config, {
3064
+ turn_id: opts?.standing_gate_turn_id || null,
3065
+ });
3066
+ }
3067
+ if (!completionSource?.run_completion_request) {
3068
+ return {
3069
+ ok: true,
3070
+ state: attachLegacyCurrentTurnAlias(currentState),
3071
+ completed: false,
3072
+ };
3073
+ }
3074
+
3075
+ const completionResult = evaluateRunCompletion({
3076
+ state: { ...currentState, history: historyEntries },
3077
+ config,
3078
+ acceptedTurn: completionSource,
3079
+ root,
3080
+ });
3081
+
3082
+ if (completionResult.action === 'awaiting_human_approval') {
3083
+ const approvalResult = evaluateApprovalPolicy({
3084
+ gateResult: completionResult,
3085
+ gateType: 'run_completion',
3086
+ state: { ...currentState, history: historyEntries },
3087
+ config,
3088
+ });
3089
+
3090
+ if (approvalResult.action === 'auto_approve') {
3091
+ const now = new Date().toISOString();
3092
+ const updatedState = {
3093
+ ...currentState,
3094
+ status: 'completed',
3095
+ completed_at: now,
3096
+ blocked_on: null,
3097
+ blocked_reason: null,
3098
+ last_gate_failure: null,
3099
+ pending_run_completion: null,
3100
+ queued_run_completion: null,
3101
+ queued_phase_transition: null,
3102
+ phase_gate_status: {
3103
+ ...(currentState.phase_gate_status || {}),
3104
+ [completionResult.gate_id || 'no_gate']: 'passed',
3105
+ },
3106
+ };
3107
+ writeState(root, updatedState);
3108
+ clearSlaReminders(root, 'pending_run_completion');
3109
+ appendJsonl(root, LEDGER_PATH, {
3110
+ type: 'approval_policy',
3111
+ gate_type: 'run_completion',
3112
+ action: 'auto_approve',
3113
+ matched_rule: approvalResult.matched_rule,
3114
+ reason: approvalResult.reason,
3115
+ gate_id: completionResult.gate_id || null,
3116
+ timestamp: now,
3117
+ });
3118
+ emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
3119
+ completed_at: updatedState.completed_at,
3120
+ completed_via: 'approval_policy_reconciled_before_dispatch',
3121
+ gate: completionResult.gate_id || null,
3122
+ requested_by_turn: completionSource.turn_id || null,
3123
+ }, null);
3124
+ emitRunEvent(root, 'gate_approved', {
3125
+ run_id: updatedState.run_id,
3126
+ phase: updatedState.phase,
3127
+ status: 'completed',
3128
+ turn: completionSource.turn_id ? { turn_id: completionSource.turn_id, role_id: completionSource.role || completionSource.assigned_role || null } : undefined,
3129
+ payload: {
3130
+ gate_type: 'run_completion',
3131
+ gate_id: completionResult.gate_id || null,
3132
+ requested_by_turn: completionSource.turn_id || null,
3133
+ trigger: 'auto_approved',
3134
+ },
3135
+ });
3136
+ emitRunEvent(root, 'run_completed', {
3137
+ run_id: updatedState.run_id,
3138
+ phase: updatedState.phase,
3139
+ status: 'completed',
3140
+ payload: { completed_at: updatedState.completed_at },
3141
+ });
3142
+ writeSessionCheckpoint(root, updatedState, 'run_completed');
3143
+ syncPausedContinuousSessionAfterOutOfBandCompletion(root, updatedState, {
3144
+ completed_via: 'approval_policy_reconciled_before_dispatch',
3145
+ });
3146
+ recordRunHistory(root, updatedState, config, 'completed');
3147
+ return {
3148
+ ok: true,
3149
+ state: attachLegacyCurrentTurnAlias(updatedState),
3150
+ completed: true,
3151
+ gate_id: completionResult.gate_id || null,
3152
+ completionResult,
3153
+ approval_policy: approvalResult,
3154
+ };
3155
+ }
3156
+
3157
+ const pausedState = {
3158
+ ...currentState,
3159
+ status: 'paused',
3160
+ blocked_on: `human_approval:${completionResult.gate_id}`,
3161
+ blocked_reason: null,
3162
+ pending_run_completion: {
3163
+ gate: completionResult.gate_id,
3164
+ requested_by_turn: completionSource.turn_id,
3165
+ requested_at: new Date().toISOString(),
3166
+ },
3167
+ queued_run_completion: null,
3168
+ queued_phase_transition: null,
3169
+ };
3170
+ writeState(root, pausedState);
3171
+ const approved = approveRunCompletion(root, config);
3172
+ return {
3173
+ ok: approved.ok,
3174
+ error: approved.error,
3175
+ state: approved.state || attachLegacyCurrentTurnAlias(readState(root)),
3176
+ completed: approved.ok,
3177
+ gate_id: completionResult.gate_id || null,
3178
+ completionResult,
3179
+ approval_policy: approvalResult,
3180
+ };
3181
+ }
3182
+
3183
+ if (completionResult.action !== 'complete') {
3184
+ return {
3185
+ ok: true,
3186
+ state: attachLegacyCurrentTurnAlias(currentState),
3187
+ completed: false,
3188
+ completionResult,
3189
+ };
3190
+ }
3191
+
3192
+ const now = new Date().toISOString();
3193
+ const updatedState = {
3194
+ ...currentState,
3195
+ status: 'completed',
3196
+ completed_at: now,
3197
+ blocked_on: null,
3198
+ blocked_reason: null,
3199
+ last_gate_failure: null,
3200
+ pending_run_completion: null,
3201
+ queued_run_completion: null,
3202
+ queued_phase_transition: null,
3203
+ phase_gate_status: {
3204
+ ...(currentState.phase_gate_status || {}),
3205
+ [completionResult.gate_id || 'no_gate']: 'passed',
3206
+ },
3207
+ };
3208
+ writeState(root, updatedState);
3209
+ emitPendingLifecycleNotification(root, config, updatedState, 'run_completed', {
3210
+ completed_at: updatedState.completed_at,
3211
+ completed_via: 'reconciled_before_dispatch',
3212
+ gate: completionResult.gate_id || null,
3213
+ requested_by_turn: completionSource.turn_id || null,
3214
+ }, null);
3215
+ emitRunEvent(root, 'gate_approved', {
3216
+ run_id: updatedState.run_id,
3217
+ phase: updatedState.phase,
3218
+ status: 'completed',
3219
+ turn: completionSource.turn_id ? { turn_id: completionSource.turn_id, role_id: completionSource.role || completionSource.assigned_role || null } : undefined,
3220
+ payload: {
3221
+ gate_type: 'run_completion',
3222
+ gate_id: completionResult.gate_id || null,
3223
+ requested_by_turn: completionSource.turn_id || null,
3224
+ trigger: 'reconciled_before_dispatch',
3225
+ },
3226
+ });
3227
+ emitRunEvent(root, 'run_completed', {
3228
+ run_id: updatedState.run_id,
3229
+ phase: updatedState.phase,
3230
+ status: 'completed',
3231
+ payload: { completed_at: updatedState.completed_at },
3232
+ });
3233
+ writeSessionCheckpoint(root, updatedState, 'run_completed');
3234
+ syncPausedContinuousSessionAfterOutOfBandCompletion(root, updatedState, {
3235
+ completed_via: 'reconciled_before_dispatch',
3236
+ });
3237
+ recordRunHistory(root, updatedState, config, 'completed');
3238
+ return {
3239
+ ok: true,
3240
+ state: attachLegacyCurrentTurnAlias(updatedState),
3241
+ completed: true,
3242
+ gate_id: completionResult.gate_id || null,
3243
+ completionResult,
3244
+ };
3245
+ }
3246
+
2977
3247
  // ── Core Operations ──────────────────────────────────────────────────────────
2978
3248
 
2979
3249
  /**
@@ -6352,6 +6622,9 @@ export function approveRunCompletion(root, config, opts = {}) {
6352
6622
 
6353
6623
  // Session checkpoint — non-fatal
6354
6624
  writeSessionCheckpoint(root, updatedState, 'run_completed');
6625
+ syncPausedContinuousSessionAfterOutOfBandCompletion(root, updatedState, {
6626
+ completed_via: 'approve_run_completion',
6627
+ });
6355
6628
 
6356
6629
  // Run history — non-fatal
6357
6630
  recordRunHistory(root, updatedState, config, 'completed');