agentxchain 2.114.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 +14 -1
- package/package.json +1 -1
- package/src/commands/mission.js +353 -2
- package/src/lib/dashboard/plan-reader.js +3 -21
- package/src/lib/mission-plans.js +100 -2
- package/src/lib/missions.js +3 -0
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'));
|
|
@@ -491,12 +491,25 @@ missionPlanCmd
|
|
|
491
491
|
.description('Launch workstream(s) from an approved plan (default: latest plan)')
|
|
492
492
|
.option('-w, --workstream <id>', 'Workstream ID to launch (mutually exclusive with --all-ready)')
|
|
493
493
|
.option('--all-ready', 'Launch all ready workstreams sequentially (mutually exclusive with --workstream)')
|
|
494
|
+
.option('--retry', 'Retry a failed workstream (requires --workstream, only for needs_attention status)')
|
|
494
495
|
.option('-m, --mission <mission_id>', 'Explicit mission ID')
|
|
495
496
|
.option('--auto-approve', 'Auto-approve run gates while executing the launched workstream')
|
|
496
497
|
.option('-j, --json', 'Output as JSON')
|
|
497
498
|
.option('-d, --dir <path>', 'Project directory')
|
|
498
499
|
.action(missionPlanLaunchCommand);
|
|
499
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
|
+
|
|
500
513
|
missionPlanCmd
|
|
501
514
|
.command('list')
|
|
502
515
|
.description('List all plans for a mission')
|
package/package.json
CHANGED
package/src/commands/mission.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getReadyWorkstreams,
|
|
19
19
|
getWorkstreamStatusSummary,
|
|
20
20
|
launchWorkstream,
|
|
21
|
+
retryWorkstream,
|
|
21
22
|
markWorkstreamOutcome,
|
|
22
23
|
loadAllPlans,
|
|
23
24
|
loadLatestPlan,
|
|
@@ -423,6 +424,16 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
423
424
|
process.exit(1);
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
if (opts.retry && !opts.workstream) {
|
|
428
|
+
console.error(chalk.red('--retry requires --workstream <id>. Specify which failed workstream to retry.'));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (opts.retry && opts.allReady) {
|
|
433
|
+
console.error(chalk.red('--retry and --all-ready are mutually exclusive. Retry targets a specific workstream.'));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
|
|
426
437
|
if (!opts.allReady && !opts.workstream) {
|
|
427
438
|
console.error(chalk.red('--workstream <id> or --all-ready is required. Specify which workstream(s) to launch.'));
|
|
428
439
|
process.exit(1);
|
|
@@ -457,7 +468,9 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
457
468
|
process.exit(1);
|
|
458
469
|
}
|
|
459
470
|
|
|
460
|
-
const launch =
|
|
471
|
+
const launch = opts.retry
|
|
472
|
+
? retryWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream)
|
|
473
|
+
: launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
|
|
461
474
|
if (!launch.ok) {
|
|
462
475
|
console.error(chalk.red(launch.error));
|
|
463
476
|
process.exit(1);
|
|
@@ -727,6 +740,333 @@ async function missionPlanLaunchAllReady(planTarget, opts, context) {
|
|
|
727
740
|
}
|
|
728
741
|
}
|
|
729
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
|
+
|
|
730
1070
|
// ── Plan rendering ───────────────────────────────────────────────────────────
|
|
731
1071
|
|
|
732
1072
|
function renderPlan(plan) {
|
|
@@ -949,6 +1289,8 @@ function renderMissionPlanError(error) {
|
|
|
949
1289
|
}
|
|
950
1290
|
|
|
951
1291
|
function renderMissionSnapshot(snapshot) {
|
|
1292
|
+
const latestPlan = snapshot.latest_plan || null;
|
|
1293
|
+
|
|
952
1294
|
console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
|
|
953
1295
|
console.log('');
|
|
954
1296
|
console.log(` Title: ${snapshot.title || '—'}`);
|
|
@@ -967,10 +1309,19 @@ function renderMissionSnapshot(snapshot) {
|
|
|
967
1309
|
console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
|
|
968
1310
|
}
|
|
969
1311
|
|
|
1312
|
+
if (latestPlan) {
|
|
1313
|
+
console.log('');
|
|
1314
|
+
console.log(chalk.bold(' Latest plan:'));
|
|
1315
|
+
console.log(` Plan ID: ${latestPlan.plan_id || '—'}`);
|
|
1316
|
+
console.log(` Status: ${formatPlanStatus(latestPlan.status)}`);
|
|
1317
|
+
console.log(` Completion: ${latestPlan.completion_percentage}% (${latestPlan.completed_count}/${latestPlan.workstream_count} completed)`);
|
|
1318
|
+
console.log(` Workstream summary: ready ${latestPlan.ready_count}, blocked ${latestPlan.blocked_count}, launched ${latestPlan.launched_count}, completed ${latestPlan.completed_count}, needs_attention ${latestPlan.needs_attention_count}`);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
970
1321
|
if (!snapshot.chains || snapshot.chains.length === 0) {
|
|
971
1322
|
console.log('');
|
|
972
1323
|
console.log(chalk.dim(' No chains attached.'));
|
|
973
|
-
console.log(chalk.dim(' Use `agentxchain mission attach-chain latest`
|
|
1324
|
+
console.log(chalk.dim(' Use `agentxchain run --chain --mission latest` for new work, or `agentxchain mission attach-chain latest` to repair an unbound chain.'));
|
|
974
1325
|
return;
|
|
975
1326
|
}
|
|
976
1327
|
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* Plans are advisory repo-local artifacts; this reader is not protocol-normative.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { loadAllPlans, loadLatestPlan } from '../mission-plans.js';
|
|
8
|
+
import { buildPlanProgressSummary, loadAllPlans } from '../mission-plans.js';
|
|
11
9
|
import { loadAllMissionArtifacts } from '../missions.js';
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -65,28 +63,12 @@ export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
|
|
|
65
63
|
* Build a dashboard-ready summary for a single plan.
|
|
66
64
|
*/
|
|
67
65
|
function buildPlanSummary(plan) {
|
|
66
|
+
const summary = buildPlanProgressSummary(plan);
|
|
68
67
|
const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
|
|
69
68
|
const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
70
69
|
|
|
71
|
-
const statusCounts = {};
|
|
72
|
-
for (const ws of workstreams) {
|
|
73
|
-
const status = ws.launch_status || 'unknown';
|
|
74
|
-
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
70
|
return {
|
|
78
|
-
|
|
79
|
-
mission_id: plan.mission_id,
|
|
80
|
-
status: plan.status,
|
|
81
|
-
created_at: plan.created_at,
|
|
82
|
-
updated_at: plan.updated_at,
|
|
83
|
-
approved_at: plan.approved_at || null,
|
|
84
|
-
supersedes_plan_id: plan.supersedes_plan_id || null,
|
|
85
|
-
superseded_by_plan_id: plan.superseded_by_plan_id || null,
|
|
86
|
-
input_goal: plan.input?.goal || null,
|
|
87
|
-
workstream_count: workstreams.length,
|
|
88
|
-
launch_record_count: launchRecords.length,
|
|
89
|
-
workstream_status_counts: statusCounts,
|
|
71
|
+
...summary,
|
|
90
72
|
workstreams: workstreams.map((ws) => ({
|
|
91
73
|
workstream_id: ws.workstream_id,
|
|
92
74
|
title: ws.title,
|
package/src/lib/mission-plans.js
CHANGED
|
@@ -306,6 +306,38 @@ export function loadPlan(root, missionId, planId) {
|
|
|
306
306
|
return plans.find((p) => p.plan_id === planId) || null;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
export function buildPlanProgressSummary(plan) {
|
|
310
|
+
if (!plan || typeof plan !== 'object') return null;
|
|
311
|
+
|
|
312
|
+
const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
|
|
313
|
+
const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
314
|
+
const workstreamStatusCounts = getWorkstreamStatusSummary(plan);
|
|
315
|
+
const completedCount = workstreamStatusCounts.completed || 0;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
plan_id: plan.plan_id,
|
|
319
|
+
mission_id: plan.mission_id,
|
|
320
|
+
status: plan.status,
|
|
321
|
+
created_at: plan.created_at,
|
|
322
|
+
updated_at: plan.updated_at,
|
|
323
|
+
approved_at: plan.approved_at || null,
|
|
324
|
+
supersedes_plan_id: plan.supersedes_plan_id || null,
|
|
325
|
+
superseded_by_plan_id: plan.superseded_by_plan_id || null,
|
|
326
|
+
input_goal: plan.input?.goal || null,
|
|
327
|
+
workstream_count: workstreams.length,
|
|
328
|
+
launch_record_count: launchRecords.length,
|
|
329
|
+
workstream_status_counts: workstreamStatusCounts,
|
|
330
|
+
ready_count: workstreamStatusCounts.ready || 0,
|
|
331
|
+
blocked_count: workstreamStatusCounts.blocked || 0,
|
|
332
|
+
launched_count: workstreamStatusCounts.launched || 0,
|
|
333
|
+
completed_count: completedCount,
|
|
334
|
+
needs_attention_count: workstreamStatusCounts.needs_attention || 0,
|
|
335
|
+
completion_percentage: workstreams.length === 0
|
|
336
|
+
? 0
|
|
337
|
+
: Math.round((completedCount / workstreams.length) * 100),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
309
341
|
// ── Workstream launch ───────────────────────────────────────────────────────
|
|
310
342
|
|
|
311
343
|
export function didChainFinishSuccessfully(chainReport) {
|
|
@@ -358,8 +390,12 @@ export function launchWorkstream(root, missionId, planId, workstreamId, options
|
|
|
358
390
|
if (!plan) {
|
|
359
391
|
return { ok: false, error: `Plan not found: ${planId}` };
|
|
360
392
|
}
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
};
|
|
363
399
|
}
|
|
364
400
|
|
|
365
401
|
const ws = plan.workstreams.find((w) => w.workstream_id === workstreamId);
|
|
@@ -443,6 +479,12 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
|
|
|
443
479
|
}
|
|
444
480
|
}
|
|
445
481
|
}
|
|
482
|
+
|
|
483
|
+
// Auto-complete plan when all workstreams are completed
|
|
484
|
+
const allCompleted = plan.workstreams.every((w) => w.launch_status === 'completed');
|
|
485
|
+
if (allCompleted) {
|
|
486
|
+
plan.status = 'completed';
|
|
487
|
+
}
|
|
446
488
|
} else {
|
|
447
489
|
ws.launch_status = 'needs_attention';
|
|
448
490
|
plan.status = 'needs_attention';
|
|
@@ -454,6 +496,62 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
|
|
|
454
496
|
return { ok: true, plan, workstream: ws };
|
|
455
497
|
}
|
|
456
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Retry a failed workstream by resetting its status and creating a new launch record.
|
|
501
|
+
*
|
|
502
|
+
* Only workstreams with launch_status === 'needs_attention' can be retried.
|
|
503
|
+
* The old launch record is preserved for audit. A new launch record with a new
|
|
504
|
+
* chain_id is created. If the plan was in 'needs_attention' status, it returns
|
|
505
|
+
* to 'approved' since the retry represents a new attempt.
|
|
506
|
+
*
|
|
507
|
+
* @returns {{ ok: boolean, plan?: object, workstream?: object, chainId?: string, launchRecord?: object, error?: string }}
|
|
508
|
+
*/
|
|
509
|
+
export function retryWorkstream(root, missionId, planId, workstreamId, options = {}) {
|
|
510
|
+
const plan = loadPlan(root, missionId, planId);
|
|
511
|
+
if (!plan) {
|
|
512
|
+
return { ok: false, error: `Plan not found: ${planId}` };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const ws = plan.workstreams.find((w) => w.workstream_id === workstreamId);
|
|
516
|
+
if (!ws) {
|
|
517
|
+
return { ok: false, error: `Workstream not found: ${workstreamId}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (ws.launch_status !== 'needs_attention') {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
error: `Workstream ${workstreamId} cannot be retried (status: "${ws.launch_status}"). Only "needs_attention" workstreams can be retried.`,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Generate new chain ID for the retry
|
|
528
|
+
const chainId = options.chainId || `chain-${randomUUID().slice(0, 8)}`;
|
|
529
|
+
const now = new Date().toISOString();
|
|
530
|
+
const launchRecord = {
|
|
531
|
+
workstream_id: workstreamId,
|
|
532
|
+
chain_id: chainId,
|
|
533
|
+
launched_at: now,
|
|
534
|
+
status: 'launched',
|
|
535
|
+
retry: true,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (!Array.isArray(plan.launch_records)) {
|
|
539
|
+
plan.launch_records = [];
|
|
540
|
+
}
|
|
541
|
+
plan.launch_records.push(launchRecord);
|
|
542
|
+
ws.launch_status = 'launched';
|
|
543
|
+
|
|
544
|
+
// Restore plan status from needs_attention to approved (retry in progress)
|
|
545
|
+
if (plan.status === 'needs_attention') {
|
|
546
|
+
plan.status = 'approved';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
plan.updated_at = now;
|
|
550
|
+
writePlanArtifact(root, missionId, plan);
|
|
551
|
+
|
|
552
|
+
return { ok: true, plan, workstream: ws, chainId, launchRecord };
|
|
553
|
+
}
|
|
554
|
+
|
|
457
555
|
// ── Batch launch helpers ───────────────────────────────────────────────────
|
|
458
556
|
|
|
459
557
|
/**
|
package/src/lib/missions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { loadAllChainReports, loadChainReport, loadLatestChainReport } from './chain-reports.js';
|
|
4
|
+
import { buildPlanProgressSummary, loadLatestPlan } from './mission-plans.js';
|
|
4
5
|
import { getActiveRepoDecisions } from './repo-decisions.js';
|
|
5
6
|
|
|
6
7
|
const MISSION_ATTENTION_TERMINALS = new Set(['operator_abort', 'parent_validation_failed']);
|
|
@@ -139,6 +140,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
|
|
|
139
140
|
const totalRuns = chains.reduce((sum, chain) => sum + (chain.runs?.length || 0), 0);
|
|
140
141
|
const totalTurns = chains.reduce((sum, chain) => sum + (chain.total_turns || 0), 0);
|
|
141
142
|
const latestChain = chains[0] || null;
|
|
143
|
+
const latestPlan = loadLatestPlan(root, missionArtifact.mission_id);
|
|
142
144
|
const activeRepoDecisions = getActiveRepoDecisions(root);
|
|
143
145
|
|
|
144
146
|
return {
|
|
@@ -151,6 +153,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
|
|
|
151
153
|
total_turns: totalTurns,
|
|
152
154
|
latest_chain_id: latestChain?.chain_id || null,
|
|
153
155
|
latest_terminal_reason: latestChain?.terminal_reason || null,
|
|
156
|
+
latest_plan: buildPlanProgressSummary(latestPlan),
|
|
154
157
|
active_repo_decisions_count: activeRepoDecisions.length,
|
|
155
158
|
chains,
|
|
156
159
|
};
|