agentxchain 2.96.1 → 2.97.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.96.1",
3
+ "version": "2.97.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,190 @@ import { createBridgeServer } from '../lib/dashboard/bridge-server.js';
17
17
 
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
 
20
+ function restoreExportFiles(root, files, scopeLabel) {
21
+ if (!files || typeof files !== 'object') {
22
+ throw new Error(`${scopeLabel} is missing a valid files object.`);
23
+ }
24
+
25
+ let restored = 0;
26
+ for (const [relPath, entry] of Object.entries(files)) {
27
+ const absPath = join(root, relPath);
28
+ mkdirSync(dirname(absPath), { recursive: true });
29
+
30
+ if (typeof entry === 'string') {
31
+ writeFileSync(absPath, entry);
32
+ restored++;
33
+ continue;
34
+ }
35
+
36
+ if (!entry || typeof entry !== 'object' || typeof entry.content_base64 !== 'string') {
37
+ throw new Error(`${scopeLabel} entry "${relPath}" must provide content_base64.`);
38
+ }
39
+
40
+ writeFileSync(absPath, Buffer.from(entry.content_base64, 'base64'));
41
+ restored++;
42
+ }
43
+
44
+ return restored;
45
+ }
46
+
47
+ function restoreCoordinatorRepos(tempRoot, repos) {
48
+ if (!repos || typeof repos !== 'object') {
49
+ return 0;
50
+ }
51
+
52
+ let restored = 0;
53
+ for (const [repoId, repoEntry] of Object.entries(repos)) {
54
+ if (!repoEntry || repoEntry.ok === false) {
55
+ continue;
56
+ }
57
+
58
+ if (typeof repoEntry.path !== 'string' || !repoEntry.path.trim()) {
59
+ throw new Error(`Coordinator repo "${repoId}" is marked ok but has no path.`);
60
+ }
61
+
62
+ const nestedFiles = repoEntry.export?.files;
63
+ if (!nestedFiles || typeof nestedFiles !== 'object') {
64
+ throw new Error(`Coordinator repo "${repoId}" is marked ok but has no nested export.files.`);
65
+ }
66
+
67
+ const repoRoot = join(tempRoot, repoEntry.path);
68
+ mkdirSync(repoRoot, { recursive: true });
69
+ restored += restoreExportFiles(repoRoot, nestedFiles, `repos.${repoId}.export.files`);
70
+ }
71
+
72
+ return restored;
73
+ }
74
+
75
+ function readEmbeddedJsonEntry(entry, label) {
76
+ if (!entry || typeof entry !== 'object') {
77
+ throw new Error(`${label} is not a valid export file entry.`);
78
+ }
79
+
80
+ if (entry.data && typeof entry.data === 'object') {
81
+ return entry.data;
82
+ }
83
+
84
+ if (typeof entry.content_base64 !== 'string' || !entry.content_base64) {
85
+ throw new Error(`${label} must provide content_base64.`);
86
+ }
87
+
88
+ try {
89
+ return JSON.parse(Buffer.from(entry.content_base64, 'base64').toString('utf8'));
90
+ } catch (err) {
91
+ throw new Error(`${label} contains invalid JSON: ${err.message}`);
92
+ }
93
+ }
94
+
95
+ function getCoordinatorReplayPhases(exportData) {
96
+ const coordinatorConfig = exportData?.config
97
+ || readEmbeddedJsonEntry(exportData?.files?.['agentxchain-multi.json'], 'files["agentxchain-multi.json"]');
98
+ const routingPhases = Object.keys(coordinatorConfig?.routing || {});
99
+ if (routingPhases.length > 0) {
100
+ return routingPhases;
101
+ }
102
+
103
+ const phases = [];
104
+ for (const workstreamId of Object.keys(coordinatorConfig?.workstreams || {})) {
105
+ const phase = coordinatorConfig.workstreams?.[workstreamId]?.phase;
106
+ if (phase && !phases.includes(phase)) {
107
+ phases.push(phase);
108
+ }
109
+ }
110
+
111
+ return phases.length > 0 ? phases : ['planning'];
112
+ }
113
+
114
+ function restoreFailedCoordinatorRepoStubs(tempRoot, exportData) {
115
+ if (!exportData?.repos || typeof exportData.repos !== 'object') {
116
+ return 0;
117
+ }
118
+
119
+ const phases = getCoordinatorReplayPhases(exportData);
120
+ const coordinatorState = exportData?.files?.['.agentxchain/multirepo/state.json']
121
+ ? readEmbeddedJsonEntry(exportData.files['.agentxchain/multirepo/state.json'], 'files[".agentxchain/multirepo/state.json"]')
122
+ : null;
123
+
124
+ let restored = 0;
125
+ for (const [repoId, repoEntry] of Object.entries(exportData.repos)) {
126
+ if (!repoEntry || repoEntry.ok !== false) {
127
+ continue;
128
+ }
129
+
130
+ if (typeof repoEntry.path !== 'string' || !repoEntry.path.trim()) {
131
+ throw new Error(`Coordinator repo "${repoId}" failed export but has no path.`);
132
+ }
133
+
134
+ const repoRoot = join(tempRoot, repoEntry.path);
135
+ const promptPath = '.agentxchain/prompts/replay.md';
136
+ const repoRun = coordinatorState?.repo_runs?.[repoId] || {};
137
+ const defaultPhase = repoRun.phase || phases[0] || 'planning';
138
+ const routing = Object.fromEntries(
139
+ phases.map((phase) => [
140
+ phase,
141
+ {
142
+ entry_role: 'replay',
143
+ allowed_next_roles: ['replay', 'human'],
144
+ },
145
+ ]),
146
+ );
147
+ const config = {
148
+ schema_version: '1.0',
149
+ template: 'generic',
150
+ project: {
151
+ id: `${repoId}-replay-placeholder`,
152
+ name: `${repoId} Replay Placeholder`,
153
+ },
154
+ roles: {
155
+ replay: {
156
+ title: 'Replay Placeholder',
157
+ mandate: 'Preserve coordinator replay continuity when nested repo export is unavailable.',
158
+ write_authority: 'review_only',
159
+ runtime: 'replay-placeholder',
160
+ },
161
+ },
162
+ runtimes: {
163
+ 'replay-placeholder': { type: 'manual' },
164
+ },
165
+ routing,
166
+ gates: {},
167
+ prompts: {
168
+ replay: promptPath,
169
+ },
170
+ rules: {
171
+ challenge_required: true,
172
+ max_turn_retries: 2,
173
+ max_deadlock_cycles: 2,
174
+ },
175
+ };
176
+ const state = {
177
+ schema_version: '1.1',
178
+ run_id: repoRun.run_id || null,
179
+ status: repoRun.status || 'blocked',
180
+ phase: defaultPhase,
181
+ active_turns: {},
182
+ turn_sequence: 0,
183
+ retained_turns: {},
184
+ budget_reservations: {},
185
+ phase_gate_status: {},
186
+ };
187
+
188
+ mkdirSync(join(repoRoot, '.agentxchain', 'prompts'), { recursive: true });
189
+ writeFileSync(join(repoRoot, 'agentxchain.json'), `${JSON.stringify(config, null, 2)}\n`);
190
+ writeFileSync(join(repoRoot, '.agentxchain', 'state.json'), `${JSON.stringify(state, null, 2)}\n`);
191
+ writeFileSync(join(repoRoot, '.agentxchain', 'history.jsonl'), '');
192
+ writeFileSync(join(repoRoot, '.agentxchain', 'events.jsonl'), '');
193
+ writeFileSync(join(repoRoot, '.agentxchain', 'decision-ledger.jsonl'), '');
194
+ writeFileSync(
195
+ join(repoRoot, promptPath),
196
+ '# Replay Placeholder\n\nThis repo export was unavailable in the coordinator artifact. Replay restores a placeholder so coordinator dashboards remain readable.\n',
197
+ );
198
+ restored += 6;
199
+ }
200
+
201
+ return restored;
202
+ }
203
+
20
204
  export async function replayExportCommand(exportFile, opts = {}) {
21
205
  if (!exportFile) {
22
206
  console.error(chalk.red('Usage: agentxchain replay export <export-file>'));
@@ -51,21 +235,22 @@ export async function replayExportCommand(exportFile, opts = {}) {
51
235
  mkdirSync(tempRoot, { recursive: true });
52
236
  mkdirSync(tempAgentxchainDir, { recursive: true });
53
237
 
54
- // Write all embedded files from the export
55
- let fileCount = 0;
56
- for (const [relPath, content] of Object.entries(exportData.files)) {
57
- const absPath = join(tempRoot, relPath);
58
- mkdirSync(dirname(absPath), { recursive: true });
59
- writeFileSync(absPath, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
60
- fileCount++;
238
+ // Restore the real exported bytes for top-level files.
239
+ let fileCount = restoreExportFiles(tempRoot, exportData.files, 'files');
240
+
241
+ // Coordinator exports also need successful nested child repo exports
242
+ // rehydrated under their declared repo paths for dashboard replay.
243
+ if (exportData.export_kind === 'agentxchain_coordinator_export') {
244
+ fileCount += restoreCoordinatorRepos(tempRoot, exportData.repos);
245
+ fileCount += restoreFailedCoordinatorRepoStubs(tempRoot, exportData);
61
246
  }
62
247
 
63
248
  // Ensure agentxchain.json exists (needed by some dashboard endpoints)
64
249
  const configPath = join(tempRoot, 'agentxchain.json');
65
- if (!existsSync(configPath)) {
250
+ if (exportData.export_kind !== 'agentxchain_coordinator_export' && !existsSync(configPath)) {
66
251
  // Synthesize a minimal config from export summary
67
252
  const minimalConfig = {
68
- protocol_version: exportData.summary?.protocol_version || 6,
253
+ protocol_version: exportData.summary?.protocol_version || null,
69
254
  protocol_mode: 'governed',
70
255
  version: 4,
71
256
  project: { name: exportData.summary?.project_name || 'replay-export' },
@@ -153,6 +338,10 @@ export async function replayExportCommand(exportFile, opts = {}) {
153
338
  console.error(chalk.red(`Port ${opts.port || 3847} is already in use. Try --port <number>.`));
154
339
  process.exit(1);
155
340
  }
341
+ if (err instanceof Error) {
342
+ console.error(chalk.red(err.message));
343
+ process.exit(2);
344
+ }
156
345
  throw err;
157
346
  }
158
347
  }