agentxchain 2.116.0 → 2.118.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.
@@ -6,13 +6,24 @@ import {
6
6
  listSchedules,
7
7
  updateScheduleState,
8
8
  evaluateScheduleLaunchEligibility,
9
+ findContinuableScheduleRun,
9
10
  readDaemonState,
10
11
  writeDaemonState,
11
12
  updateDaemonHeartbeat,
12
13
  createDaemonState,
13
14
  evaluateDaemonStatus,
14
15
  } from '../lib/run-schedule.js';
16
+ import { consumePreemptionMarker } from '../lib/intake.js';
15
17
  import { executeGovernedRun } from './run.js';
18
+ import {
19
+ readContinuousSession,
20
+ writeContinuousSession,
21
+ advanceContinuousRunOnce,
22
+ resolveContinuousOptions,
23
+ } from '../lib/continuous-run.js';
24
+ import { resolveVisionPath } from '../lib/vision-reader.js';
25
+ import { existsSync } from 'node:fs';
26
+ import { randomUUID } from 'node:crypto';
16
27
 
17
28
  function loadScheduleContext() {
18
29
  const context = loadProjectContext();
@@ -86,7 +97,136 @@ function buildScheduleProvenance(entry) {
86
97
  };
87
98
  }
88
99
 
100
+ function buildScheduleExecutionResult(entryId, execution, fallbackState, action = 'ran') {
101
+ const state = execution.result?.state || fallbackState || null;
102
+ return {
103
+ id: entryId,
104
+ action,
105
+ run_id: state?.run_id || null,
106
+ stop_reason: execution.result?.stop_reason || null,
107
+ exit_code: execution.exitCode,
108
+ };
109
+ }
110
+
111
+ function recordScheduleExecution(context, entryId, execution, fallbackState, nowIso, action = 'ran') {
112
+ const state = execution.result?.state || fallbackState || null;
113
+ const runId = state?.run_id || null;
114
+ const startedAt = state?.created_at || nowIso;
115
+
116
+ updateScheduleState(context.root, context.config, entryId, (record) => ({
117
+ ...record,
118
+ last_started_at: startedAt,
119
+ last_finished_at: new Date().toISOString(),
120
+ last_run_id: runId,
121
+ last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
122
+ last_skip_at: null,
123
+ last_skip_reason: null,
124
+ }));
125
+
126
+ return buildScheduleExecutionResult(entryId, execution, fallbackState, action);
127
+ }
128
+
129
+ function consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, fallbackState, at) {
130
+ const scheduleResult = recordScheduleExecution(
131
+ context,
132
+ scheduleId,
133
+ execution,
134
+ fallbackState,
135
+ at || new Date().toISOString(),
136
+ 'preempted',
137
+ );
138
+ const consumed = consumePreemptionMarker(context.root, {
139
+ role: schedule.initial_role || undefined,
140
+ });
141
+
142
+ if (!consumed.ok) {
143
+ return {
144
+ ok: false,
145
+ exitCode: 1,
146
+ result: {
147
+ ...scheduleResult,
148
+ action: 'preemption_failed',
149
+ error: consumed.error,
150
+ injected_intent_id: execution.result?.preempted_by || null,
151
+ },
152
+ };
153
+ }
154
+
155
+ return {
156
+ ok: true,
157
+ exitCode: 0,
158
+ result: {
159
+ ...scheduleResult,
160
+ action: 'preempted',
161
+ injected_intent_id: consumed.intent_id,
162
+ injected_turn_id: consumed.turn_id,
163
+ injected_role: consumed.role,
164
+ },
165
+ };
166
+ }
167
+
168
+ async function continueActiveScheduledRun(context, opts = {}) {
169
+ const continuation = findContinuableScheduleRun(context.root, context.config, {
170
+ scheduleId: opts.schedule || null,
171
+ });
172
+ if (!continuation.ok) {
173
+ return { matched: false, reason: continuation.reason };
174
+ }
175
+
176
+ const { schedule_id: scheduleId, schedule, state } = continuation;
177
+
178
+ if (!opts.json) {
179
+ console.log(chalk.cyan(`Continuing active scheduled run: ${scheduleId}`));
180
+ }
181
+
182
+ const execution = await executeGovernedRun(context, {
183
+ maxTurns: schedule.max_turns,
184
+ autoApprove: schedule.auto_approve !== false,
185
+ report: true,
186
+ log: opts.json ? () => {} : console.log,
187
+ });
188
+
189
+ if (execution.result?.stop_reason === 'priority_preempted') {
190
+ const promoted = consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, state, opts.at);
191
+ return {
192
+ matched: true,
193
+ ok: promoted.ok,
194
+ exitCode: promoted.exitCode,
195
+ result: promoted.result,
196
+ };
197
+ }
198
+
199
+ const blocked = execution.result?.stop_reason === 'blocked';
200
+ const action = blocked && opts.tolerateBlockedRun ? 'blocked' : 'continued';
201
+ const result = recordScheduleExecution(context, scheduleId, execution, state, opts.at || new Date().toISOString(), action);
202
+
203
+ if (execution.exitCode !== 0 && !(opts.tolerateBlockedRun && blocked)) {
204
+ return {
205
+ matched: true,
206
+ ok: false,
207
+ exitCode: execution.exitCode,
208
+ result,
209
+ };
210
+ }
211
+
212
+ return {
213
+ matched: true,
214
+ ok: true,
215
+ exitCode: 0,
216
+ result,
217
+ };
218
+ }
219
+
89
220
  async function runDueSchedules(context, opts = {}) {
221
+ if (opts.continueActiveScheduleRuns) {
222
+ const continuation = await continueActiveScheduledRun(context, opts);
223
+ if (continuation.matched) {
224
+ return continuation.ok
225
+ ? { ok: true, exitCode: continuation.exitCode, results: [continuation.result] }
226
+ : { ok: false, exitCode: continuation.exitCode, results: [continuation.result], error: 'Scheduled run failed' };
227
+ }
228
+ }
229
+
90
230
  const resolved = resolveScheduleEntries(context, opts.schedule, opts.at);
91
231
  if (!resolved.ok) {
92
232
  return { ok: false, exitCode: 1, error: resolved.error, results: [] };
@@ -96,6 +236,10 @@ async function runDueSchedules(context, opts = {}) {
96
236
  const results = [];
97
237
 
98
238
  for (const entry of resolved.entries) {
239
+ // Skip entries handled by the continuous session manager
240
+ if (opts.excludeSchedule && entry.id === opts.excludeSchedule) {
241
+ continue;
242
+ }
99
243
  if (!entry.enabled) {
100
244
  results.push({ id: entry.id, action: 'disabled' });
101
245
  continue;
@@ -150,26 +294,29 @@ async function runDueSchedules(context, opts = {}) {
150
294
  continue;
151
295
  }
152
296
 
153
- const runId = execution.result?.state?.run_id || null;
154
- const startedAt = execution.result?.state?.created_at || nowIso;
155
- updateScheduleState(context.root, context.config, entry.id, (record) => ({
156
- ...record,
157
- last_started_at: startedAt,
158
- last_finished_at: new Date().toISOString(),
159
- last_run_id: runId,
160
- last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
161
- last_skip_at: null,
162
- last_skip_reason: null,
163
- }));
164
- results.push({
165
- id: entry.id,
166
- action: 'ran',
167
- run_id: runId,
168
- stop_reason: execution.result?.stop_reason || null,
169
- exit_code: execution.exitCode,
170
- });
297
+ if (execution.result?.stop_reason === 'priority_preempted') {
298
+ const promoted = consumeScheduledPriorityPreemption(context, entry.id, entry, execution, execution.result?.state || null, nowIso);
299
+ results.push(promoted.result);
300
+ if (!promoted.ok) {
301
+ return { ok: false, exitCode: promoted.exitCode, results };
302
+ }
303
+ continue;
304
+ }
305
+
306
+ const blocked = execution.result?.stop_reason === 'blocked';
307
+ results.push(recordScheduleExecution(
308
+ context,
309
+ entry.id,
310
+ execution,
311
+ execution.result?.state || null,
312
+ nowIso,
313
+ blocked && opts.tolerateBlockedRun ? 'blocked' : 'ran',
314
+ ));
171
315
 
172
316
  if (execution.exitCode !== 0) {
317
+ if (opts.tolerateBlockedRun && blocked) {
318
+ continue;
319
+ }
173
320
  return { ok: false, exitCode: execution.exitCode, results };
174
321
  }
175
322
  }
@@ -177,6 +324,159 @@ async function runDueSchedules(context, opts = {}) {
177
324
  return { ok: true, exitCode: 0, results };
178
325
  }
179
326
 
327
+ // ---------------------------------------------------------------------------
328
+ // Schedule-owned continuous session management
329
+ // ---------------------------------------------------------------------------
330
+
331
+ function isSessionTerminal(session) {
332
+ return ['completed', 'idle_exit', 'failed', 'stopped'].includes(session?.status);
333
+ }
334
+
335
+ export function selectContinuousScheduleEntry(root, config, opts = {}) {
336
+ const entries = listSchedules(root, config, { at: opts.at });
337
+ const continuousEntries = entries.filter((entry) => config?.schedules?.[entry.id]?.continuous?.enabled === true);
338
+
339
+ if (continuousEntries.length === 0) {
340
+ return null;
341
+ }
342
+
343
+ if (opts.scheduleId) {
344
+ const selected = continuousEntries.find((entry) => entry.id === opts.scheduleId);
345
+ return selected
346
+ ? { id: selected.id, schedule: config.schedules[selected.id], due: selected.due }
347
+ : null;
348
+ }
349
+
350
+ const activeSession = readContinuousSession(root);
351
+ if (activeSession && !isSessionTerminal(activeSession) && activeSession.owner_type === 'schedule') {
352
+ const ownerEntry = continuousEntries.find((entry) => entry.id === activeSession.owner_id);
353
+ if (!ownerEntry) {
354
+ return {
355
+ id: activeSession.owner_id,
356
+ error: `active continuous session owned by unknown schedule "${activeSession.owner_id}"`,
357
+ };
358
+ }
359
+ return { id: ownerEntry.id, schedule: config.schedules[ownerEntry.id], due: ownerEntry.due };
360
+ }
361
+
362
+ const dueEntry = continuousEntries.find((entry) => entry.due);
363
+ if (!dueEntry) {
364
+ return null;
365
+ }
366
+
367
+ return { id: dueEntry.id, schedule: config.schedules[dueEntry.id], due: dueEntry.due };
368
+ }
369
+
370
+ function createScheduleOwnedSession(schedule, scheduleId) {
371
+ return {
372
+ session_id: `cont-${randomUUID().slice(0, 8)}`,
373
+ started_at: new Date().toISOString(),
374
+ vision_path: schedule.continuous.vision_path,
375
+ runs_completed: 0,
376
+ max_runs: schedule.continuous.max_runs,
377
+ idle_cycles: 0,
378
+ max_idle_cycles: schedule.continuous.max_idle_cycles,
379
+ current_run_id: null,
380
+ current_vision_objective: null,
381
+ status: 'running',
382
+ owner_type: 'schedule',
383
+ owner_id: scheduleId,
384
+ };
385
+ }
386
+
387
+ async function advanceScheduleContinuousSession(context, entry, opts = {}) {
388
+ const { root, config } = context;
389
+ const scheduleId = entry.id;
390
+ const schedule = entry.schedule;
391
+ const contConfig = schedule.continuous;
392
+ const log = opts.json ? () => {} : console.log;
393
+
394
+ // Read existing session
395
+ let session = readContinuousSession(root);
396
+
397
+ // If there's an active session owned by a different schedule, fail closed
398
+ if (session && !isSessionTerminal(session) && session.owner_type === 'schedule' && session.owner_id !== scheduleId) {
399
+ return {
400
+ ok: false,
401
+ action: 'skipped',
402
+ reason: `continuous session owned by schedule "${session.owner_id}"`,
403
+ };
404
+ }
405
+
406
+ // Determine if we need a new session
407
+ const needsNewSession = !session || isSessionTerminal(session) || session.owner_id !== scheduleId;
408
+
409
+ if (needsNewSession) {
410
+ // Only start a new session if the schedule is due
411
+ if (!opts.isDue) {
412
+ return { ok: true, action: 'not_due', reason: 'waiting_interval' };
413
+ }
414
+
415
+ // Check launch eligibility
416
+ const eligibility = evaluateScheduleLaunchEligibility(root, config);
417
+ if (!eligibility.ok) {
418
+ return { ok: false, action: 'skipped', reason: eligibility.reason };
419
+ }
420
+
421
+ // Validate vision path
422
+ const absVision = resolveVisionPath(root, contConfig.vision_path);
423
+ if (!existsSync(absVision)) {
424
+ return { ok: false, action: 'failed', reason: `VISION.md not found at ${absVision}` };
425
+ }
426
+
427
+ session = createScheduleOwnedSession(schedule, scheduleId);
428
+ writeContinuousSession(root, session);
429
+ log(chalk.cyan(`Started schedule-owned continuous session: ${session.session_id} (schedule: ${scheduleId})`));
430
+
431
+ // Record schedule start
432
+ updateScheduleState(root, config, scheduleId, (record) => ({
433
+ ...record,
434
+ last_started_at: new Date().toISOString(),
435
+ last_status: 'continuous_running',
436
+ last_continuous_session_id: session.session_id,
437
+ }));
438
+ }
439
+
440
+ // Build contOpts from schedule continuous config
441
+ const contOpts = {
442
+ visionPath: contConfig.vision_path,
443
+ maxRuns: contConfig.max_runs,
444
+ maxIdleCycles: contConfig.max_idle_cycles,
445
+ triageApproval: contConfig.triage_approval,
446
+ };
447
+
448
+ // Advance one step
449
+ const step = await advanceContinuousRunOnce(context, session, contOpts, executeGovernedRun, log);
450
+
451
+ // Update schedule state based on step result
452
+ const statusMap = {
453
+ completed: 'continuous_completed',
454
+ idle_exit: 'continuous_idle_exit',
455
+ failed: 'continuous_failed',
456
+ blocked: 'continuous_blocked',
457
+ running: 'continuous_running',
458
+ };
459
+ const schedStatus = statusMap[step.status] || 'continuous_running';
460
+
461
+ updateScheduleState(root, config, scheduleId, (record) => ({
462
+ ...record,
463
+ last_finished_at: new Date().toISOString(),
464
+ last_status: schedStatus,
465
+ last_run_id: step.run_id || record.last_run_id,
466
+ last_continuous_session_id: session.session_id,
467
+ }));
468
+
469
+ return {
470
+ ok: step.ok,
471
+ action: step.action,
472
+ status: step.status,
473
+ session_id: session.session_id,
474
+ run_id: step.run_id || null,
475
+ intent_id: step.intent_id || null,
476
+ runs_completed: session.runs_completed,
477
+ };
478
+ }
479
+
180
480
  export async function scheduleListCommand(opts) {
181
481
  const context = loadScheduleContext();
182
482
  if (!context) return;
@@ -214,6 +514,14 @@ export async function scheduleRunDueCommand(opts) {
214
514
  for (const entry of result.results) {
215
515
  if (entry.action === 'ran') {
216
516
  console.log(chalk.green(`Schedule ran: ${entry.id} (${entry.run_id || 'no run id'})`));
517
+ } else if (entry.action === 'continued') {
518
+ console.log(chalk.green(`Schedule continued: ${entry.id} (${entry.run_id || 'no run id'})`));
519
+ } else if (entry.action === 'preempted') {
520
+ console.log(chalk.yellow(`Schedule preempted by injected priority: ${entry.id} (${entry.injected_intent_id || 'unknown intent'})`));
521
+ } else if (entry.action === 'preemption_failed') {
522
+ console.log(chalk.red(`Schedule preemption failed: ${entry.id} (${entry.error || 'unknown error'})`));
523
+ } else if (entry.action === 'blocked') {
524
+ console.log(chalk.yellow(`Schedule waiting on unblock: ${entry.id}`));
217
525
  } else if (entry.action === 'skipped') {
218
526
  console.log(chalk.yellow(`Schedule skipped: ${entry.id} (${entry.reason})`));
219
527
  } else if (entry.action === 'not_due') {
@@ -338,7 +646,66 @@ export async function scheduleDaemonCommand(opts) {
338
646
  while (true) {
339
647
  cycle += 1;
340
648
  daemonState.last_cycle_started_at = new Date().toISOString();
341
- const result = await runDueSchedules(context, opts);
649
+
650
+ // Check for continuous schedule entries first
651
+ const contEntry = selectContinuousScheduleEntry(context.root, context.config, {
652
+ scheduleId: opts.schedule || null,
653
+ at: opts.at,
654
+ });
655
+ let result;
656
+
657
+ if (contEntry?.error) {
658
+ result = {
659
+ ok: false,
660
+ exitCode: 1,
661
+ results: [{
662
+ id: contEntry.id,
663
+ action: 'failed',
664
+ continuous: true,
665
+ reason: contEntry.error,
666
+ }],
667
+ };
668
+ } else if (contEntry) {
669
+ const isDue = contEntry.due ?? false;
670
+
671
+ const contResult = await advanceScheduleContinuousSession(context, contEntry, {
672
+ isDue,
673
+ json: opts.json,
674
+ at: opts.at,
675
+ });
676
+
677
+ // Run non-continuous schedules normally alongside
678
+ const nonContResult = await runDueSchedules(context, {
679
+ ...opts,
680
+ continueActiveScheduleRuns: true,
681
+ tolerateBlockedRun: true,
682
+ excludeSchedule: contEntry.id,
683
+ });
684
+
685
+ // Merge results
686
+ const contResultEntry = {
687
+ id: contEntry.id,
688
+ action: contResult.action,
689
+ continuous: true,
690
+ session_id: contResult.session_id || null,
691
+ status: contResult.status || null,
692
+ run_id: contResult.run_id || null,
693
+ runs_completed: contResult.runs_completed ?? null,
694
+ };
695
+ if (contResult.reason) contResultEntry.reason = contResult.reason;
696
+
697
+ result = {
698
+ ok: contResult.ok !== false && nonContResult.ok,
699
+ exitCode: (contResult.ok === false || !nonContResult.ok) ? 1 : 0,
700
+ results: [contResultEntry, ...nonContResult.results],
701
+ };
702
+ } else {
703
+ result = await runDueSchedules(context, {
704
+ ...opts,
705
+ continueActiveScheduleRuns: true,
706
+ tolerateBlockedRun: true,
707
+ });
708
+ }
342
709
 
343
710
  updateDaemonHeartbeat(context.root, daemonState, result);
344
711
 
@@ -19,7 +19,10 @@ import { summarizeRunProvenance } from '../lib/run-provenance.js';
19
19
  import { readRecentRunEventSummary } from '../lib/recent-event-summary.js';
20
20
  import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.js';
21
21
  import { summarizeLatestGateActionAttempt } from '../lib/gate-actions.js';
22
+ import { findCurrentHumanEscalation } from '../lib/human-escalations.js';
22
23
  import { getDashboardPid, getDashboardSession } from './dashboard.js';
24
+ import { readPreemptionMarker } from '../lib/intake.js';
25
+ import { readContinuousSession } from '../lib/continuous-run.js';
23
26
 
24
27
  export async function statusCommand(opts) {
25
28
  const context = loadStatusContext();
@@ -127,6 +130,9 @@ function renderGovernedStatus(context, opts) {
127
130
  const repoDecisionSummary = summarizeRepoDecisions(readRepoDecisions(root), config);
128
131
 
129
132
  const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
133
+ const humanEscalation = findCurrentHumanEscalation(root, state);
134
+ const preemptionMarker = readPreemptionMarker(root);
135
+ const continuousSession = readContinuousSession(root);
130
136
  const gateActionAttempt = state?.pending_phase_transition
131
137
  ? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
132
138
  : state?.pending_run_completion
@@ -162,6 +168,9 @@ function renderGovernedStatus(context, opts) {
162
168
  next_actions: nextActions,
163
169
  connector_health: connectorHealth,
164
170
  recent_event_summary: recentEventSummary,
171
+ human_escalation: humanEscalation,
172
+ preemption_marker: preemptionMarker,
173
+ continuous_session: continuousSession,
165
174
  gate_action_attempt: gateActionAttempt,
166
175
  workflow_kit_artifacts: workflowKitArtifacts,
167
176
  dashboard_session: dashboardSessionObj,
@@ -174,6 +183,42 @@ function renderGovernedStatus(context, opts) {
174
183
  console.log(chalk.dim(' ' + '─'.repeat(44)));
175
184
  console.log('');
176
185
 
186
+ // Priority injection banner — above all other status
187
+ if (preemptionMarker) {
188
+ console.log(chalk.red.bold(' ⚡ Priority injection pending'));
189
+ console.log(chalk.dim(` Intent: ${preemptionMarker.intent_id}`));
190
+ console.log(` Priority: ${chalk.red.bold(preemptionMarker.priority)}`);
191
+ if (preemptionMarker.description) {
192
+ console.log(chalk.dim(` Description: ${preemptionMarker.description}`));
193
+ }
194
+ if (preemptionMarker.injected_at) {
195
+ console.log(chalk.dim(` Injected at: ${preemptionMarker.injected_at}`));
196
+ }
197
+ console.log(chalk.dim(' Effect: Will preempt current workstream after this turn completes'));
198
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
199
+ console.log('');
200
+ }
201
+
202
+ // Continuous session banner
203
+ if (continuousSession) {
204
+ console.log(chalk.cyan.bold(' 🔄 Continuous Vision-Driven Session'));
205
+ console.log(chalk.dim(` Session: ${continuousSession.session_id}`));
206
+ console.log(chalk.dim(` Vision: ${continuousSession.vision_path}`));
207
+ console.log(` Status: ${chalk.cyan(continuousSession.status || 'unknown')}`);
208
+ console.log(` Runs: ${continuousSession.runs_completed || 0}/${continuousSession.max_runs || '?'}`);
209
+ if (continuousSession.owner_type === 'schedule') {
210
+ console.log(chalk.dim(` Owner: schedule:${continuousSession.owner_id}`));
211
+ }
212
+ if (continuousSession.current_vision_objective) {
213
+ console.log(` Objective: ${chalk.yellow(continuousSession.current_vision_objective)}`);
214
+ }
215
+ if (continuousSession.idle_cycles > 0) {
216
+ console.log(chalk.dim(` Idle cycles: ${continuousSession.idle_cycles}/${continuousSession.max_idle_cycles}`));
217
+ }
218
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
219
+ console.log('');
220
+ }
221
+
177
222
  console.log(` ${chalk.dim('Project:')} ${config.project.name}`);
178
223
  if (config.project.goal) {
179
224
  console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
@@ -325,6 +370,16 @@ function renderGovernedStatus(context, opts) {
325
370
  }
326
371
  }
327
372
 
373
+ if (humanEscalation) {
374
+ console.log('');
375
+ console.log(` ${chalk.dim('Human task:')} ${chalk.yellow(humanEscalation.escalation_id)}${humanEscalation.service ? ` (${humanEscalation.service})` : ''}`);
376
+ console.log(` ${chalk.dim('Type:')} ${humanEscalation.type}`);
377
+ console.log(` ${chalk.dim('Unblock:')} ${chalk.cyan(humanEscalation.resolution_command)}`);
378
+ if (humanEscalation.action) {
379
+ console.log(` ${chalk.dim('Task:')} ${humanEscalation.action}`);
380
+ }
381
+ }
382
+
328
383
  if (runtimeGuidance.length > 0) {
329
384
  console.log('');
330
385
  console.log(` ${chalk.dim('Runtime guidance:')}`);
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { findCurrentHumanEscalation, getOpenHumanEscalation } from '../lib/human-escalations.js';
4
+ import { resumeCommand } from './resume.js';
5
+
6
+ export async function unblockCommand(escalationId) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ const { root, config } = context;
14
+
15
+ if (config.protocol_mode !== 'governed') {
16
+ console.log(chalk.red('The unblock command is only available for governed projects.'));
17
+ process.exit(1);
18
+ }
19
+
20
+ if (!escalationId || !String(escalationId).trim()) {
21
+ console.log(chalk.red('An escalation id is required. Example: agentxchain unblock hesc_1234'));
22
+ process.exit(1);
23
+ }
24
+
25
+ const state = loadProjectState(root, config);
26
+ if (!state) {
27
+ console.log(chalk.red('No governed state.json found. Run `agentxchain init --governed` first.'));
28
+ process.exit(1);
29
+ }
30
+
31
+ if (state.status !== 'blocked') {
32
+ console.log(chalk.red(`Cannot unblock run: status is "${state.status}", expected "blocked".`));
33
+ process.exit(1);
34
+ }
35
+
36
+ const requested = getOpenHumanEscalation(root, escalationId);
37
+ if (!requested) {
38
+ console.log(chalk.red(`No open human escalation found for ${escalationId}.`));
39
+ process.exit(1);
40
+ }
41
+
42
+ const current = findCurrentHumanEscalation(root, state);
43
+ if (!current) {
44
+ console.log(chalk.red('The current blocked run does not have a linked human escalation record.'));
45
+ process.exit(1);
46
+ }
47
+
48
+ if (current.escalation_id !== requested.escalation_id) {
49
+ console.log(chalk.red(`Escalation ${escalationId} is not the current blocker for this run.`));
50
+ console.log(chalk.dim(`Current blocker: ${current.escalation_id}`));
51
+ process.exit(1);
52
+ }
53
+
54
+ console.log('');
55
+ console.log(chalk.green(` Unblocking ${requested.escalation_id}`));
56
+ console.log(chalk.dim(` Type: ${requested.type}${requested.service ? ` (${requested.service})` : ''}`));
57
+ if (requested.detail) {
58
+ console.log(chalk.dim(` Detail: ${requested.detail}`));
59
+ }
60
+ console.log(chalk.dim(' Continuing governed execution...'));
61
+ console.log('');
62
+
63
+ await resumeCommand({
64
+ _via: 'operator_unblock',
65
+ turn: requested.turn_id || undefined,
66
+ });
67
+ }