agentxchain 2.112.0 → 2.114.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.
@@ -31,6 +31,7 @@ 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
33
  import { readMissionSnapshot } from './mission-reader.js';
34
+ import { readPlanSnapshot } from './plan-reader.js';
34
35
 
35
36
  const MIME_TYPES = {
36
37
  '.html': 'text/html; charset=utf-8',
@@ -478,6 +479,14 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
478
479
  return;
479
480
  }
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
+
481
490
  if (pathname === '/api/gate-actions') {
482
491
  const result = readGateActionSnapshot(workspacePath);
483
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, resourcesForRelativePath } from './state-reader.js';
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,14 +36,15 @@ export class FileWatcher extends EventEmitter {
36
36
  }
37
37
 
38
38
  try {
39
- const watcher = watch(watchPath, { recursive: false }, (eventType, filename) => {
39
+ const watcher = watch(watchPath, { recursive }, (eventType, filename) => {
40
40
  if (!filename || this.#closed) return;
41
- const base = basename(filename);
42
- const relativePath = relativeDir ? `${relativeDir}/${base}` : base;
41
+ // For recursive watchers, filename includes subdirectory path
42
+ const fileSegment = recursive ? filename.replace(/\\/g, '/') : basename(filename);
43
+ const relativePath = relativeDir ? `${relativeDir}/${fileSegment}` : fileSegment;
43
44
  const resources = resourcesForRelativePath(relativePath);
44
45
 
45
46
  if (resources.length === 0) {
46
- if (!relativeDir && base === 'multirepo') {
47
+ if (!relativeDir && fileSegment === 'multirepo') {
47
48
  this.#watchPath('multirepo');
48
49
  }
49
50
  return;
@@ -79,6 +80,9 @@ export class FileWatcher extends EventEmitter {
79
80
  for (const relativeDir of WATCH_DIRECTORIES) {
80
81
  this.#watchPath(relativeDir);
81
82
  }
83
+ for (const relativeDir of RECURSIVE_WATCH_DIRECTORIES) {
84
+ this.#watchPath(relativeDir, { recursive: true });
85
+ }
82
86
  }
83
87
 
84
88
  stop() {
@@ -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
+ }
@@ -66,12 +66,23 @@ export const WATCH_DIRECTORIES = [
66
66
  'reports',
67
67
  ];
68
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
+
69
77
  export function normalizeRelativePath(filePath) {
70
78
  return normalize(filePath).replace(/\\/g, '/').replace(/^\.\/+/, '');
71
79
  }
72
80
 
73
81
  export function resourcesForRelativePath(filePath) {
74
82
  const normalized = normalizeRelativePath(filePath);
83
+ if (normalized.startsWith('missions/plans/') && normalized.endsWith('.json')) {
84
+ return ['/api/plans', '/api/missions'];
85
+ }
75
86
  if (normalized.startsWith('missions/') && normalized.endsWith('.json')) {
76
87
  return ['/api/missions'];
77
88
  }