agentxchain 2.115.0 → 2.116.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/bin/agentxchain.js +13 -1
- package/package.json +1 -1
- package/src/commands/mission.js +327 -0
- package/src/lib/mission-plans.js +6 -2
package/bin/agentxchain.js
CHANGED
|
@@ -122,7 +122,7 @@ import { eventsCommand } from '../src/commands/events.js';
|
|
|
122
122
|
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
123
123
|
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
|
|
124
124
|
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';
|
|
125
|
+
import { missionAttachChainCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
|
|
126
126
|
|
|
127
127
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
128
128
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -498,6 +498,18 @@ missionPlanCmd
|
|
|
498
498
|
.option('-d, --dir <path>', 'Project directory')
|
|
499
499
|
.action(missionPlanLaunchCommand);
|
|
500
500
|
|
|
501
|
+
missionPlanCmd
|
|
502
|
+
.command('autopilot [plan_id]')
|
|
503
|
+
.description('Run unattended wave execution of an approved plan (default: latest plan)')
|
|
504
|
+
.option('-m, --mission <mission_id>', 'Explicit mission ID')
|
|
505
|
+
.option('--max-waves <n>', 'Maximum number of dependency waves (default: 10)')
|
|
506
|
+
.option('--continue-on-failure', 'Skip failed workstreams and keep launching ready ones')
|
|
507
|
+
.option('--cooldown <seconds>', 'Pause between waves in seconds (default: 5)')
|
|
508
|
+
.option('--auto-approve', 'Auto-approve run gates during execution')
|
|
509
|
+
.option('-j, --json', 'Output as JSON')
|
|
510
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
511
|
+
.action(missionPlanAutopilotCommand);
|
|
512
|
+
|
|
501
513
|
missionPlanCmd
|
|
502
514
|
.command('list')
|
|
503
515
|
.description('List all plans for a mission')
|
package/package.json
CHANGED
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/lib/mission-plans.js
CHANGED
|
@@ -390,8 +390,12 @@ export function launchWorkstream(root, missionId, planId, workstreamId, options
|
|
|
390
390
|
if (!plan) {
|
|
391
391
|
return { ok: false, error: `Plan not found: ${planId}` };
|
|
392
392
|
}
|
|
393
|
-
|
|
394
|
-
|
|
393
|
+
const allowNeedsAttention = options.allowNeedsAttention === true;
|
|
394
|
+
if (plan.status !== 'approved' && !(allowNeedsAttention && plan.status === 'needs_attention')) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: `Plan ${planId} is not approved (status: "${plan.status}"). Approve the plan before launching workstreams.`,
|
|
398
|
+
};
|
|
395
399
|
}
|
|
396
400
|
|
|
397
401
|
const ws = plan.workstreams.find((w) => w.workstream_id === workstreamId);
|