agentxchain 2.115.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';
@@ -122,7 +124,7 @@ import { eventsCommand } from '../src/commands/events.js';
122
124
  import { connectorCheckCommand } from '../src/commands/connector.js';
123
125
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
124
126
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
125
- import { missionAttachChainCommand, missionListCommand, missionPlanApproveCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
127
+ import { missionAttachChainCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
126
128
 
127
129
  const __dirname = dirname(fileURLToPath(import.meta.url));
128
130
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -498,6 +500,18 @@ missionPlanCmd
498
500
  .option('-d, --dir <path>', 'Project directory')
499
501
  .action(missionPlanLaunchCommand);
500
502
 
503
+ missionPlanCmd
504
+ .command('autopilot [plan_id]')
505
+ .description('Run unattended wave execution of an approved plan (default: latest plan)')
506
+ .option('-m, --mission <mission_id>', 'Explicit mission ID')
507
+ .option('--max-waves <n>', 'Maximum number of dependency waves (default: 10)')
508
+ .option('--continue-on-failure', 'Skip failed workstreams and keep launching ready ones')
509
+ .option('--cooldown <seconds>', 'Pause between waves in seconds (default: 5)')
510
+ .option('--auto-approve', 'Auto-approve run gates during execution')
511
+ .option('-j, --json', 'Output as JSON')
512
+ .option('-d, --dir <path>', 'Project directory')
513
+ .action(missionPlanAutopilotCommand);
514
+
501
515
  missionPlanCmd
502
516
  .command('list')
503
517
  .description('List all plans for a mission')
@@ -588,6 +602,23 @@ program
588
602
  .option('--turn <id>', 'Target a specific retained turn when multiple exist')
589
603
  .action(resumeCommand);
590
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
+
591
622
  program
592
623
  .command('escalate')
593
624
  .description('Raise an operator escalation and block the governed run intentionally')
@@ -640,6 +671,11 @@ program
640
671
  .option('--chain-on <statuses>', 'Comma-separated terminal statuses that trigger chaining (default: completed)')
641
672
  .option('--chain-cooldown <seconds>', 'Seconds to wait between chained runs (default: 5)', parseInt)
642
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)
643
679
  .action(runCommand);
644
680
 
645
681
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.115.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
+ }
@@ -740,6 +740,333 @@ async function missionPlanLaunchAllReady(planTarget, opts, context) {
740
740
  }
741
741
  }
742
742
 
