agentxchain 2.154.3 → 2.154.5

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.3",
3
+ "version": "2.154.5",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,33 @@ import { summarizeRunProvenance } from '../lib/run-provenance.js';
44
44
  import { consumeNextApprovedIntent } from '../lib/intake.js';
45
45
  import { reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
46
46
 
47
+ function hasStandingPendingExitGate(state, config) {
48
+ const phase = state?.phase;
49
+ const gateId = phase ? config?.routing?.[phase]?.exit_gate : null;
50
+ return Boolean(gateId && (state?.phase_gate_status || {})[gateId] === 'pending');
51
+ }
52
+
53
+ function latestCompletedTurnWantsPhaseContinuation(root, state, config) {
54
+ const turnId = state?.last_completed_turn_id || state?.blocked_reason?.turn_id || null;
55
+ if (!turnId) return false;
56
+ const historyPath = join(root, config?.files?.history || '.agentxchain/history.jsonl');
57
+ if (!existsSync(historyPath)) return false;
58
+ const lines = readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
59
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
60
+ let entry = null;
61
+ try {
62
+ entry = JSON.parse(lines[index]);
63
+ } catch {
64
+ continue;
65
+ }
66
+ if (entry?.turn_id !== turnId) continue;
67
+ if (entry.phase_transition_request) return true;
68
+ const proposed = typeof entry.proposed_next_role === 'string' ? entry.proposed_next_role.trim() : '';
69
+ return Boolean(proposed && proposed !== 'human');
70
+ }
71
+ return false;
72
+ }
73
+
47
74
  export async function resumeCommand(opts) {
48
75
  const context = loadProjectContext();
49
76
  if (!context) {
@@ -143,7 +170,23 @@ export async function resumeCommand(opts) {
143
170
  // patched defensively) once the schema citation + migration citation are
144
171
  // documented in code and the coverage matrix.
145
172
 
146
- if (state.status === 'blocked' && activeCount > 0 && resumeVia === 'operator_unblock') {
173
+ // BUG-52 third variant (Turn 203/204): operator_unblock must run the
174
+ // standing-gate reconcile path regardless of `activeCount`, but only when the
175
+ // current phase actually has a standing pending exit gate and the blocked
176
+ // turn was trying to continue into a non-human phase role. The tester's
177
+ // v2.151.0 lights-out repro on `tusq.dev` left `active_turns: {}` with
178
+ // `phase_gate_status.planning_signoff: "pending"`, so the old
179
+ // `activeCount > 0` guard skipped the only path that could synthesize the
180
+ // missing transition source. Non-gate human escalations (OAuth, external
181
+ // decisions, schedule recovery with `proposed_next_role: "human"`) must keep
182
+ // the normal unblock/resume behavior instead of being forced through a
183
+ // phase-transition materialization check.
184
+ if (
185
+ state.status === 'blocked'
186
+ && resumeVia === 'operator_unblock'
187
+ && hasStandingPendingExitGate(state, config)
188
+ && latestCompletedTurnWantsPhaseContinuation(root, state, config)
189
+ ) {
147
190
  const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
148
191
  if (!reactivated.ok) {
149
192
  console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));