agentxchain 2.111.0 → 2.113.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 +86 -0
- package/dashboard/app.js +4 -0
- package/dashboard/components/mission.js +356 -0
- package/dashboard/index.html +1 -0
- package/package.json +2 -1
- package/scripts/check-release-alignment.mjs +66 -0
- package/scripts/release-bump.sh +8 -59
- package/scripts/release-preflight.sh +23 -8
- package/src/commands/mission.js +780 -0
- package/src/commands/run.js +3 -1
- package/src/lib/dashboard/bridge-server.js +17 -0
- package/src/lib/dashboard/file-watcher.js +22 -16
- package/src/lib/dashboard/mission-reader.js +14 -0
- package/src/lib/dashboard/plan-reader.js +108 -0
- package/src/lib/dashboard/state-reader.js +23 -3
- package/src/lib/mission-plans.js +520 -0
- package/src/lib/missions.js +195 -0
- package/src/lib/release-alignment.js +336 -0
- package/src/lib/run-chain.js +39 -3
package/src/commands/run.js
CHANGED
|
@@ -56,7 +56,9 @@ export async function runCommand(opts) {
|
|
|
56
56
|
const chainOpts = resolveChainOptions(opts, context.config);
|
|
57
57
|
if (chainOpts.enabled) {
|
|
58
58
|
console.log(chalk.cyan.bold('agentxchain run --chain'));
|
|
59
|
-
|
|
59
|
+
const chainParts = [`max ${chainOpts.maxChains} continuations`, `on: ${chainOpts.chainOn.join(',')}`, `cooldown: ${chainOpts.cooldownSeconds}s`];
|
|
60
|
+
if (chainOpts.mission) chainParts.push(`mission: ${chainOpts.mission}`);
|
|
61
|
+
console.log(chalk.dim(` Chain mode: enabled (${chainParts.join(', ')})`));
|
|
60
62
|
const { exitCode } = await executeChainedRun(context, opts, chainOpts, executeGovernedRun);
|
|
61
63
|
process.exit(exitCode);
|
|
62
64
|
}
|
|
@@ -30,6 +30,8 @@ import { loadProjectContext, loadProjectState } from '../config.js';
|
|
|
30
30
|
import { evaluateApprovalSlaReminders } from '../notification-runner.js';
|
|
31
31
|
import { readGateActionSnapshot } from './gate-action-reader.js';
|
|
32
32
|
import { readChainReportSnapshot } from './chain-report-reader.js';
|
|
33
|
+
import { readMissionSnapshot } from './mission-reader.js';
|
|
34
|
+
import { readPlanSnapshot } from './plan-reader.js';
|
|
33
35
|
|
|
34
36
|
const MIME_TYPES = {
|
|
35
37
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -470,6 +472,21 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
|
|
|
470
472
|
return;
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
if (pathname === '/api/missions') {
|
|
476
|
+
const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
|
|
477
|
+
const result = readMissionSnapshot(workspacePath, { limit });
|
|
478
|
+
writeJson(res, result.status, result.body);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (pathname === '/api/plans') {
|
|
483
|
+
const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit'), 10) : undefined;
|
|
484
|
+
const missionId = url.searchParams.get('mission') || undefined;
|
|
485
|
+
const result = readPlanSnapshot(workspacePath, { limit, missionId });
|
|
486
|
+
writeJson(res, result.status, result.body);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
473
490
|
if (pathname === '/api/gate-actions') {
|
|
474
491
|
const result = readGateActionSnapshot(workspacePath);
|
|
475
492
|
writeJson(res, result.status, result.body);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { watch, existsSync } from 'fs';
|
|
9
9
|
import { basename, join } from 'path';
|
|
10
10
|
import { EventEmitter } from 'events';
|
|
11
|
-
import { WATCH_DIRECTORIES,
|
|
11
|
+
import { WATCH_DIRECTORIES, RECURSIVE_WATCH_DIRECTORIES, resourcesForRelativePath } from './state-reader.js';
|
|
12
12
|
|
|
13
13
|
const DEBOUNCE_MS = 100;
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ export class FileWatcher extends EventEmitter {
|
|
|
23
23
|
this.#agentxchainDir = agentxchainDir;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
#watchPath(relativeDir) {
|
|
26
|
+
#watchPath(relativeDir, { recursive = false } = {}) {
|
|
27
27
|
if (this.#watchers.has(relativeDir)) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
@@ -36,28 +36,31 @@ export class FileWatcher extends EventEmitter {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
const watcher = watch(watchPath, { recursive
|
|
39
|
+
const watcher = watch(watchPath, { recursive }, (eventType, filename) => {
|
|
40
40
|
if (!filename || this.#closed) return;
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
41
|
+
// For recursive watchers, filename includes subdirectory path
|
|
42
|
+
const fileSegment = recursive ? filename.replace(/\\/g, '/') : basename(filename);
|
|
43
|
+
const relativePath = relativeDir ? `${relativeDir}/${fileSegment}` : fileSegment;
|
|
44
|
+
const resources = resourcesForRelativePath(relativePath);
|
|
44
45
|
|
|
45
|
-
if (
|
|
46
|
-
if (!relativeDir &&
|
|
46
|
+
if (resources.length === 0) {
|
|
47
|
+
if (!relativeDir && fileSegment === 'multirepo') {
|
|
47
48
|
this.#watchPath('multirepo');
|
|
48
49
|
}
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.#debounceTimers.set(resource, setTimeout(() => {
|
|
56
|
-
this.#debounceTimers.delete(resource);
|
|
57
|
-
if (!this.#closed) {
|
|
58
|
-
this.emit('invalidate', { resource });
|
|
53
|
+
for (const resource of resources) {
|
|
54
|
+
if (this.#debounceTimers.has(resource)) {
|
|
55
|
+
clearTimeout(this.#debounceTimers.get(resource));
|
|
59
56
|
}
|
|
60
|
-
|
|
57
|
+
this.#debounceTimers.set(resource, setTimeout(() => {
|
|
58
|
+
this.#debounceTimers.delete(resource);
|
|
59
|
+
if (!this.#closed) {
|
|
60
|
+
this.emit('invalidate', { resource });
|
|
61
|
+
}
|
|
62
|
+
}, DEBOUNCE_MS));
|
|
63
|
+
}
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
watcher.on('error', (err) => {
|
|
@@ -77,6 +80,9 @@ export class FileWatcher extends EventEmitter {
|
|
|
77
80
|
for (const relativeDir of WATCH_DIRECTORIES) {
|
|
78
81
|
this.#watchPath(relativeDir);
|
|
79
82
|
}
|
|
83
|
+
for (const relativeDir of RECURSIVE_WATCH_DIRECTORIES) {
|
|
84
|
+
this.#watchPath(relativeDir, { recursive: true });
|
|
85
|
+
}
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
stop() {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildMissionListSummary, loadLatestMissionSnapshot } from '../missions.js';
|
|
2
|
+
|
|
3
|
+
export function readMissionSnapshot(workspacePath, { limit } = {}) {
|
|
4
|
+
const missions = buildMissionListSummary(workspacePath, limit);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
ok: true,
|
|
8
|
+
status: 200,
|
|
9
|
+
body: {
|
|
10
|
+
latest: loadLatestMissionSnapshot(workspacePath),
|
|
11
|
+
missions,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan reader — reads mission plan artifacts for the dashboard bridge.
|
|
3
|
+
*
|
|
4
|
+
* Provides a snapshot of all plans across all missions for the dashboard.
|
|
5
|
+
* Plans are advisory repo-local artifacts; this reader is not protocol-normative.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { loadAllPlans, loadLatestPlan } from '../mission-plans.js';
|
|
11
|
+
import { loadAllMissionArtifacts } from '../missions.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a dashboard-ready plan snapshot across all missions.
|
|
15
|
+
*
|
|
16
|
+
* Returns newest-first plans with per-workstream launch_status and launch_records.
|
|
17
|
+
* If missionId is provided, returns plans for that mission only.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} workspacePath - project root
|
|
20
|
+
* @param {{ limit?: number, missionId?: string }} options
|
|
21
|
+
* @returns {{ ok: boolean, status: number, body: object }}
|
|
22
|
+
*/
|
|
23
|
+
export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
|
|
24
|
+
const allPlans = [];
|
|
25
|
+
|
|
26
|
+
if (missionId) {
|
|
27
|
+
const plans = loadAllPlans(workspacePath, missionId);
|
|
28
|
+
allPlans.push(...plans);
|
|
29
|
+
} else {
|
|
30
|
+
const missions = loadAllMissionArtifacts(workspacePath);
|
|
31
|
+
for (const mission of missions) {
|
|
32
|
+
const plans = loadAllPlans(workspacePath, mission.mission_id);
|
|
33
|
+
allPlans.push(...plans);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Sort newest-first across all missions
|
|
38
|
+
allPlans.sort((a, b) => {
|
|
39
|
+
const aTime = new Date(a.created_at || 0).getTime();
|
|
40
|
+
const bTime = new Date(b.created_at || 0).getTime();
|
|
41
|
+
if (bTime !== aTime) return bTime - aTime;
|
|
42
|
+
return (b.plan_id || '').localeCompare(a.plan_id || '');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const plans = limit ? allPlans.slice(0, limit) : allPlans;
|
|
46
|
+
|
|
47
|
+
// Derive summary for the latest plan
|
|
48
|
+
const latest = plans[0] || null;
|
|
49
|
+
let latestSummary = null;
|
|
50
|
+
if (latest) {
|
|
51
|
+
latestSummary = buildPlanSummary(latest);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
status: 200,
|
|
57
|
+
body: {
|
|
58
|
+
latest: latestSummary,
|
|
59
|
+
plans: plans.map(buildPlanSummary),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a dashboard-ready summary for a single plan.
|
|
66
|
+
*/
|
|
67
|
+
function buildPlanSummary(plan) {
|
|
68
|
+
const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
|
|
69
|
+
const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
70
|
+
|
|
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
|
+
return {
|
|
78
|
+
plan_id: plan.plan_id,
|
|
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,
|
|
90
|
+
workstreams: workstreams.map((ws) => ({
|
|
91
|
+
workstream_id: ws.workstream_id,
|
|
92
|
+
title: ws.title,
|
|
93
|
+
goal: ws.goal,
|
|
94
|
+
roles: ws.roles,
|
|
95
|
+
phases: ws.phases,
|
|
96
|
+
depends_on: ws.depends_on,
|
|
97
|
+
launch_status: ws.launch_status,
|
|
98
|
+
})),
|
|
99
|
+
launch_records: launchRecords.map((lr) => ({
|
|
100
|
+
workstream_id: lr.workstream_id,
|
|
101
|
+
chain_id: lr.chain_id,
|
|
102
|
+
launched_at: lr.launched_at,
|
|
103
|
+
completed_at: lr.completed_at || null,
|
|
104
|
+
status: lr.status,
|
|
105
|
+
terminal_reason: lr.terminal_reason || null,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -62,19 +62,39 @@ FILE_TO_RESOURCE[normalizeRelativePath(REPO_DECISIONS_FILE)] = '/api/repo-decisi
|
|
|
62
62
|
export const WATCH_DIRECTORIES = [
|
|
63
63
|
'',
|
|
64
64
|
MULTIREPO_DIR,
|
|
65
|
+
'missions',
|
|
65
66
|
'reports',
|
|
66
67
|
];
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Directories that require recursive watching because they contain
|
|
71
|
+
* dynamic subdirectories (e.g., missions/plans/<mission_id>/).
|
|
72
|
+
*/
|
|
73
|
+
export const RECURSIVE_WATCH_DIRECTORIES = [
|
|
74
|
+
'missions/plans',
|
|
75
|
+
];
|
|
76
|
+
|
|
68
77
|
export function normalizeRelativePath(filePath) {
|
|
69
78
|
return normalize(filePath).replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
export function
|
|
81
|
+
export function resourcesForRelativePath(filePath) {
|
|
73
82
|
const normalized = normalizeRelativePath(filePath);
|
|
83
|
+
if (normalized.startsWith('missions/plans/') && normalized.endsWith('.json')) {
|
|
84
|
+
return ['/api/plans', '/api/missions'];
|
|
85
|
+
}
|
|
86
|
+
if (normalized.startsWith('missions/') && normalized.endsWith('.json')) {
|
|
87
|
+
return ['/api/missions'];
|
|
88
|
+
}
|
|
74
89
|
if (normalized.startsWith('reports/chain-') && normalized.endsWith('.json')) {
|
|
75
|
-
return '/api/chain-reports';
|
|
90
|
+
return ['/api/chain-reports', '/api/missions'];
|
|
76
91
|
}
|
|
77
|
-
return FILE_TO_RESOURCE[normalized]
|
|
92
|
+
return FILE_TO_RESOURCE[normalized] ? [FILE_TO_RESOURCE[normalized]] : [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resourceForRelativePath(filePath) {
|
|
96
|
+
const resources = resourcesForRelativePath(filePath);
|
|
97
|
+
return resources[0] || null;
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
/**
|