agentxchain 2.5.0 → 2.7.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.
@@ -0,0 +1,426 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { isDeepStrictEqual } from 'node:util';
5
+
6
+ const SUPPORTED_EXPORT_SCHEMA_VERSION = '0.2';
7
+ const VALID_FILE_FORMATS = new Set(['json', 'jsonl', 'text']);
8
+
9
+ function sha256(buffer) {
10
+ return createHash('sha256').update(buffer).digest('hex');
11
+ }
12
+
13
+ function parseJsonl(raw, relPath) {
14
+ if (!raw.trim()) {
15
+ return [];
16
+ }
17
+
18
+ return raw
19
+ .split('\n')
20
+ .filter((line) => line.trim())
21
+ .map((line, index) => {
22
+ try {
23
+ return JSON.parse(line);
24
+ } catch (error) {
25
+ throw new Error(`${relPath}: invalid JSONL at line ${index + 1}: ${error.message}`);
26
+ }
27
+ });
28
+ }
29
+
30
+ function addError(errors, path, message) {
31
+ errors.push(`${path}: ${message}`);
32
+ }
33
+
34
+ function verifyFileEntry(relPath, entry, errors) {
35
+ const path = `files.${relPath}`;
36
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
37
+ addError(errors, path, 'file entry must be an object');
38
+ return;
39
+ }
40
+
41
+ if (!VALID_FILE_FORMATS.has(entry.format)) {
42
+ addError(errors, path, `unsupported format "${entry.format}"`);
43
+ return;
44
+ }
45
+
46
+ if (!Number.isInteger(entry.bytes) || entry.bytes < 0) {
47
+ addError(errors, path, 'bytes must be a non-negative integer');
48
+ }
49
+
50
+ if (typeof entry.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
51
+ addError(errors, path, 'sha256 must be a 64-character lowercase hex digest');
52
+ }
53
+
54
+ if (typeof entry.content_base64 !== 'string' || entry.content_base64.length === 0) {
55
+ addError(errors, path, 'content_base64 must be a non-empty string');
56
+ return;
57
+ }
58
+
59
+ let buffer;
60
+ try {
61
+ buffer = Buffer.from(entry.content_base64, 'base64');
62
+ } catch (error) {
63
+ addError(errors, path, `content_base64 is not valid base64: ${error.message}`);
64
+ return;
65
+ }
66
+
67
+ if (buffer.byteLength !== entry.bytes) {
68
+ addError(errors, path, `bytes mismatch: expected ${entry.bytes}, got ${buffer.byteLength}`);
69
+ }
70
+
71
+ if (sha256(buffer) !== entry.sha256) {
72
+ addError(errors, path, 'sha256 does not match content_base64');
73
+ }
74
+
75
+ const raw = buffer.toString('utf8');
76
+
77
+ try {
78
+ if (entry.format === 'json') {
79
+ const parsed = JSON.parse(raw);
80
+ if (!isDeepStrictEqual(parsed, entry.data)) {
81
+ addError(errors, path, 'data does not match decoded JSON content');
82
+ }
83
+ return;
84
+ }
85
+
86
+ if (entry.format === 'jsonl') {
87
+ const parsed = parseJsonl(raw, relPath);
88
+ if (!isDeepStrictEqual(parsed, entry.data)) {
89
+ addError(errors, path, 'data does not match decoded JSONL content');
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (raw !== entry.data) {
95
+ addError(errors, path, 'data does not match decoded text content');
96
+ }
97
+ } catch (error) {
98
+ addError(errors, path, error.message);
99
+ }
100
+ }
101
+
102
+ function verifyFilesMap(files, errors) {
103
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
104
+ addError(errors, 'files', 'must be an object keyed by relative path');
105
+ return;
106
+ }
107
+
108
+ for (const [relPath, entry] of Object.entries(files)) {
109
+ verifyFileEntry(relPath, entry, errors);
110
+ }
111
+ }
112
+
113
+ function countJsonl(files, relPath) {
114
+ return Array.isArray(files?.[relPath]?.data) ? files[relPath].data.length : 0;
115
+ }
116
+
117
+ function countDirectoryFiles(files, prefix) {
118
+ return Object.keys(files || {}).filter((path) => path.startsWith(`${prefix}/`)).length;
119
+ }
120
+
121
+ function verifyRunExport(artifact, errors) {
122
+ if (typeof artifact.project_root !== 'string' || artifact.project_root.length === 0) {
123
+ addError(errors, 'project_root', 'must be a non-empty string');
124
+ }
125
+
126
+ if (!artifact.project || typeof artifact.project !== 'object' || Array.isArray(artifact.project)) {
127
+ addError(errors, 'project', 'must be an object');
128
+ } else {
129
+ const expectedProtocolMode = artifact.config?.protocol_mode
130
+ || artifact.state?.protocol_mode
131
+ || 'governed';
132
+ if (artifact.project.id !== artifact.config?.project?.id) {
133
+ addError(errors, 'project.id', 'must match config.project.id');
134
+ }
135
+ if (artifact.project.name !== artifact.config?.project?.name) {
136
+ addError(errors, 'project.name', 'must match config.project.name');
137
+ }
138
+ if (artifact.project.template !== (artifact.config?.template || 'generic')) {
139
+ addError(errors, 'project.template', 'must match config.template or implicit generic');
140
+ }
141
+ if (artifact.project.protocol_mode !== expectedProtocolMode) {
142
+ addError(errors, 'project.protocol_mode', 'must match exported protocol mode');
143
+ }
144
+ }
145
+
146
+ if (!artifact.summary || typeof artifact.summary !== 'object' || Array.isArray(artifact.summary)) {
147
+ addError(errors, 'summary', 'must be an object');
148
+ return;
149
+ }
150
+
151
+ if (!artifact.state || typeof artifact.state !== 'object' || Array.isArray(artifact.state)) {
152
+ addError(errors, 'state', 'must be an object');
153
+ return;
154
+ }
155
+
156
+ if (!isDeepStrictEqual(artifact.config, artifact.files?.['agentxchain.json']?.data)) {
157
+ addError(errors, 'config', 'must match files.agentxchain.json.data');
158
+ }
159
+
160
+ if (!isDeepStrictEqual(artifact.state, artifact.files?.['.agentxchain/state.json']?.data)) {
161
+ addError(errors, 'state', 'must match files..agentxchain/state.json.data');
162
+ }
163
+
164
+ const activeTurnIds = Object.keys(artifact.state.active_turns || {}).sort((a, b) => a.localeCompare(b, 'en'));
165
+ const retainedTurnIds = Object.keys(artifact.state.retained_turns || {}).sort((a, b) => a.localeCompare(b, 'en'));
166
+
167
+ if (!isDeepStrictEqual(artifact.summary.active_turn_ids, activeTurnIds)) {
168
+ addError(errors, 'summary.active_turn_ids', 'must match sorted state.active_turns keys');
169
+ }
170
+
171
+ if (!isDeepStrictEqual(artifact.summary.retained_turn_ids, retainedTurnIds)) {
172
+ addError(errors, 'summary.retained_turn_ids', 'must match sorted state.retained_turns keys');
173
+ }
174
+
175
+ if (artifact.summary.run_id !== (artifact.state.run_id || null)) {
176
+ addError(errors, 'summary.run_id', 'must match state.run_id');
177
+ }
178
+
179
+ if (artifact.summary.status !== (artifact.state.status || null)) {
180
+ addError(errors, 'summary.status', 'must match state.status');
181
+ }
182
+
183
+ if (artifact.summary.phase !== (artifact.state.phase || null)) {
184
+ addError(errors, 'summary.phase', 'must match state.phase');
185
+ }
186
+
187
+ const expectedHistoryEntries = countJsonl(artifact.files, '.agentxchain/history.jsonl');
188
+ const expectedDecisionEntries = countJsonl(artifact.files, '.agentxchain/decision-ledger.jsonl');
189
+ const expectedHookAuditEntries = countJsonl(artifact.files, '.agentxchain/hook-audit.jsonl');
190
+ const expectedNotificationAuditEntries = countJsonl(artifact.files, '.agentxchain/notification-audit.jsonl');
191
+ const expectedDispatchFiles = countDirectoryFiles(artifact.files, '.agentxchain/dispatch');
192
+ const expectedStagingFiles = countDirectoryFiles(artifact.files, '.agentxchain/staging');
193
+ const expectedIntakePresent = Object.keys(artifact.files).some((path) => path.startsWith('.agentxchain/intake/'));
194
+ const expectedCoordinatorPresent = Object.keys(artifact.files).some((path) => path.startsWith('.agentxchain/multirepo/'));
195
+
196
+ if (artifact.summary.history_entries !== expectedHistoryEntries) {
197
+ addError(errors, 'summary.history_entries', 'must match .agentxchain/history.jsonl entry count');
198
+ }
199
+ if (artifact.summary.decision_entries !== expectedDecisionEntries) {
200
+ addError(errors, 'summary.decision_entries', 'must match .agentxchain/decision-ledger.jsonl entry count');
201
+ }
202
+ if (artifact.summary.hook_audit_entries !== expectedHookAuditEntries) {
203
+ addError(errors, 'summary.hook_audit_entries', 'must match .agentxchain/hook-audit.jsonl entry count');
204
+ }
205
+ if (artifact.summary.notification_audit_entries !== expectedNotificationAuditEntries) {
206
+ addError(errors, 'summary.notification_audit_entries', 'must match .agentxchain/notification-audit.jsonl entry count');
207
+ }
208
+ if (artifact.summary.dispatch_artifact_files !== expectedDispatchFiles) {
209
+ addError(errors, 'summary.dispatch_artifact_files', 'must match .agentxchain/dispatch file count');
210
+ }
211
+ if (artifact.summary.staging_artifact_files !== expectedStagingFiles) {
212
+ addError(errors, 'summary.staging_artifact_files', 'must match .agentxchain/staging file count');
213
+ }
214
+ if (artifact.summary.intake_present !== expectedIntakePresent) {
215
+ addError(errors, 'summary.intake_present', 'must match intake file presence');
216
+ }
217
+ if (artifact.summary.coordinator_present !== expectedCoordinatorPresent) {
218
+ addError(errors, 'summary.coordinator_present', 'must match multirepo file presence');
219
+ }
220
+ }
221
+
222
+ function verifyCoordinatorExport(artifact, errors) {
223
+ if (typeof artifact.workspace_root !== 'string' || artifact.workspace_root.length === 0) {
224
+ addError(errors, 'workspace_root', 'must be a non-empty string');
225
+ }
226
+
227
+ if (!artifact.coordinator || typeof artifact.coordinator !== 'object' || Array.isArray(artifact.coordinator)) {
228
+ addError(errors, 'coordinator', 'must be an object');
229
+ } else {
230
+ if (artifact.coordinator.project_id !== (artifact.config?.project?.id || null)) {
231
+ addError(errors, 'coordinator.project_id', 'must match config.project.id');
232
+ }
233
+ if (artifact.coordinator.project_name !== (artifact.config?.project?.name || null)) {
234
+ addError(errors, 'coordinator.project_name', 'must match config.project.name');
235
+ }
236
+
237
+ const expectedRepoCount = Object.keys(artifact.config?.repos || {}).length;
238
+ const expectedWorkstreamCount = Object.keys(artifact.config?.workstreams || {}).length;
239
+ if (artifact.coordinator.repo_count !== expectedRepoCount) {
240
+ addError(errors, 'coordinator.repo_count', 'must match config.repos size');
241
+ }
242
+ if (artifact.coordinator.workstream_count !== expectedWorkstreamCount) {
243
+ addError(errors, 'coordinator.workstream_count', 'must match config.workstreams size');
244
+ }
245
+ }
246
+
247
+ if (!artifact.summary || typeof artifact.summary !== 'object' || Array.isArray(artifact.summary)) {
248
+ addError(errors, 'summary', 'must be an object');
249
+ } else {
250
+ const coordinatorState = artifact.files?.['.agentxchain/multirepo/state.json']?.data || null;
251
+ const expectedStatuses = {};
252
+ if (coordinatorState?.repo_runs && typeof coordinatorState.repo_runs === 'object') {
253
+ for (const [repoId, repoRun] of Object.entries(coordinatorState.repo_runs)) {
254
+ expectedStatuses[repoId] = repoRun.status || 'unknown';
255
+ }
256
+ }
257
+
258
+ const barriers = artifact.files?.['.agentxchain/multirepo/barriers.json']?.data;
259
+ const expectedBarrierCount = barriers && typeof barriers === 'object' && !Array.isArray(barriers)
260
+ ? Object.keys(barriers).length
261
+ : 0;
262
+
263
+ if (artifact.summary.super_run_id !== (coordinatorState?.super_run_id || null)) {
264
+ addError(errors, 'summary.super_run_id', 'must match coordinator state super_run_id');
265
+ }
266
+ if (artifact.summary.status !== (coordinatorState?.status || null)) {
267
+ addError(errors, 'summary.status', 'must match coordinator state status');
268
+ }
269
+ if (artifact.summary.phase !== (coordinatorState?.phase || null)) {
270
+ addError(errors, 'summary.phase', 'must match coordinator state phase');
271
+ }
272
+ if (!isDeepStrictEqual(artifact.summary.repo_run_statuses, expectedStatuses)) {
273
+ addError(errors, 'summary.repo_run_statuses', 'must match coordinator state repo run statuses');
274
+ }
275
+ if (artifact.summary.barrier_count !== expectedBarrierCount) {
276
+ addError(errors, 'summary.barrier_count', 'must match barriers.json object size');
277
+ }
278
+ if (artifact.summary.history_entries !== countJsonl(artifact.files, '.agentxchain/multirepo/history.jsonl')) {
279
+ addError(errors, 'summary.history_entries', 'must match multirepo history entry count');
280
+ }
281
+ if (artifact.summary.decision_entries !== countJsonl(artifact.files, '.agentxchain/multirepo/decision-ledger.jsonl')) {
282
+ addError(errors, 'summary.decision_entries', 'must match multirepo decision entry count');
283
+ }
284
+ }
285
+
286
+ if (!isDeepStrictEqual(artifact.config, artifact.files?.['agentxchain-multi.json']?.data)) {
287
+ addError(errors, 'config', 'must match files.agentxchain-multi.json.data');
288
+ }
289
+
290
+ if (!artifact.repos || typeof artifact.repos !== 'object' || Array.isArray(artifact.repos)) {
291
+ addError(errors, 'repos', 'must be an object');
292
+ return;
293
+ }
294
+
295
+ for (const [repoId, repoEntry] of Object.entries(artifact.repos)) {
296
+ const repoPath = `repos.${repoId}`;
297
+ if (!repoEntry || typeof repoEntry !== 'object' || Array.isArray(repoEntry)) {
298
+ addError(errors, repoPath, 'must be an object');
299
+ continue;
300
+ }
301
+ if (typeof repoEntry.path !== 'string' || repoEntry.path.length === 0) {
302
+ addError(errors, `${repoPath}.path`, 'must be a non-empty string');
303
+ }
304
+ if (typeof repoEntry.ok !== 'boolean') {
305
+ addError(errors, `${repoPath}.ok`, 'must be a boolean');
306
+ continue;
307
+ }
308
+ if (!repoEntry.ok) {
309
+ if (typeof repoEntry.error !== 'string' || repoEntry.error.length === 0) {
310
+ addError(errors, `${repoPath}.error`, 'must be a non-empty string when ok is false');
311
+ }
312
+ continue;
313
+ }
314
+ if (!repoEntry.export || typeof repoEntry.export !== 'object' || Array.isArray(repoEntry.export)) {
315
+ addError(errors, `${repoPath}.export`, 'must be an object when ok is true');
316
+ continue;
317
+ }
318
+ const nested = verifyExportArtifact(repoEntry.export);
319
+ for (const nestedError of nested.errors) {
320
+ addError(errors, `${repoPath}.export`, nestedError);
321
+ }
322
+ }
323
+ }
324
+
325
+ export function verifyExportArtifact(artifact) {
326
+ const errors = [];
327
+
328
+ if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
329
+ addError(errors, 'artifact', 'must be a JSON object');
330
+ return {
331
+ ok: false,
332
+ errors,
333
+ report: {
334
+ overall: 'fail',
335
+ schema_version: null,
336
+ export_kind: null,
337
+ file_count: 0,
338
+ errors,
339
+ },
340
+ };
341
+ }
342
+
343
+ if (artifact.schema_version !== SUPPORTED_EXPORT_SCHEMA_VERSION) {
344
+ addError(errors, 'schema_version', `must be "${SUPPORTED_EXPORT_SCHEMA_VERSION}"`);
345
+ }
346
+
347
+ if (typeof artifact.export_kind !== 'string') {
348
+ addError(errors, 'export_kind', 'must be a string');
349
+ }
350
+
351
+ if (typeof artifact.exported_at !== 'string' || Number.isNaN(Date.parse(artifact.exported_at))) {
352
+ addError(errors, 'exported_at', 'must be a valid ISO timestamp');
353
+ }
354
+
355
+ if (!artifact.config || typeof artifact.config !== 'object' || Array.isArray(artifact.config)) {
356
+ addError(errors, 'config', 'must be an object');
357
+ }
358
+
359
+ verifyFilesMap(artifact.files, errors);
360
+
361
+ if (artifact.export_kind === 'agentxchain_run_export') {
362
+ verifyRunExport(artifact, errors);
363
+ } else if (artifact.export_kind === 'agentxchain_coordinator_export') {
364
+ verifyCoordinatorExport(artifact, errors);
365
+ } else {
366
+ addError(errors, 'export_kind', `unsupported export kind "${artifact.export_kind}"`);
367
+ }
368
+
369
+ return {
370
+ ok: errors.length === 0,
371
+ errors,
372
+ report: {
373
+ overall: errors.length === 0 ? 'pass' : 'fail',
374
+ schema_version: artifact.schema_version || null,
375
+ export_kind: artifact.export_kind || null,
376
+ file_count: artifact.files && typeof artifact.files === 'object' && !Array.isArray(artifact.files)
377
+ ? Object.keys(artifact.files).length
378
+ : 0,
379
+ repo_count: artifact.repos && typeof artifact.repos === 'object' && !Array.isArray(artifact.repos)
380
+ ? Object.keys(artifact.repos).length
381
+ : 0,
382
+ errors,
383
+ },
384
+ };
385
+ }
386
+
387
+ export function loadExportArtifact(input, cwd = process.cwd()) {
388
+ const source = input || '-';
389
+ let raw;
390
+
391
+ try {
392
+ if (source === '-') {
393
+ if (process.stdin.isTTY) {
394
+ return {
395
+ ok: false,
396
+ input: 'stdin',
397
+ error: 'No export input provided. Pass --input <path> or pipe JSON on stdin.',
398
+ };
399
+ }
400
+ raw = readFileSync(0, 'utf8');
401
+ } else {
402
+ const resolved = resolve(cwd, source);
403
+ raw = readFileSync(resolved, 'utf8');
404
+ }
405
+ } catch (error) {
406
+ return {
407
+ ok: false,
408
+ input: source === '-' ? 'stdin' : resolve(cwd, source),
409
+ error: error.message,
410
+ };
411
+ }
412
+
413
+ try {
414
+ return {
415
+ ok: true,
416
+ input: source === '-' ? 'stdin' : resolve(cwd, source),
417
+ artifact: JSON.parse(raw),
418
+ };
419
+ } catch (error) {
420
+ return {
421
+ ok: false,
422
+ input: source === '-' ? 'stdin' : resolve(cwd, source),
423
+ error: `Invalid JSON export artifact: ${error.message}`,
424
+ };
425
+ }
426
+ }