agentxchain 2.6.0 → 2.8.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,330 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+
6
+ import { interpolateHeaders } from './hook-runner.js';
7
+
8
+ export const NOTIFICATION_AUDIT_PATH = '.agentxchain/notification-audit.jsonl';
9
+ export const VALID_NOTIFICATION_EVENTS = [
10
+ 'run_blocked',
11
+ 'operator_escalation_raised',
12
+ 'escalation_resolved',
13
+ 'phase_transition_pending',
14
+ 'run_completion_pending',
15
+ 'run_completed',
16
+ ];
17
+
18
+ const NOTIFICATION_NAME_RE = /^[a-z0-9_-]+$/;
19
+ const MAX_NOTIFICATION_WEBHOOKS = 8;
20
+ const HEADER_VAR_RE = /\$\{([^}]+)\}/g;
21
+ const SIGKILL_GRACE_MS = 2000;
22
+ const MAX_STDERR_CAPTURE = 4096;
23
+
24
+ function collectMissingHeaderVars(headers, webhookEnv) {
25
+ if (!headers) return [];
26
+ const missing = [];
27
+ const mergedEnv = { ...process.env };
28
+
29
+ for (const [key, value] of Object.entries(webhookEnv || {})) {
30
+ if (typeof value === 'string') {
31
+ mergedEnv[key] = value;
32
+ }
33
+ }
34
+
35
+ for (const [headerName, value] of Object.entries(headers)) {
36
+ let match;
37
+ while ((match = HEADER_VAR_RE.exec(value)) !== null) {
38
+ if (mergedEnv[match[1]] === undefined) {
39
+ missing.push({ header: headerName, varName: match[1] });
40
+ }
41
+ }
42
+ HEADER_VAR_RE.lastIndex = 0;
43
+ }
44
+
45
+ return missing;
46
+ }
47
+
48
+ function appendAudit(root, entry) {
49
+ const filePath = join(root, NOTIFICATION_AUDIT_PATH);
50
+ if (!existsSync(dirname(filePath))) {
51
+ mkdirSync(dirname(filePath), { recursive: true });
52
+ }
53
+ appendFileSync(filePath, `${JSON.stringify(entry)}\n`);
54
+ }
55
+
56
+ function generateEventId() {
57
+ return `notif_${randomBytes(8).toString('hex')}`;
58
+ }
59
+
60
+ function executeWebhook(webhook, envelope) {
61
+ const startedAt = Date.now();
62
+ let headers;
63
+
64
+ try {
65
+ headers = interpolateHeaders(webhook.headers, webhook.env);
66
+ } catch (error) {
67
+ return {
68
+ delivered: false,
69
+ timed_out: false,
70
+ status_code: null,
71
+ duration_ms: Date.now() - startedAt,
72
+ message: String(error?.message || error),
73
+ stderr_excerpt: String(error?.message || error).slice(0, MAX_STDERR_CAPTURE),
74
+ };
75
+ }
76
+
77
+ headers['Content-Type'] = 'application/json';
78
+
79
+ const script = `
80
+ const url = process.argv[1];
81
+ const body = process.argv[2];
82
+ const headers = JSON.parse(process.argv[3]);
83
+ headers.Connection = 'close';
84
+ (async () => {
85
+ try {
86
+ const response = await fetch(url, {
87
+ method: 'POST',
88
+ headers,
89
+ body,
90
+ signal: AbortSignal.timeout(${webhook.timeout_ms}),
91
+ });
92
+ const text = await response.text();
93
+ process.stdout.write(JSON.stringify({ status: response.status, body: text }));
94
+ process.exit(0);
95
+ } catch (error) {
96
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
97
+ process.stderr.write('timeout');
98
+ process.exit(2);
99
+ }
100
+ process.stderr.write(error?.message || String(error));
101
+ process.exit(1);
102
+ }
103
+ })();
104
+ `;
105
+
106
+ const result = spawnSync(process.execPath, ['-e', script, webhook.url, JSON.stringify(envelope), JSON.stringify(headers)], {
107
+ timeout: webhook.timeout_ms + SIGKILL_GRACE_MS,
108
+ maxBuffer: 1024 * 1024,
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ env: { ...process.env, ...(webhook.env || {}) },
111
+ });
112
+
113
+ const durationMs = Date.now() - startedAt;
114
+ const timedOut = result.error?.code === 'ETIMEDOUT' || durationMs > webhook.timeout_ms;
115
+ const stdout = result.stdout ? result.stdout.toString('utf8') : '';
116
+ const stderr = result.stderr ? result.stderr.toString('utf8').slice(0, MAX_STDERR_CAPTURE) : '';
117
+
118
+ if (timedOut) {
119
+ return {
120
+ delivered: false,
121
+ timed_out: true,
122
+ status_code: null,
123
+ duration_ms: durationMs,
124
+ message: `Timed out after ${webhook.timeout_ms}ms`,
125
+ stderr_excerpt: stderr,
126
+ };
127
+ }
128
+
129
+ if (result.status !== 0) {
130
+ return {
131
+ delivered: false,
132
+ timed_out: false,
133
+ status_code: null,
134
+ duration_ms: durationMs,
135
+ message: stderr || `Webhook delivery failed with exit code ${result.status}`,
136
+ stderr_excerpt: stderr,
137
+ };
138
+ }
139
+
140
+ try {
141
+ const bridge = JSON.parse(stdout || '{}');
142
+ const statusCode = Number.isInteger(bridge.status) ? bridge.status : null;
143
+ const delivered = statusCode >= 200 && statusCode < 300;
144
+ return {
145
+ delivered,
146
+ timed_out: false,
147
+ status_code: statusCode,
148
+ duration_ms: durationMs,
149
+ message: delivered ? 'Delivered' : `Webhook returned HTTP ${statusCode}`,
150
+ stderr_excerpt: stderr,
151
+ };
152
+ } catch {
153
+ return {
154
+ delivered: false,
155
+ timed_out: false,
156
+ status_code: null,
157
+ duration_ms: durationMs,
158
+ message: 'Failed to parse webhook bridge response',
159
+ stderr_excerpt: stderr || stdout.slice(0, MAX_STDERR_CAPTURE),
160
+ };
161
+ }
162
+ }
163
+
164
+ export function validateNotificationsConfig(notifications) {
165
+ const errors = [];
166
+
167
+ if (!notifications || typeof notifications !== 'object' || Array.isArray(notifications)) {
168
+ errors.push('notifications must be an object');
169
+ return { ok: false, errors };
170
+ }
171
+
172
+ const allowedKeys = new Set(['webhooks']);
173
+ for (const key of Object.keys(notifications)) {
174
+ if (!allowedKeys.has(key)) {
175
+ errors.push(`notifications contains unknown field "${key}"`);
176
+ }
177
+ }
178
+
179
+ if (!('webhooks' in notifications)) {
180
+ return { ok: errors.length === 0, errors };
181
+ }
182
+
183
+ if (!Array.isArray(notifications.webhooks)) {
184
+ errors.push('notifications.webhooks must be an array');
185
+ return { ok: false, errors };
186
+ }
187
+
188
+ if (notifications.webhooks.length > MAX_NOTIFICATION_WEBHOOKS) {
189
+ errors.push(`notifications.webhooks: maximum ${MAX_NOTIFICATION_WEBHOOKS} webhooks`);
190
+ }
191
+
192
+ const names = new Set();
193
+ notifications.webhooks.forEach((webhook, index) => {
194
+ const label = `notifications.webhooks[${index}]`;
195
+
196
+ if (!webhook || typeof webhook !== 'object' || Array.isArray(webhook)) {
197
+ errors.push(`${label} must be an object`);
198
+ return;
199
+ }
200
+
201
+ if (typeof webhook.name !== 'string' || !webhook.name.trim()) {
202
+ errors.push(`${label}: name must be a non-empty string`);
203
+ } else if (!NOTIFICATION_NAME_RE.test(webhook.name)) {
204
+ errors.push(`${label}: name must match ^[a-z0-9_-]+$`);
205
+ } else if (names.has(webhook.name)) {
206
+ errors.push(`${label}: duplicate webhook name "${webhook.name}"`);
207
+ } else {
208
+ names.add(webhook.name);
209
+ }
210
+
211
+ if (typeof webhook.url !== 'string' || !webhook.url.trim()) {
212
+ errors.push(`${label}: url must be a non-empty string`);
213
+ } else if (!/^https?:\/\/.+/.test(webhook.url)) {
214
+ errors.push(`${label}: url must be a valid HTTP or HTTPS URL`);
215
+ }
216
+
217
+ if (!Array.isArray(webhook.events) || webhook.events.length === 0) {
218
+ errors.push(`${label}: events must be a non-empty array`);
219
+ } else {
220
+ for (const eventName of webhook.events) {
221
+ if (!VALID_NOTIFICATION_EVENTS.includes(eventName)) {
222
+ errors.push(
223
+ `${label}: events contains unknown event "${eventName}". Valid events: ${VALID_NOTIFICATION_EVENTS.join(', ')}`
224
+ );
225
+ }
226
+ }
227
+ }
228
+
229
+ if (!Number.isInteger(webhook.timeout_ms) || webhook.timeout_ms < 100 || webhook.timeout_ms > 30000) {
230
+ errors.push(`${label}: timeout_ms must be an integer between 100 and 30000`);
231
+ }
232
+
233
+ if ('headers' in webhook && webhook.headers !== undefined) {
234
+ if (!webhook.headers || typeof webhook.headers !== 'object' || Array.isArray(webhook.headers)) {
235
+ errors.push(`${label}: headers must be an object`);
236
+ } else {
237
+ for (const [key, value] of Object.entries(webhook.headers)) {
238
+ if (typeof value !== 'string') {
239
+ errors.push(`${label}: headers.${key} must be a string`);
240
+ }
241
+ }
242
+ const missingHeaderVars = collectMissingHeaderVars(webhook.headers, webhook.env);
243
+ if (missingHeaderVars.length > 0) {
244
+ errors.push(
245
+ `${label}: unresolved header env vars ${missingHeaderVars.map(({ header, varName }) => `${header}:${varName}`).join(', ')}`
246
+ );
247
+ }
248
+ }
249
+ }
250
+
251
+ if ('env' in webhook && webhook.env !== undefined) {
252
+ if (!webhook.env || typeof webhook.env !== 'object' || Array.isArray(webhook.env)) {
253
+ errors.push(`${label}: env must be an object`);
254
+ } else {
255
+ for (const [key, value] of Object.entries(webhook.env)) {
256
+ if (typeof value !== 'string') {
257
+ errors.push(`${label}: env.${key} must be a string`);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ });
263
+
264
+ return { ok: errors.length === 0, errors };
265
+ }
266
+
267
+ export function emitNotifications(root, config, state, eventType, payload = {}, turn = null) {
268
+ const webhooks = config?.notifications?.webhooks;
269
+ if (!Array.isArray(webhooks) || webhooks.length === 0) {
270
+ return { ok: true, results: [] };
271
+ }
272
+
273
+ if (!VALID_NOTIFICATION_EVENTS.includes(eventType)) {
274
+ return { ok: false, error: `Unknown notification event "${eventType}"`, results: [] };
275
+ }
276
+
277
+ const eventId = generateEventId();
278
+ const emittedAt = new Date().toISOString();
279
+ const envelope = {
280
+ schema_version: '0.1',
281
+ event_id: eventId,
282
+ event_type: eventType,
283
+ emitted_at: emittedAt,
284
+ project: {
285
+ id: config?.project?.id || 'unknown',
286
+ name: config?.project?.name || 'Unknown',
287
+ root,
288
+ },
289
+ run: {
290
+ run_id: state?.run_id || null,
291
+ status: state?.status || null,
292
+ phase: state?.phase || null,
293
+ },
294
+ turn: turn ? {
295
+ turn_id: turn.turn_id || null,
296
+ role_id: turn.assigned_role || turn.role_id || null,
297
+ attempt: Number.isInteger(turn.attempt) ? turn.attempt : null,
298
+ assigned_sequence: Number.isInteger(turn.assigned_sequence) ? turn.assigned_sequence : null,
299
+ } : null,
300
+ payload,
301
+ };
302
+
303
+ const results = [];
304
+ for (const webhook of webhooks) {
305
+ if (!webhook.events.includes(eventType)) {
306
+ continue;
307
+ }
308
+
309
+ const delivery = executeWebhook(webhook, envelope);
310
+ const auditEntry = {
311
+ event_id: eventId,
312
+ event_type: eventType,
313
+ notification_name: webhook.name,
314
+ transport: 'webhook',
315
+ delivered: delivery.delivered,
316
+ status_code: delivery.status_code,
317
+ timed_out: delivery.timed_out,
318
+ duration_ms: delivery.duration_ms,
319
+ message: delivery.message,
320
+ emitted_at: emittedAt,
321
+ };
322
+ if (delivery.stderr_excerpt) {
323
+ auditEntry.stderr_excerpt = delivery.stderr_excerpt;
324
+ }
325
+ appendAudit(root, auditEntry);
326
+ results.push(auditEntry);
327
+ }
328
+
329
+ return { ok: true, event_id: eventId, results };
330
+ }
@@ -0,0 +1,379 @@
1
+ import { verifyExportArtifact } from './export-verifier.js';
2
+
3
+ export const GOVERNANCE_REPORT_VERSION = '0.1';
4
+
5
+ function yesNo(value) {
6
+ return value ? 'yes' : 'no';
7
+ }
8
+
9
+ function summarizeBlockedOn(blockedOn) {
10
+ if (!blockedOn) return 'none';
11
+ if (typeof blockedOn === 'string') return blockedOn;
12
+ if (typeof blockedOn !== 'object' || Array.isArray(blockedOn)) return 'present';
13
+ if (typeof blockedOn.typed_reason === 'string' && blockedOn.typed_reason.length > 0) {
14
+ return blockedOn.typed_reason;
15
+ }
16
+ if (typeof blockedOn.reason === 'string' && blockedOn.reason.length > 0) {
17
+ return blockedOn.reason;
18
+ }
19
+ return 'present';
20
+ }
21
+
22
+ function summarizeBlockedState(run) {
23
+ const blockedReason = run?.blocked_reason;
24
+ if (blockedReason && typeof blockedReason === 'object' && !Array.isArray(blockedReason)) {
25
+ if (typeof blockedReason.recovery?.typed_reason === 'string' && blockedReason.recovery.typed_reason.length > 0) {
26
+ return blockedReason.recovery.typed_reason;
27
+ }
28
+ if (typeof blockedReason.category === 'string' && blockedReason.category.length > 0) {
29
+ return blockedReason.category;
30
+ }
31
+ }
32
+ return summarizeBlockedOn(run?.blocked_on);
33
+ }
34
+
35
+ function normalizeBudgetStatus(budgetStatus) {
36
+ if (!budgetStatus || typeof budgetStatus !== 'object' || Array.isArray(budgetStatus)) {
37
+ return null;
38
+ }
39
+
40
+ const normalized = {};
41
+ if (Number.isFinite(budgetStatus.spent_usd)) {
42
+ normalized.spent_usd = budgetStatus.spent_usd;
43
+ }
44
+ if (Number.isFinite(budgetStatus.remaining_usd)) {
45
+ normalized.remaining_usd = budgetStatus.remaining_usd;
46
+ }
47
+
48
+ return Object.keys(normalized).length > 0 ? normalized : null;
49
+ }
50
+
51
+ function formatUsd(value) {
52
+ return typeof value === 'number' ? `$${value.toFixed(2)}` : 'n/a';
53
+ }
54
+
55
+ function formatStatusCounts(statusCounts) {
56
+ const entries = Object.entries(statusCounts || {}).sort(([left], [right]) => left.localeCompare(right, 'en'));
57
+ if (entries.length === 0) return 'none';
58
+ return entries.map(([status, count]) => `${status}(${count})`).join(', ');
59
+ }
60
+
61
+ function deriveRepoStatusCounts(repoStatuses) {
62
+ const counts = {};
63
+ for (const status of Object.values(repoStatuses || {})) {
64
+ const key = status || 'unknown';
65
+ counts[key] = (counts[key] || 0) + 1;
66
+ }
67
+ return counts;
68
+ }
69
+
70
+ function buildRunSubject(artifact) {
71
+ const activeTurns = artifact.summary?.active_turn_ids || [];
72
+ const retainedTurns = artifact.summary?.retained_turn_ids || [];
73
+ const activeRoles = [...new Set(
74
+ Object.values(artifact.state?.active_turns || {})
75
+ .map((turn) => turn?.assigned_role)
76
+ .filter((role) => typeof role === 'string' && role.length > 0),
77
+ )].sort((a, b) => a.localeCompare(b, 'en'));
78
+
79
+ return {
80
+ kind: 'governed_run',
81
+ project: {
82
+ id: artifact.project?.id || null,
83
+ name: artifact.project?.name || null,
84
+ template: artifact.project?.template || 'generic',
85
+ protocol_mode: artifact.project?.protocol_mode || null,
86
+ schema_version: artifact.project?.schema_version || null,
87
+ },
88
+ run: {
89
+ run_id: artifact.summary?.run_id || null,
90
+ status: artifact.summary?.status || null,
91
+ phase: artifact.summary?.phase || null,
92
+ blocked_on: artifact.state?.blocked_on || null,
93
+ blocked_reason: artifact.state?.blocked_reason || null,
94
+ active_turn_count: activeTurns.length,
95
+ retained_turn_count: retainedTurns.length,
96
+ active_turn_ids: activeTurns,
97
+ retained_turn_ids: retainedTurns,
98
+ active_roles: activeRoles,
99
+ budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
100
+ },
101
+ artifacts: {
102
+ history_entries: artifact.summary?.history_entries || 0,
103
+ decision_entries: artifact.summary?.decision_entries || 0,
104
+ hook_audit_entries: artifact.summary?.hook_audit_entries || 0,
105
+ notification_audit_entries: artifact.summary?.notification_audit_entries || 0,
106
+ dispatch_artifact_files: artifact.summary?.dispatch_artifact_files || 0,
107
+ staging_artifact_files: artifact.summary?.staging_artifact_files || 0,
108
+ intake_present: Boolean(artifact.summary?.intake_present),
109
+ coordinator_present: Boolean(artifact.summary?.coordinator_present),
110
+ },
111
+ };
112
+ }
113
+
114
+ function buildCoordinatorSubject(artifact) {
115
+ const repoStatuses = artifact.summary?.repo_run_statuses || {};
116
+ const repoStatusCounts = deriveRepoStatusCounts(repoStatuses);
117
+ const repos = Object.entries(artifact.repos || {})
118
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))
119
+ .map(([repoId, repoEntry]) => ({
120
+ repo_id: repoId,
121
+ path: repoEntry?.path || null,
122
+ ok: Boolean(repoEntry?.ok),
123
+ status: repoEntry?.ok ? repoEntry.export?.summary?.status || null : null,
124
+ run_id: repoEntry?.ok ? repoEntry.export?.summary?.run_id || null : null,
125
+ phase: repoEntry?.ok ? repoEntry.export?.summary?.phase || null : null,
126
+ project_id: repoEntry?.ok ? repoEntry.export?.project?.id || null : null,
127
+ project_name: repoEntry?.ok ? repoEntry.export?.project?.name || null : null,
128
+ error: repoEntry?.ok ? null : repoEntry?.error || null,
129
+ }));
130
+
131
+ const repoErrorCount = repos.filter((repo) => !repo.ok).length;
132
+
133
+ return {
134
+ kind: 'coordinator_workspace',
135
+ coordinator: {
136
+ project_id: artifact.coordinator?.project_id || null,
137
+ project_name: artifact.coordinator?.project_name || null,
138
+ schema_version: artifact.coordinator?.schema_version || null,
139
+ repo_count: artifact.coordinator?.repo_count || 0,
140
+ workstream_count: artifact.coordinator?.workstream_count || 0,
141
+ },
142
+ run: {
143
+ super_run_id: artifact.summary?.super_run_id || null,
144
+ status: artifact.summary?.status || null,
145
+ phase: artifact.summary?.phase || null,
146
+ barrier_count: artifact.summary?.barrier_count || 0,
147
+ repo_status_counts: repoStatusCounts,
148
+ repo_ok_count: repos.length - repoErrorCount,
149
+ repo_error_count: repoErrorCount,
150
+ },
151
+ repos,
152
+ artifacts: {
153
+ history_entries: artifact.summary?.history_entries || 0,
154
+ decision_entries: artifact.summary?.decision_entries || 0,
155
+ },
156
+ };
157
+ }
158
+
159
+ export function buildGovernanceReport(artifact, { input = 'stdin', generatedAt = new Date().toISOString() } = {}) {
160
+ const verification = verifyExportArtifact(artifact);
161
+ if (!verification.ok) {
162
+ return {
163
+ ok: false,
164
+ exitCode: 1,
165
+ report: {
166
+ overall: 'fail',
167
+ input,
168
+ message: 'Cannot build governance report from invalid export artifact.',
169
+ verification: verification.report,
170
+ },
171
+ };
172
+ }
173
+
174
+ let subject;
175
+ if (artifact.export_kind === 'agentxchain_run_export') {
176
+ subject = buildRunSubject(artifact);
177
+ } else if (artifact.export_kind === 'agentxchain_coordinator_export') {
178
+ subject = buildCoordinatorSubject(artifact);
179
+ } else {
180
+ return {
181
+ ok: false,
182
+ exitCode: 1,
183
+ report: {
184
+ overall: 'fail',
185
+ input,
186
+ message: 'Cannot build governance report from invalid export artifact.',
187
+ verification: verification.report,
188
+ },
189
+ };
190
+ }
191
+
192
+ return {
193
+ ok: true,
194
+ exitCode: 0,
195
+ report: {
196
+ report_version: GOVERNANCE_REPORT_VERSION,
197
+ overall: 'pass',
198
+ generated_at: generatedAt,
199
+ input,
200
+ export_kind: artifact.export_kind,
201
+ verification: verification.report,
202
+ subject,
203
+ },
204
+ };
205
+ }
206
+
207
+ export function formatGovernanceReportText(report) {
208
+ if (report.overall === 'error') {
209
+ return [
210
+ 'AgentXchain Governance Report',
211
+ `Input: ${report.input}`,
212
+ 'Status: ERROR',
213
+ `Message: ${report.message}`,
214
+ ].join('\n');
215
+ }
216
+
217
+ if (report.overall === 'fail') {
218
+ return [
219
+ 'AgentXchain Governance Report',
220
+ `Input: ${report.input}`,
221
+ 'Verification: FAIL',
222
+ report.message,
223
+ 'Errors:',
224
+ ...(report.verification?.errors || []).map((error) => `- ${error}`),
225
+ ].join('\n');
226
+ }
227
+
228
+ if (report.subject.kind === 'governed_run') {
229
+ const { project, run, artifacts } = report.subject;
230
+ const lines = [
231
+ 'AgentXchain Governance Report',
232
+ `Input: ${report.input}`,
233
+ `Export kind: ${report.export_kind}`,
234
+ 'Verification: PASS',
235
+ `Project: ${project.name || 'unknown'} (${project.id || 'unknown'})`,
236
+ `Template: ${project.template}`,
237
+ `Protocol: ${project.protocol_mode || 'unknown'} (config schema ${project.schema_version || 'unknown'})`,
238
+ `Run: ${run.run_id || 'none'}`,
239
+ `Status: ${run.status || 'unknown'}`,
240
+ `Phase: ${run.phase || 'unknown'}`,
241
+ `Blocked on: ${summarizeBlockedState(run)}`,
242
+ `Active turns: ${run.active_turn_count}${run.active_turn_ids.length ? ` (${run.active_turn_ids.join(', ')})` : ''}`,
243
+ `Retained turns: ${run.retained_turn_count}${run.retained_turn_ids.length ? ` (${run.retained_turn_ids.join(', ')})` : ''}`,
244
+ `Active roles: ${run.active_roles.length ? run.active_roles.join(', ') : 'none'}`,
245
+ ];
246
+
247
+ if (run.budget_status) {
248
+ lines.push(
249
+ `Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`,
250
+ );
251
+ }
252
+
253
+ lines.push(
254
+ `History entries: ${artifacts.history_entries}`,
255
+ `Decision entries: ${artifacts.decision_entries}`,
256
+ `Hook audit entries: ${artifacts.hook_audit_entries}`,
257
+ `Notification audit entries: ${artifacts.notification_audit_entries}`,
258
+ `Dispatch files: ${artifacts.dispatch_artifact_files}`,
259
+ `Staging files: ${artifacts.staging_artifact_files}`,
260
+ `Intake artifacts: ${yesNo(artifacts.intake_present)}`,
261
+ `Coordinator artifacts: ${yesNo(artifacts.coordinator_present)}`,
262
+ );
263
+
264
+ return lines.join('\n');
265
+ }
266
+
267
+ const { coordinator, run, artifacts, repos } = report.subject;
268
+ return [
269
+ 'AgentXchain Governance Report',
270
+ `Input: ${report.input}`,
271
+ `Export kind: ${report.export_kind}`,
272
+ 'Verification: PASS',
273
+ `Workspace: ${coordinator.project_name || 'unknown'} (${coordinator.project_id || 'unknown'})`,
274
+ `Coordinator schema: ${coordinator.schema_version || 'unknown'}`,
275
+ `Super run: ${run.super_run_id || 'none'}`,
276
+ `Status: ${run.status || 'unknown'}`,
277
+ `Phase: ${run.phase || 'unknown'}`,
278
+ `Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
279
+ `Workstreams: ${coordinator.workstream_count}`,
280
+ `Barriers: ${run.barrier_count}`,
281
+ `Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
282
+ `History entries: ${artifacts.history_entries}`,
283
+ `Decision entries: ${artifacts.decision_entries}`,
284
+ 'Repo details:',
285
+ ...repos.map((repo) => repo.ok
286
+ ? `- ${repo.repo_id}: ok, status ${repo.status || 'unknown'}, run ${repo.run_id || 'none'}, path ${repo.path || 'unknown'}`
287
+ : `- ${repo.repo_id}: failed export, ${repo.error || 'unknown error'}, path ${repo.path || 'unknown'}`),
288
+ ].join('\n');
289
+ }
290
+
291
+ export function formatGovernanceReportMarkdown(report) {
292
+ if (report.overall === 'error') {
293
+ return [
294
+ '# AgentXchain Governance Report',
295
+ '',
296
+ `- Input: \`${report.input}\``,
297
+ '- Status: `error`',
298
+ `- Message: ${report.message}`,
299
+ ].join('\n');
300
+ }
301
+
302
+ if (report.overall === 'fail') {
303
+ return [
304
+ '# AgentXchain Governance Report',
305
+ '',
306
+ `- Input: \`${report.input}\``,
307
+ '- Verification: `fail`',
308
+ `- Message: ${report.message}`,
309
+ '',
310
+ '## Verification Errors',
311
+ '',
312
+ ...(report.verification?.errors || []).map((error) => `- ${error}`),
313
+ ].join('\n');
314
+ }
315
+
316
+ if (report.subject.kind === 'governed_run') {
317
+ const { project, run, artifacts } = report.subject;
318
+ const lines = [
319
+ '# AgentXchain Governance Report',
320
+ '',
321
+ `- Input: \`${report.input}\``,
322
+ `- Export kind: \`${report.export_kind}\``,
323
+ '- Verification: `pass`',
324
+ `- Project: ${project.name || 'unknown'} (\`${project.id || 'unknown'}\`)`,
325
+ `- Template: \`${project.template}\``,
326
+ `- Protocol: \`${project.protocol_mode || 'unknown'}\` (config schema \`${project.schema_version || 'unknown'}\`)`,
327
+ `- Run: \`${run.run_id || 'none'}\``,
328
+ `- Status: \`${run.status || 'unknown'}\``,
329
+ `- Phase: \`${run.phase || 'unknown'}\``,
330
+ `- Blocked on: \`${summarizeBlockedState(run)}\``,
331
+ `- Active turns: ${run.active_turn_count}${run.active_turn_ids.length ? ` (\`${run.active_turn_ids.join('`, `')}\`)` : ''}`,
332
+ `- Retained turns: ${run.retained_turn_count}${run.retained_turn_ids.length ? ` (\`${run.retained_turn_ids.join('`, `')}\`)` : ''}`,
333
+ `- Active roles: ${run.active_roles.length ? `\`${run.active_roles.join('`, `')}\`` : '`none`'}`,
334
+ ];
335
+
336
+ if (run.budget_status) {
337
+ lines.push(`- Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`);
338
+ }
339
+
340
+ lines.push(
341
+ `- History entries: ${artifacts.history_entries}`,
342
+ `- Decision entries: ${artifacts.decision_entries}`,
343
+ `- Hook audit entries: ${artifacts.hook_audit_entries}`,
344
+ `- Notification audit entries: ${artifacts.notification_audit_entries}`,
345
+ `- Dispatch files: ${artifacts.dispatch_artifact_files}`,
346
+ `- Staging files: ${artifacts.staging_artifact_files}`,
347
+ `- Intake artifacts: \`${yesNo(artifacts.intake_present)}\``,
348
+ `- Coordinator artifacts: \`${yesNo(artifacts.coordinator_present)}\``,
349
+ );
350
+
351
+ return lines.join('\n');
352
+ }
353
+
354
+ const { coordinator, run, artifacts, repos } = report.subject;
355
+ return [
356
+ '# AgentXchain Governance Report',
357
+ '',
358
+ `- Input: \`${report.input}\``,
359
+ `- Export kind: \`${report.export_kind}\``,
360
+ '- Verification: `pass`',
361
+ `- Workspace: ${coordinator.project_name || 'unknown'} (\`${coordinator.project_id || 'unknown'}\`)`,
362
+ `- Coordinator schema: \`${coordinator.schema_version || 'unknown'}\``,
363
+ `- Super run: \`${run.super_run_id || 'none'}\``,
364
+ `- Status: \`${run.status || 'unknown'}\``,
365
+ `- Phase: \`${run.phase || 'unknown'}\``,
366
+ `- Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
367
+ `- Workstreams: ${coordinator.workstream_count}`,
368
+ `- Barriers: ${run.barrier_count}`,
369
+ `- Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
370
+ `- History entries: ${artifacts.history_entries}`,
371
+ `- Decision entries: ${artifacts.decision_entries}`,
372
+ '',
373
+ '## Repo Details',
374
+ '',
375
+ ...repos.map((repo) => repo.ok
376
+ ? `- \`${repo.repo_id}\`: ok, status \`${repo.status || 'unknown'}\`, run \`${repo.run_id || 'none'}\`, path \`${repo.path || 'unknown'}\``
377
+ : `- \`${repo.repo_id}\`: failed export, ${repo.error || 'unknown error'}, path \`${repo.path || 'unknown'}\``),
378
+ ].join('\n');
379
+ }