743
+ // ── Autopilot (unattended wave execution) ───────────────────────────────────
744
+
745
+ export async function missionPlanAutopilotCommand(planTarget, opts) {
746
+ const context = loadProjectContext(opts.dir || process.cwd());
747
+ if (!context) {
748
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
749
+ process.exit(1);
750
+ }
751
+ const { root } = context;
752
+
753
+ const mission = opts.mission
754
+ ? loadMissionArtifact(root, opts.mission)
755
+ : loadLatestMissionArtifact(root);
756
+
757
+ if (!mission) {
758
+ console.error(chalk.red('No mission found.'));
759
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
760
+ process.exit(1);
761
+ }
762
+
763
+ const plan = planTarget && planTarget !== 'latest'
764
+ ? loadPlan(root, mission.mission_id, planTarget)
765
+ : loadLatestPlan(root, mission.mission_id);
766
+
767
+ if (!plan) {
768
+ if (planTarget && planTarget !== 'latest') {
769
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
770
+ } else {
771
+ console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
772
+ console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
773
+ }
774
+ process.exit(1);
775
+ }
776
+
777
+ const continueOnFailure = !!opts.continueOnFailure;
778
+
779
+ if (plan.status === 'completed') {
780
+ if (opts.json) {
781
+ console.log(JSON.stringify({
782
+ plan_id: plan.plan_id,
783
+ mission_id: mission.mission_id,
784
+ waves: [],
785
+ summary: { total_waves: 0, total_launched: 0, completed: 0, failed: 0, terminal_reason: 'plan_completed' },
786
+ }, null, 2));
787
+ } else {
788
+ console.log(chalk.green(`Plan ${plan.plan_id} is already completed. Nothing to do.`));
789
+ }
790
+ return;
791
+ }
792
+
793
+ if (plan.status !== 'approved' && !(plan.status === 'needs_attention' && continueOnFailure)) {
794
+ console.error(chalk.red(`Plan ${plan.plan_id} status is "${plan.status}". Autopilot requires an approved plan${continueOnFailure ? ' (or needs_attention with --continue-on-failure)' : ''}.`));
795
+ process.exit(1);
796
+ }
797
+
798
+ const maxWaves = Math.max(1, parseInt(opts.maxWaves, 10) || 10);
799
+ const cooldownSeconds = Math.max(0, parseInt(opts.cooldown, 10) || 5);
800
+ const executor = opts._executeGovernedRun || executeGovernedRun;
801
+ const logger = opts._log || console.log;
802
+ const sleep = opts._sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
803
+
804
+ const waves = [];
805
+ let totalLaunched = 0;
806
+ let totalCompleted = 0;
807
+ let totalFailed = 0;
808
+ let terminalReason = null;
809
+ let interrupted = false;
810
+
811
+ // SIGINT handler
812
+ const onSigint = () => { interrupted = true; };
813
+ process.on('SIGINT', onSigint);
814
+
815
+ try {
816
+ for (let waveNum = 1; waveNum <= maxWaves; waveNum++) {
817
+ if (interrupted) {
818
+ terminalReason = 'interrupted';
819
+ break;
820
+ }
821
+
822
+ // Reload plan from disk to pick up outcomes from previous waves
823
+ const currentPlan = loadPlan(root, mission.mission_id, plan.plan_id);
824
+ if (!currentPlan) {
825
+ terminalReason = 'plan_read_error';
826
+ break;
827
+ }
828
+
829
+ if (currentPlan.status === 'completed') {
830
+ terminalReason = 'plan_completed';
831
+ break;
832
+ }
833
+
834
+ const readyWorkstreams = getReadyWorkstreams(currentPlan);
835
+ if (readyWorkstreams.length === 0) {
836
+ terminalReason = deriveAutopilotIdleOutcome(currentPlan, continueOnFailure);
837
+ break;
838
+ }
839
+
840
+ if (!opts.json) {
841
+ console.log(chalk.bold(`\n━━━ Wave ${waveNum} — launching ${readyWorkstreams.length} workstream(s) ━━━\n`));
842
+ }
843
+
844
+ const waveResults = [];
845
+ let waveHadFailure = false;
846
+
847
+ for (let i = 0; i < readyWorkstreams.length; i++) {
848
+ if (interrupted) break;
849
+
850
+ const ws = readyWorkstreams[i];
851
+ const prefix = `[${i + 1}/${readyWorkstreams.length}]`;
852
+
853
+ // Skip remaining in wave if failure and not continue-on-failure
854
+ if (waveHadFailure && !continueOnFailure) {
855
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'skipped', skip_reason: 'prior workstream failed' });
856
+ if (!opts.json) {
857
+ console.log(`${prefix} ${chalk.dim(ws.workstream_id)} — ${chalk.dim('skipped (prior workstream failed)')}`);
858
+ }
859
+ continue;
860
+ }
861
+
862
+ const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
863
+ allowNeedsAttention: continueOnFailure,
864
+ });
865
+ if (!launch.ok) {
866
+ waveHadFailure = true;
867
+ totalFailed++;
868
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'launch_error', error: launch.error });
869
+ if (!opts.json) {
870
+ console.log(`${prefix} ${chalk.red(ws.workstream_id)} — launch error: ${launch.error}`);
871
+ }
872
+ continue;
873
+ }
874
+
875
+ totalLaunched++;
876
+ if (!opts.json) {
877
+ process.stdout.write(`${prefix} ${chalk.cyan(ws.workstream_id)} → ${launch.chainId} ... `);
878
+ }
879
+
880
+ const chainOpts = {
881
+ enabled: true,
882
+ maxChains: 0,
883
+ chainOn: ['completed'],
884
+ cooldownSeconds: 0,
885
+ mission: mission.mission_id,
886
+ chainId: launch.chainId,
887
+ };
888
+ const runOpts = {
889
+ autoApprove: !!opts.autoApprove,
890
+ provenance: {
891
+ trigger: 'autopilot',
892
+ created_by: 'operator',
893
+ trigger_reason: `mission:${mission.mission_id} workstream:${ws.workstream_id} autopilot:wave-${waveNum}`,
894
+ },
895
+ };
896
+
897
+ let execution;
898
+ try {
899
+ execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
900
+ } catch (error) {
901
+ markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
902
+ terminalReason: 'execution_error',
903
+ completedAt: new Date().toISOString(),
904
+ });
905
+ waveHadFailure = true;
906
+ totalFailed++;
907
+ waveResults.push({ workstream_id: ws.workstream_id, chain_id: launch.chainId, status: 'needs_attention', error: error.message });
908
+ if (!opts.json) {
909
+ console.log(chalk.red(`needs_attention ✗ (${error.message})`));
910
+ }
911
+ continue;
912
+ }
913
+
914
+ const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
915
+ const wsTerminalReason = lastRun?.status === 'completed'
916
+ ? 'completed'
917
+ : (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
918
+
919
+ markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
920
+ terminalReason: wsTerminalReason,
921
+ completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
922
+ });
923
+
924
+ const wsStatus = wsTerminalReason === 'completed' ? 'completed' : 'needs_attention';
925
+ if (wsStatus === 'completed') {
926
+ totalCompleted++;
927
+ } else {
928
+ totalFailed++;
929
+ waveHadFailure = true;
930
+ }
931
+
932
+ waveResults.push({ workstream_id: ws.workstream_id, chain_id: launch.chainId, status: wsStatus });
933
+
934
+ if (!opts.json) {
935
+ if (wsStatus === 'completed') {
936
+ console.log(chalk.green('completed ✓'));
937
+ } else {
938
+ console.log(chalk.red('needs_attention ✗'));
939
+ }
940
+ }
941
+ }
942
+
943
+ waves.push({ wave: waveNum, launched: waveResults.filter((r) => r.chain_id).map((r) => r.workstream_id), results: waveResults });
944
+
945
+ if (interrupted) {
946
+ terminalReason = 'interrupted';
947
+ break;
948
+ }
949
+
950
+ // Check if plan is now complete after this wave
951
+ const afterWavePlan = loadPlan(root, mission.mission_id, plan.plan_id);
952
+ if (afterWavePlan?.status === 'completed') {
953
+ terminalReason = 'plan_completed';
954
+ break;
955
+ }
956
+
957
+ const remainingReadyWorkstreams = getReadyWorkstreams(afterWavePlan);
958
+ if (remainingReadyWorkstreams.length === 0) {
959
+ terminalReason = deriveAutopilotIdleOutcome(afterWavePlan, continueOnFailure);
960
+ break;
961
+ }
962
+
963
+ if (waveHadFailure && !continueOnFailure) {
964
+ terminalReason = 'failure_stopped';
965
+ break;
966
+ }
967
+
968
+ if (waveNum === maxWaves) {
969
+ terminalReason = 'wave_limit_reached';
970
+ break;
971
+ }
972
+
973
+ // Cooldown between waves
974
+ if (cooldownSeconds > 0 && !interrupted) {
975
+ if (!opts.json) {
976
+ console.log(chalk.dim(`\nCooldown: ${cooldownSeconds}s before next wave...\n`));
977
+ }
978
+ await sleep(cooldownSeconds * 1000);
979
+ }
980
+ }
981
+ } finally {
982
+ process.removeListener('SIGINT', onSigint);
983
+ }
984
+
985
+ if (!terminalReason) {
986
+ terminalReason = totalFailed > 0
987
+ ? (continueOnFailure ? 'plan_incomplete' : 'failure_stopped')
988
+ : 'plan_completed';
989
+ }
990
+
991
+ const jsonOutput = {
992
+ plan_id: plan.plan_id,
993
+ mission_id: mission.mission_id,
994
+ waves,
995
+ summary: {
996
+ total_waves: waves.length,
997
+ total_launched: totalLaunched,
998
+ completed: totalCompleted,
999
+ failed: totalFailed,
1000
+ terminal_reason: terminalReason,
1001
+ },
1002
+ };
1003
+
1004
+ if (opts.json) {
1005
+ console.log(JSON.stringify(jsonOutput, null, 2));
1006
+ } else {
1007
+ console.log('');
1008
+ console.log(chalk.bold('━━━ Autopilot Summary ━━━'));
1009
+ console.log(` Waves: ${waves.length}`);
1010
+ console.log(` Launched: ${totalLaunched}`);
1011
+ console.log(` Completed: ${totalCompleted}`);
1012
+ console.log(` Failed: ${totalFailed}`);
1013
+ console.log(` Outcome: ${formatTerminalReason(terminalReason)}`);
1014
+ if (terminalReason === 'plan_completed') {
1015
+ console.log(chalk.green('\n Plan completed successfully.'));
1016
+ } else if (terminalReason === 'deadlock') {
1017
+ console.log(chalk.red('\n Deadlock: remaining workstreams are blocked with unsatisfiable dependencies.'));
1018
+ console.log(chalk.dim(' Inspect with `agentxchain mission plan show latest`.'));
1019
+ } else if (terminalReason === 'wave_limit_reached') {
1020
+ console.log(chalk.yellow(`\n Wave limit reached. Run autopilot again to continue.`));
1021
+ } else if (terminalReason === 'failure_stopped') {
1022
+ console.log(chalk.red('\n Stopped due to workstream failure.'));
1023
+ console.log(chalk.dim(' Use --continue-on-failure to skip failures, or retry with `mission plan launch --workstream <id> --retry`.'));
1024
+ } else if (terminalReason === 'plan_incomplete') {
1025
+ console.log(chalk.yellow('\n Autopilot exhausted all launchable work, but failed workstreams still need attention.'));
1026
+ console.log(chalk.dim(' Retry the failed workstream or inspect the plan before running autopilot again.'));
1027
+ } else if (terminalReason === 'interrupted') {
1028
+ console.log(chalk.yellow('\n Interrupted by operator.'));
1029
+ }
1030
+ }
1031
+
1032
+ if (terminalReason !== 'plan_completed') {
1033
+ process.exit(1);
1034
+ }
1035
+ }
1036
+
1037
+ function formatTerminalReason(reason) {
1038
+ switch (reason) {
1039
+ case 'plan_completed': return chalk.green('plan completed');
1040
+ case 'failure_stopped': return chalk.red('stopped on failure');
1041
+ case 'plan_incomplete': return chalk.yellow('incomplete after failures');
1042
+ case 'deadlock': return chalk.red('deadlock');
1043
+ case 'wave_limit_reached': return chalk.yellow('wave limit reached');
1044
+ case 'interrupted': return chalk.yellow('interrupted');
1045
+ case 'no_ready_workstreams': return chalk.yellow('no ready workstreams');
1046
+ default: return reason || '—';
1047
+ }
1048
+ }
1049
+
1050
+ function deriveAutopilotIdleOutcome(plan, continueOnFailure) {
1051
+ const summary = getWorkstreamStatusSummary(plan);
1052
+ const allCompleted = plan.workstreams.every((ws) => ws.launch_status === 'completed');
1053
+ if (allCompleted) {
1054
+ return 'plan_completed';
1055
+ }
1056
+
1057
+ const hasNeedsAttention = (summary.needs_attention || 0) > 0;
1058
+ const hasBlocked = (summary.blocked || 0) > 0;
1059
+ const hasLaunched = (summary.launched || 0) > 0;
1060
+
1061
+ if (hasNeedsAttention) {
1062
+ return continueOnFailure ? 'plan_incomplete' : 'failure_stopped';
1063
+ }
1064
+ if (hasBlocked && !hasLaunched) {
1065
+ return 'deadlock';
1066
+ }
1067
+ return 'no_ready_workstreams';
1068
+ }
1069
+
743
1070
  // ── Plan rendering ───────────────────────────────────────────────────────────
744
1071
 
745
1072
  function renderPlan(plan) {
@@ -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'));