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 +1 -1
- package/bin/agentxchain.js +37 -1
- package/package.json +1 -1
- package/src/commands/events.js +8 -1
- package/src/commands/inject.js +81 -0
- package/src/commands/mission.js +327 -0
- package/src/commands/resume.js +6 -4
- package/src/commands/run.js +13 -0
- package/src/commands/schedule.js +165 -19
- package/src/commands/status.js +52 -0
- package/src/commands/unblock.js +67 -0
- package/src/lib/continuous-run.js +448 -0
- package/src/lib/governed-state.js +37 -1
- package/src/lib/human-escalations.js +434 -0
- package/src/lib/intake.js +243 -11
- package/src/lib/mission-plans.js +6 -2
- package/src/lib/notification-runner.js +3 -1
- package/src/lib/run-events.js +2 -0
- package/src/lib/run-loop.js +17 -0
- package/src/lib/run-provenance.js +4 -0
- package/src/lib/run-schedule.js +43 -0
- package/src/lib/vision-reader.js +229 -0
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 |
|
package/bin/agentxchain.js
CHANGED
|
@@ -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
package/src/commands/events.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/commands/mission.js
CHANGED
|
@@ -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) {
|
package/src/commands/resume.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|
package/src/commands/run.js
CHANGED
|
@@ -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'));
|