agentxchain 2.116.0 → 2.117.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/README.md CHANGED
@@ -202,7 +202,7 @@ Partial coordinator artifacts are first-class here too: `audit` and `report` kee
202
202
  | `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
203
203
  | `intake record\|triage\|approve\|plan\|start\|scan\|resolve` | Continuous-delivery intake: turn delivery signals into governed work items |
204
204
  | `intake handoff` | Bridge a planned intake intent to a coordinator workstream for multi-repo execution |
205
- | `schedule list\|run-due\|daemon\|status` | Run repo-local lights-out scheduling: inspect schedules, execute due runs, poll in a local daemon loop, or check daemon heartbeat |
205
+ | `schedule list\|run-due\|daemon\|status` | Run repo-local lights-out scheduling: inspect schedules, execute due runs, poll in a local daemon loop, continue explicitly unblocked schedule-owned runs, or check daemon heartbeat |
206
206
  | `plugin install\|list\|remove` | Install, inspect, or remove governed hook plugins under `.agentxchain/plugins/` |
207
207
  | `plugin list-available` | List bundled built-in plugins installable by short name |
208
208
  | `export [--output <path>]` | Export the portable raw governed/coordinator artifact for continuity or offline review |
@@ -67,6 +67,8 @@ import { rebindCommand } from '../src/commands/rebind.js';
67
67
  import { branchCommand } from '../src/commands/branch.js';
68
68
  import { migrateCommand } from '../src/commands/migrate.js';
69
69
  import { resumeCommand } from '../src/commands/resume.js';
70
+ import { unblockCommand } from '../src/commands/unblock.js';
71
+ import { injectCommand } from '../src/commands/inject.js';
70
72
  import { escalateCommand } from '../src/commands/escalate.js';
71
73
  import { acceptTurnCommand } from '../src/commands/accept-turn.js';
72
74
  import { rejectTurnCommand } from '../src/commands/reject-turn.js';
@@ -600,6 +602,23 @@ program
600
602
  .option('--turn <id>', 'Target a specific retained turn when multiple exist')
601
603
  .action(resumeCommand);
602
604
 
605
+ program
606
+ .command('unblock <escalation-id>')
607
+ .description('Resolve the current human escalation record and continue the governed run')
608
+ .action(unblockCommand);
609
+
610
+ program
611
+ .command('inject <description>')
612
+ .description('Inject a priority work item into the intake queue (composed record + triage + approve)')
613
+ .option('--priority <level>', 'Priority level (p0, p1, p2, p3)', 'p0')
614
+ .option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app, enterprise-app)', 'generic')
615
+ .option('--charter <text>', 'Delivery charter (defaults to description)')
616
+ .option('--acceptance <text>', 'Comma-separated acceptance criteria')
617
+ .option('--approver <name>', 'Approver identity', 'human')
618
+ .option('--no-approve', 'Stop at triaged state instead of auto-approving')
619
+ .option('-j, --json', 'Output as JSON')
620
+ .action(injectCommand);
621
+
603
622
  program
604
623
  .command('escalate')
605
624
  .description('Raise an operator escalation and block the governed run intentionally')
@@ -652,6 +671,11 @@ program
652
671
  .option('--chain-on <statuses>', 'Comma-separated terminal statuses that trigger chaining (default: completed)')
653
672
  .option('--chain-cooldown <seconds>', 'Seconds to wait between chained runs (default: 5)', parseInt)
654
673
  .option('--mission <mission_id>', 'Bind chained runs to a mission (use "latest" for most recent mission)')
674
+ .option('--continuous', 'Enable continuous vision-driven loop: derive work from VISION.md and run until satisfied')
675
+ .option('--vision <path>', 'Path to VISION.md (project-relative or absolute, default: .planning/VISION.md)')
676
+ .option('--max-runs <n>', 'Maximum consecutive governed runs in continuous mode (default: 100)', parseInt)
677
+ .option('--poll-seconds <n>', 'Seconds between idle-detection cycles in continuous mode (default: 30)', parseInt)
678
+ .option('--max-idle-cycles <n>', 'Stop after N consecutive idle cycles with no derivable work (default: 3)', parseInt)
655
679
  .action(runCommand);
656
680
 
657
681
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.116.0",
3
+ "version": "2.117.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,7 +73,12 @@ function printEvent(evt) {
73
73
  const gateFailedDetail = evt.event_type === 'gate_failed' && evt.payload?.from_phase
74
74
  ? ` ${evt.payload.from_phase} → ${evt.payload.to_phase || '?'}${evt.payload.reasons?.length ? ` — ${evt.payload.reasons[0]}` : ''}${evt.payload.gate_id ? ` (${evt.payload.gate_id})` : ''}`
75
75
  : '';
76
- console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${conflictDetail}${rejectionDetail}${phaseTransitionDetail}${gateFailedDetail}`);
76
+ const humanEscalationDetail = evt.event_type === 'human_escalation_raised' && evt.payload?.escalation_id
77
+ ? ` ${evt.payload.escalation_id} [${evt.payload.type || '?'}]${evt.payload.service ? ` (${evt.payload.service})` : ''}`
78
+ : evt.event_type === 'human_escalation_resolved' && evt.payload?.escalation_id
79
+ ? ` ${evt.payload.escalation_id} via ${evt.payload.resolved_via || '?'}`
80
+ : '';
81
+ console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${conflictDetail}${rejectionDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}`);
77
82
  }
78
83
 
79
84
  function formatConflictDetail(evt) {
@@ -115,6 +120,8 @@ function colorEventType(type) {
115
120
  phase_entered: chalk.magenta,
116
121
  escalation_raised: chalk.red.bold,
117
122
  escalation_resolved: chalk.green,
123
+ human_escalation_raised: chalk.red.bold,
124
+ human_escalation_resolved: chalk.green,
118
125
  gate_pending: chalk.yellow,
119
126
  gate_approved: chalk.green,
120
127
  gate_failed: chalk.red,
@@ -0,0 +1,81 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext } from '../lib/config.js';
3
+ import { injectIntent } from '../lib/intake.js';
4
+
5
+ export async function injectCommand(description, opts) {
6
+ const context = loadProjectContext();
7
+ if (!context) {
8
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const { root, config } = context;
13
+
14
+ if (config.protocol_mode !== 'governed') {
15
+ console.log(chalk.red('The inject command is only available for governed projects.'));
16
+ process.exit(1);
17
+ }
18
+
19
+ if (!description || !String(description).trim()) {
20
+ console.log(chalk.red('A description is required. Example: agentxchain inject "Fix the sidebar ordering"'));
21
+ process.exit(1);
22
+ }
23
+
24
+ const result = injectIntent(root, String(description).trim(), {
25
+ priority: opts.priority,
26
+ template: opts.template,
27
+ charter: opts.charter,
28
+ acceptance: opts.acceptance,
29
+ approver: opts.approver,
30
+ noApprove: opts.approve === false,
31
+ });
32
+
33
+ if (!result.ok) {
34
+ if (opts.json) {
35
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
36
+ } else {
37
+ console.log(chalk.red(result.error));
38
+ }
39
+ process.exit(result.exitCode || 1);
40
+ }
41
+
42
+ if (opts.json) {
43
+ console.log(JSON.stringify({
44
+ ok: true,
45
+ intent_id: result.intent.intent_id,
46
+ event_id: result.event.event_id,
47
+ status: result.intent.status,
48
+ priority: result.intent.priority,
49
+ deduplicated: result.deduplicated,
50
+ preemption_marker: result.preemption_marker,
51
+ }, null, 2));
52
+ return;
53
+ }
54
+
55
+ console.log('');
56
+ if (result.deduplicated) {
57
+ console.log(chalk.yellow(' ⚠ Duplicate injection — existing intent returned'));
58
+ console.log(chalk.dim(` Intent: ${result.intent?.intent_id || 'unknown'}`));
59
+ console.log(chalk.dim(` Status: ${result.intent?.status || 'unknown'}`));
60
+ console.log('');
61
+ return;
62
+ }
63
+
64
+ const priority = result.intent.priority || 'p0';
65
+ const priorityColor = priority === 'p0' ? chalk.red.bold : priority === 'p1' ? chalk.yellow.bold : chalk.dim;
66
+
67
+ console.log(chalk.green.bold(' Injected'));
68
+ console.log(chalk.dim(` Intent: ${result.intent.intent_id}`));
69
+ console.log(` Priority: ${priorityColor(priority)}`);
70
+ console.log(chalk.dim(` Status: ${result.intent.status}`));
71
+ console.log(chalk.dim(` Charter: ${result.intent.charter || description}`));
72
+
73
+ if (result.preemption_marker) {
74
+ console.log('');
75
+ console.log(chalk.red.bold(' ⚡ Preemption marker written'));
76
+ console.log(chalk.dim(' The current run will yield after the active turn completes.'));
77
+ console.log(chalk.dim(' The scheduler/continuous loop will pick up this intent next.'));
78
+ }
79
+
80
+ console.log('');
81
+ }
@@ -77,6 +77,8 @@ export async function resumeCommand(opts) {
77
77
  // §47: active + turns present → reject (resume assigns new turns, not re-dispatches)
78
78
  const activeCount = getActiveTurnCount(state);
79
79
  const activeTurns = getActiveTurns(state);
80
+ const resumeVia = opts?._via || 'resume';
81
+ const turnResumeVia = opts?._via || 'resume --turn';
80
82
 
81
83
  if (state.status === 'active' && activeCount > 0) {
82
84
  if (activeCount === 1) {
@@ -135,7 +137,7 @@ export async function resumeCommand(opts) {
135
137
  console.log(` Attempt: ${retainedTurn.attempt}`);
136
138
  console.log('');
137
139
 
138
- const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
140
+ const reactivated = reactivateGovernedRun(root, state, { via: turnResumeVia, notificationConfig: config });
139
141
  if (!reactivated.ok) {
140
142
  console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
141
143
  process.exit(1);
@@ -195,7 +197,7 @@ export async function resumeCommand(opts) {
195
197
  console.log(` Attempt: ${retainedTurn.attempt}`);
196
198
  console.log('');
197
199
 
198
- const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
200
+ const reactivated = reactivateGovernedRun(root, state, { via: turnResumeVia, notificationConfig: config });
199
201
  if (!reactivated.ok) {
200
202
  console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
201
203
  process.exit(1);
@@ -234,7 +236,7 @@ export async function resumeCommand(opts) {
234
236
 
235
237
  // §47: paused + run_id exists → resume same run
236
238
  if (state.status === 'blocked' && state.run_id) {
237
- const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
239
+ const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
238
240
  if (!reactivated.ok) {
239
241
  console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
240
242
  process.exit(1);
@@ -245,7 +247,7 @@ export async function resumeCommand(opts) {
245
247
 
246
248
  // §47: paused + run_id exists → resume same run
247
249
  if (state.status === 'paused' && state.run_id) {
248
- const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
250
+ const reactivated = reactivateGovernedRun(root, state, { via: resumeVia, notificationConfig: config });
249
251
  if (!reactivated.ok) {
250
252
  console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
251
253
  process.exit(1);
@@ -45,6 +45,7 @@ import {
45
45
  getTurnStagingResultPath,
46
46
  } from '../lib/turn-paths.js';
47
47
  import { resolveChainOptions, executeChainedRun } from '../lib/run-chain.js';
48
+ import { resolveContinuousOptions, executeContinuousRun } from '../lib/continuous-run.js';
48
49
 
49
50
  export async function runCommand(opts) {
50
51
  const context = loadProjectContext();
@@ -53,6 +54,18 @@ export async function runCommand(opts) {
53
54
  process.exit(1);
54
55
  }
55
56
 
57
+ // Continuous vision-driven mode
58
+ const contOpts = resolveContinuousOptions(opts, context.config);
59
+ if (contOpts.enabled) {
60
+ console.log(chalk.cyan.bold('agentxchain run --continuous'));
61
+ console.log(chalk.dim(` Vision: ${contOpts.visionPath}`));
62
+ console.log(chalk.dim(` Max runs: ${contOpts.maxRuns}, Poll: ${contOpts.pollSeconds}s, Idle limit: ${contOpts.maxIdleCycles}`));
63
+ console.log(chalk.dim(` Triage approval: ${contOpts.triageApproval}`));
64
+ console.log('');
65
+ const { exitCode } = await executeContinuousRun(context, contOpts, executeGovernedRun);
66
+ process.exit(exitCode);
67
+ }
68
+
56
69
  const chainOpts = resolveChainOptions(opts, context.config);
57
70
  if (chainOpts.enabled) {
58
71
  console.log(chalk.cyan.bold('agentxchain run --chain'));
@@ -6,12 +6,14 @@ 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';
16
18
 
17
19
  function loadScheduleContext() {
@@ -86,7 +88,136 @@ function buildScheduleProvenance(entry) {
86
88
  };
87
89
  }
88
90
 
91
+ function buildScheduleExecutionResult(entryId, execution, fallbackState, action = 'ran') {
92
+ const state = execution.result?.state || fallbackState || null;
93
+ return {
94
+ id: entryId,
95
+ action,
96
+ run_id: state?.run_id || null,
97
+ stop_reason: execution.result?.stop_reason || null,
98
+ exit_code: execution.exitCode,
99
+ };
100
+ }
101
+
102
+ function recordScheduleExecution(context, entryId, execution, fallbackState, nowIso, action = 'ran') {
103
+ const state = execution.result?.state || fallbackState || null;
104
+ const runId = state?.run_id || null;
105
+ const startedAt = state?.created_at || nowIso;
106
+
107
+ updateScheduleState(context.root, context.config, entryId, (record) => ({
108
+ ...record,
109
+ last_started_at: startedAt,
110
+ last_finished_at: new Date().toISOString(),
111
+ last_run_id: runId,
112
+ last_status: execution.result?.stop_reason || (execution.exitCode === 0 ? 'completed' : 'launch_failed'),
113
+ last_skip_at: null,
114
+ last_skip_reason: null,
115
+ }));
116
+
117
+ return buildScheduleExecutionResult(entryId, execution, fallbackState, action);
118
+ }
119
+
120
+ function consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, fallbackState, at) {
121
+ const scheduleResult = recordScheduleExecution(
122
+ context,
123
+ scheduleId,
124
+ execution,
125
+ fallbackState,
126
+ at || new Date().toISOString(),
127
+ 'preempted',
128
+ );
129
+ const consumed = consumePreemptionMarker(context.root, {
130
+ role: schedule.initial_role || undefined,
131
+ });
132
+
133
+ if (!consumed.ok) {
134
+ return {
135
+ ok: false,
136
+ exitCode: 1,
137
+ result: {
138
+ ...scheduleResult,
139
+ action: 'preemption_failed',
140
+ error: consumed.error,
141
+ injected_intent_id: execution.result?.preempted_by || null,
142
+ },
143
+ };
144
+ }
145
+
146
+ return {
147
+ ok: true,
148
+ exitCode: 0,
149
+ result: {
150
+ ...scheduleResult,
151
+ action: 'preempted',
152
+ injected_intent_id: consumed.intent_id,
153
+ injected_turn_id: consumed.turn_id,
154
+ injected_role: consumed.role,
155
+ },
156
+ };
157
+ }
158
+
159
+ async function continueActiveScheduledRun(context, opts = {}) {
160
+ const continuation = findContinuableScheduleRun(context.root, context.config, {
161
+ scheduleId: opts.schedule || null,
162
+ });
163
+ if (!continuation.ok) {
164
+ return { matched: false, reason: continuation.reason };
165
+ }
166
+
167
+ const { schedule_id: scheduleId, schedule, state } = continuation;
168
+
169
+ if (!opts.json) {
170
+ console.log(chalk.cyan(`Continuing active scheduled run: ${scheduleId}`));
171
+ }
172
+
173
+ const execution = await executeGovernedRun(context, {
174
+ maxTurns: schedule.max_turns,
175
+ autoApprove: schedule.auto_approve !== false,
176
+ report: true,
177
+ log: opts.json ? () => {} : console.log,
178
+ });
179
+
180
+ if (execution.result?.stop_reason === 'priority_preempted') {
181
+ const promoted = consumeScheduledPriorityPreemption(context, scheduleId, schedule, execution, state, opts.at);
182
+ return {
183
+ matched: true,
184
+ ok: promoted.ok,
185
+ exitCode: promoted.exitCode,
186
+ result: promoted.result,
187
+ };
188
+ }
189
+
190
+ const blocked = execution.result?.stop_reason === 'blocked';
191
+ const action = blocked && opts.tolerateBlockedRun ? 'blocked' : 'continued';
192
+ const result = recordScheduleExecution(context, scheduleId, execution, state, opts.at || new Date().toISOString(), action);
193
+
194
+ if (execution.exitCode !== 0 && !(opts.tolerateBlockedRun && blocked)) {
195
+ return {
196
+ matched: true,
197
+ ok: false,
198
+ exitCode: execution.exitCode,
199
+ result,
200
+ };
201
+ }
202
+
203
+ return {
204
+ matched: true,
205
+ ok: true,
206
+ exitCode: 0,
207
+ result,
208
+ };
209
+ }
210
+
89
211
  async function runDueSchedules(context, opts = {}) {
212
+ if (opts.continueActiveScheduleRuns) {
213
+ const continuation = await continueActiveScheduledRun(context, opts);
214
+ if (continuation.matched) {
215
+ return continuation.ok
216
+ ? { ok: true, exitCode: continuation.exitCode, results: [continuation.result] }
217
+ : { ok: false, exitCode: continuation.exitCode, results: [continuation.result], error: 'Scheduled run failed' };
218
+ }
219
+ }
220
+
90
221
  const resolved = resolveScheduleEntries(context, opts.schedule, opts.at);
91
222
  if (!resolved.ok) {
92
223
  return { ok: false, exitCode: 1, error: resolved.error, results: [] };
@@ -150,26 +281,29 @@ async function runDueSchedules(context, opts = {}) {
150
281
  continue;
151
282
  }
152
283
 
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
- });
284
+ if (execution.result?.stop_reason === 'priority_preempted') {
285
+ const promoted = consumeScheduledPriorityPreemption(context, entry.id, entry, execution, execution.result?.state || null, nowIso);
286
+ results.push(promoted.result);
287
+ if (!promoted.ok) {
288
+ return { ok: false, exitCode: promoted.exitCode, results };
289
+ }
290
+ continue;
291
+ }
292
+
293
+ const blocked = execution.result?.stop_reason === 'blocked';
294
+ results.push(recordScheduleExecution(
295
+ context,
296
+ entry.id,
297
+ execution,
298
+ execution.result?.state || null,
299
+ nowIso,
300
+ blocked && opts.tolerateBlockedRun ? 'blocked' : 'ran',
301
+ ));
171
302
 
172
303
  if (execution.exitCode !== 0) {
304
+ if (opts.tolerateBlockedRun && blocked) {
305
+ continue;
306
+ }
173
307
  return { ok: false, exitCode: execution.exitCode, results };
174
308
  }
175
309
  }
@@ -214,6 +348,14 @@ export async function scheduleRunDueCommand(opts) {
214
348
  for (const entry of result.results) {
215
349
  if (entry.action === 'ran') {
216
350
  console.log(chalk.green(`Schedule ran: ${entry.id} (${entry.run_id || 'no run id'})`));
351
+ } else if (entry.action === 'continued') {
352
+ console.log(chalk.green(`Schedule continued: ${entry.id} (${entry.run_id || 'no run id'})`));
353
+ } else if (entry.action === 'preempted') {
354
+ console.log(chalk.yellow(`Schedule preempted by injected priority: ${entry.id} (${entry.injected_intent_id || 'unknown intent'})`));
355
+ } else if (entry.action === 'preemption_failed') {
356
+ console.log(chalk.red(`Schedule preemption failed: ${entry.id} (${entry.error || 'unknown error'})`));
357
+ } else if (entry.action === 'blocked') {
358
+ console.log(chalk.yellow(`Schedule waiting on unblock: ${entry.id}`));
217
359
  } else if (entry.action === 'skipped') {
218
360
  console.log(chalk.yellow(`Schedule skipped: ${entry.id} (${entry.reason})`));
219
361
  } else if (entry.action === 'not_due') {
@@ -338,7 +480,11 @@ export async function scheduleDaemonCommand(opts) {
338
480
  while (true) {
339
481
  cycle += 1;
340
482
  daemonState.last_cycle_started_at = new Date().toISOString();
341
- const result = await runDueSchedules(context, opts);
483
+ const result = await runDueSchedules(context, {
484
+ ...opts,
485
+ continueActiveScheduleRuns: true,
486
+ tolerateBlockedRun: true,
487
+ });
342
488
 
343
489
  updateDaemonHeartbeat(context.root, daemonState, result);
344
490
 
@@ -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,39 @@ 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.current_vision_objective) {
210
+ console.log(` Objective: ${chalk.yellow(continuousSession.current_vision_objective)}`);
211
+ }
212
+ if (continuousSession.idle_cycles > 0) {
213
+ console.log(chalk.dim(` Idle cycles: ${continuousSession.idle_cycles}/${continuousSession.max_idle_cycles}`));
214
+ }
215
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
216
+ console.log('');
217
+ }
218
+
177
219
  console.log(` ${chalk.dim('Project:')} ${config.project.name}`);
178
220
  if (config.project.goal) {
179
221
  console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
@@ -325,6 +367,16 @@ function renderGovernedStatus(context, opts) {
325
367
  }
326
368
  }
327
369
 
370
+ if (humanEscalation) {
371
+ console.log('');
372
+ console.log(` ${chalk.dim('Human task:')} ${chalk.yellow(humanEscalation.escalation_id)}${humanEscalation.service ? ` (${humanEscalation.service})` : ''}`);
373
+ console.log(` ${chalk.dim('Type:')} ${humanEscalation.type}`);
374
+ console.log(` ${chalk.dim('Unblock:')} ${chalk.cyan(humanEscalation.resolution_command)}`);
375
+ if (humanEscalation.action) {
376
+ console.log(` ${chalk.dim('Task:')} ${humanEscalation.action}`);
377
+ }
378
+ }
379
+
328
380
  if (runtimeGuidance.length > 0) {
329
381
  console.log('');
330
382
  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
+ }