agentxchain 2.155.73 → 2.157.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.
Files changed (39) hide show
  1. package/bin/agentxchain.js +22 -0
  2. package/dashboard/app.js +54 -0
  3. package/dashboard/components/org-audit-trail.js +161 -0
  4. package/dashboard/components/org-history.js +140 -0
  5. package/dashboard/components/org-overview.js +145 -0
  6. package/dashboard/components/org-runs.js +168 -0
  7. package/dashboard/index.html +4 -0
  8. package/package.json +2 -1
  9. package/src/commands/ci-report.js +80 -0
  10. package/src/commands/doctor.js +22 -1
  11. package/src/commands/intake-approve.js +1 -0
  12. package/src/commands/replay.js +1 -0
  13. package/src/commands/run.js +47 -0
  14. package/src/commands/serve.js +64 -0
  15. package/src/commands/step.js +16 -0
  16. package/src/commands/verify.js +1 -0
  17. package/src/lib/adapters/local-cli-adapter.js +184 -0
  18. package/src/lib/api/execution-worker.js +192 -0
  19. package/src/lib/api/hosted-runner.js +494 -0
  20. package/src/lib/api/job-queue.js +152 -0
  21. package/src/lib/api/org-state-aggregator.js +428 -0
  22. package/src/lib/api/project-registry.js +148 -0
  23. package/src/lib/api/protocol-bridge.js +476 -0
  24. package/src/lib/approval-policy.js +12 -0
  25. package/src/lib/ci-reporter.js +200 -0
  26. package/src/lib/claude-local-auth.js +75 -1
  27. package/src/lib/connector-probe.js +21 -0
  28. package/src/lib/continuous-run.js +75 -4
  29. package/src/lib/dashboard/bridge-server.js +10 -5
  30. package/src/lib/governed-state.js +1 -1
  31. package/src/lib/intake.js +32 -6
  32. package/src/lib/normalized-config.js +2 -0
  33. package/src/lib/scope-overlap.js +214 -0
  34. package/src/lib/stream-json-cost-parser.js +169 -0
  35. package/src/lib/turn-checkpoint.js +21 -0
  36. package/src/lib/turn-result-validator.js +25 -0
  37. package/src/lib/validation.js +11 -3
  38. package/src/lib/verification-replay.js +125 -4
  39. package/src/lib/vision-reader.js +7 -2
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Org State Aggregator — reads governed state from each registered project
3
+ * and returns aggregated cross-project views.
4
+ *
5
+ * Uses readJsonFile/readJsonlFile from state-reader.js for all file reads.
6
+ * Individual project read failures are isolated — never throws.
7
+ *
8
+ * @module org-state-aggregator
9
+ */
10
+
11
+ import { readJsonFile, readJsonlFile } from '../dashboard/state-reader.js';
12
+ import { join } from 'node:path';
13
+
14
+ /**
15
+ * Read state for a single project. Returns a normalized project state object.
16
+ * Never throws — returns a degraded object with error flag on failure.
17
+ */
18
+ function readProjectState(project) {
19
+ const axDir = join(project.root, '.agentxchain');
20
+
21
+ try {
22
+ const state = readJsonFile(axDir, 'state.json');
23
+ const decisions = readJsonlFile(axDir, 'decision-ledger.jsonl') || [];
24
+ const history = readJsonlFile(axDir, 'history.jsonl') || [];
25
+
26
+ if (!state) {
27
+ return {
28
+ id: project.id,
29
+ name: project.name,
30
+ root: project.root,
31
+ state: {
32
+ run_id: null,
33
+ status: null,
34
+ phase: null,
35
+ active_turns: 0,
36
+ budget_spent_usd: 0,
37
+ updated_at: null,
38
+ },
39
+ pending_gates: [],
40
+ decision_count: 0,
41
+ decisions,
42
+ history,
43
+ error: 'state_unreadable',
44
+ };
45
+ }
46
+
47
+ const activeTurns = state.active_turns
48
+ ? Object.keys(state.active_turns).length
49
+ : 0;
50
+
51
+ const budgetSpent = state.cost_tracker?.total_cost_usd || 0;
52
+
53
+ // Extract pending gates
54
+ const pendingGates = [];
55
+ if (state.gates && typeof state.gates === 'object') {
56
+ for (const [gateName, gateStatus] of Object.entries(state.gates)) {
57
+ if (gateStatus === 'pending' || gateStatus?.status === 'pending') {
58
+ pendingGates.push(gateName);
59
+ }
60
+ }
61
+ }
62
+
63
+ return {
64
+ id: project.id,
65
+ name: project.name,
66
+ root: project.root,
67
+ state: {
68
+ run_id: state.run_id || null,
69
+ status: state.status || null,
70
+ phase: state.phase || null,
71
+ active_turns: activeTurns,
72
+ budget_spent_usd: budgetSpent,
73
+ updated_at: state.updated_at || null,
74
+ },
75
+ pending_gates: pendingGates,
76
+ decision_count: decisions.length,
77
+ decisions,
78
+ history,
79
+ };
80
+ } catch {
81
+ return {
82
+ id: project.id,
83
+ name: project.name,
84
+ root: project.root,
85
+ state: {
86
+ run_id: null,
87
+ status: null,
88
+ phase: null,
89
+ active_turns: 0,
90
+ budget_spent_usd: 0,
91
+ updated_at: null,
92
+ },
93
+ pending_gates: [],
94
+ decision_count: 0,
95
+ decisions: [],
96
+ history: [],
97
+ error: 'state_unreadable',
98
+ };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Create an org state aggregator.
104
+ * @param {object} registry - project registry instance
105
+ * @returns {{ getOverview(), getRuns(query?), getDecisions(query?) }}
106
+ */
107
+ export function createOrgAggregator(registry) {
108
+
109
+ function getOverview() {
110
+ const projects = registry.list();
111
+ const projectStates = projects.map(readProjectState);
112
+
113
+ let activeRuns = 0;
114
+ let pendingGates = 0;
115
+ let totalDecisions = 0;
116
+ let totalCostUsd = 0;
117
+
118
+ for (const ps of projectStates) {
119
+ if (ps.state.status === 'active') activeRuns++;
120
+ pendingGates += ps.pending_gates.length;
121
+ totalDecisions += ps.decision_count;
122
+ totalCostUsd += ps.state.budget_spent_usd;
123
+ }
124
+
125
+ return {
126
+ total_projects: projects.length,
127
+ active_runs: activeRuns,
128
+ pending_gates: pendingGates,
129
+ total_decisions: totalDecisions,
130
+ total_cost_usd: Math.round(totalCostUsd * 100) / 100,
131
+ projects: projectStates.map(ps => {
132
+ const result = {
133
+ id: ps.id,
134
+ name: ps.name,
135
+ root: ps.root,
136
+ state: ps.state,
137
+ pending_gates: ps.pending_gates,
138
+ decision_count: ps.decision_count,
139
+ };
140
+ if (ps.error) result.error = ps.error;
141
+ return result;
142
+ }),
143
+ };
144
+ }
145
+
146
+ function getRuns(query) {
147
+ const projects = registry.list();
148
+ const runs = [];
149
+
150
+ for (const project of projects) {
151
+ const axDir = join(project.root, '.agentxchain');
152
+
153
+ try {
154
+ const state = readJsonFile(axDir, 'state.json');
155
+ const history = readJsonlFile(axDir, 'history.jsonl') || [];
156
+ const runHistory = readJsonlFile(axDir, 'run-history.jsonl') || [];
157
+
158
+ // Active run from state.json
159
+ if (state && state.run_id) {
160
+ runs.push({
161
+ project_id: project.id,
162
+ project_name: project.name,
163
+ run_id: state.run_id,
164
+ status: state.status || 'unknown',
165
+ phase: state.phase || 'unknown',
166
+ turns_completed: history.length,
167
+ cost_usd: state.cost_tracker?.total_cost_usd || 0,
168
+ started_at: state.created_at || null,
169
+ updated_at: state.updated_at || null,
170
+ });
171
+ }
172
+
173
+ // Historical runs
174
+ for (const entry of runHistory) {
175
+ if (entry.run_id === state?.run_id) continue; // skip active run
176
+ runs.push({
177
+ project_id: project.id,
178
+ project_name: project.name,
179
+ run_id: entry.run_id || 'unknown',
180
+ status: entry.status || 'completed',
181
+ phase: entry.phase || entry.final_phase || 'unknown',
182
+ turns_completed: entry.turns_completed || 0,
183
+ cost_usd: entry.cost_usd || 0,
184
+ started_at: entry.started_at || null,
185
+ updated_at: entry.completed_at || entry.updated_at || null,
186
+ });
187
+ }
188
+ } catch {
189
+ // Skip unreadable projects
190
+ }
191
+ }
192
+
193
+ // Apply filters
194
+ let filtered = runs;
195
+ if (query?.project) {
196
+ filtered = filtered.filter(r => r.project_id === query.project);
197
+ }
198
+ if (query?.phase) {
199
+ filtered = filtered.filter(r => r.phase === query.phase);
200
+ }
201
+ if (query?.status) {
202
+ filtered = filtered.filter(r => r.status === query.status);
203
+ }
204
+
205
+ // Sort by updated_at descending
206
+ filtered.sort((a, b) => {
207
+ const ta = a.updated_at || '';
208
+ const tb = b.updated_at || '';
209
+ return tb.localeCompare(ta);
210
+ });
211
+
212
+ const limit = parseInt(query?.limit, 10) || 50;
213
+ return { data: filtered.slice(0, limit) };
214
+ }
215
+
216
+ function getDecisions(query) {
217
+ const projects = registry.list();
218
+ const decisions = [];
219
+
220
+ for (const project of projects) {
221
+ const axDir = join(project.root, '.agentxchain');
222
+
223
+ try {
224
+ const entries = readJsonlFile(axDir, 'decision-ledger.jsonl') || [];
225
+ for (const entry of entries) {
226
+ decisions.push({
227
+ project_id: project.id,
228
+ project_name: project.name,
229
+ id: entry.id || entry.decision_id || 'unknown',
230
+ phase: entry.phase || 'unknown',
231
+ role: entry.role || 'unknown',
232
+ runtime_id: entry.runtime_id || null,
233
+ category: entry.category || 'unknown',
234
+ statement: entry.statement || '',
235
+ rationale: entry.rationale || '',
236
+ });
237
+ }
238
+ } catch {
239
+ // Skip unreadable projects
240
+ }
241
+ }
242
+
243
+ // Apply filters
244
+ let filtered = decisions;
245
+ if (query?.project) {
246
+ filtered = filtered.filter(d => d.project_id === query.project);
247
+ }
248
+ if (query?.phase) {
249
+ filtered = filtered.filter(d => d.phase === query.phase);
250
+ }
251
+ if (query?.role) {
252
+ filtered = filtered.filter(d => d.role === query.role);
253
+ }
254
+
255
+ const limit = parseInt(query?.limit, 10) || 100;
256
+ return { data: filtered.slice(0, limit) };
257
+ }
258
+
259
+ // ── Run History (full-fidelity) ──────────────────────────────────────────
260
+
261
+ function getRunHistory(query) {
262
+ const projects = registry.list();
263
+ const records = [];
264
+
265
+ for (const project of projects) {
266
+ const axDir = join(project.root, '.agentxchain');
267
+ try {
268
+ const entries = readJsonlFile(axDir, 'run-history.jsonl') || [];
269
+ for (const entry of entries) {
270
+ records.push({
271
+ ...entry,
272
+ project_id: project.id,
273
+ project_name: project.name,
274
+ });
275
+ }
276
+ } catch { /* skip unreadable */ }
277
+ }
278
+
279
+ let filtered = records;
280
+ if (query?.project) filtered = filtered.filter(r => r.project_id === query.project);
281
+ if (query?.status) filtered = filtered.filter(r => r.status === query.status);
282
+
283
+ filtered.sort((a, b) => {
284
+ const ta = a.completed_at || a.recorded_at || '';
285
+ const tb = b.completed_at || b.recorded_at || '';
286
+ return tb.localeCompare(ta);
287
+ });
288
+
289
+ const limit = parseInt(query?.limit, 10) || 50;
290
+ const offset = parseInt(query?.offset, 10) || 0;
291
+ return { data: filtered.slice(offset, offset + limit), total: filtered.length };
292
+ }
293
+
294
+ // ── Governance Audit Trail ─────────────────────────────────────────────
295
+
296
+ const GOVERNANCE_EVENT_TYPES = new Set([
297
+ 'escalation_raised', 'escalation_resolved',
298
+ 'gate_pending', 'gate_approved', 'gate_failed',
299
+ 'run_blocked', 'human_escalation_raised',
300
+ 'budget_exceeded_warn',
301
+ ]);
302
+
303
+ const GOVERNANCE_DECISION_TYPES = new Set([
304
+ 'policy_escalation', 'conflict_detected', 'conflict_rejected',
305
+ 'conflict_resolution_selected', 'operator_escalated',
306
+ 'escalation_resolved', 'timeout_turn_level',
307
+ 'timeout_phase_level', 'timeout_run_level',
308
+ ]);
309
+
310
+ function classifySeverity(eventType) {
311
+ if (['run_blocked', 'timeout_run_level', 'policy_escalation', 'hook_block'].includes(eventType)) return 'high';
312
+ if (['escalation_raised', 'human_escalation_raised', 'gate_failed', 'conflict_detected', 'timeout_phase_level', 'timeout_turn_level', 'hook_warn', 'budget_exceeded_warn'].includes(eventType)) return 'medium';
313
+ return 'low';
314
+ }
315
+
316
+ function buildDecisionSummary(entry) {
317
+ const type = entry.decision || entry.type || 'unknown';
318
+ if (type === 'policy_escalation') return `Policy violation: ${(entry.violations || []).map(v => v.message).join('; ') || 'unknown'}`;
319
+ if (type === 'conflict_detected') return `File conflict detected: ${(entry.conflict?.conflicting_files || []).join(', ')}`;
320
+ if (type.startsWith('timeout_')) return `Timeout (${entry.scope || type}): ${entry.elapsed_minutes || '?'}m elapsed vs ${entry.limit_minutes || '?'}m limit`;
321
+ if (type === 'operator_escalated') return `Operator escalated: ${entry.escalation?.reason || entry.blocked_on || 'unknown'}`;
322
+ if (type === 'escalation_resolved') return `Escalation resolved: ${entry.resolution || 'resolved'}`;
323
+ if (type.startsWith('conflict_')) return `Conflict ${type.replace('conflict_', '')}: ${(entry.conflict?.conflicting_files || []).join(', ')}`;
324
+ return type;
325
+ }
326
+
327
+ function buildEventSummary(entry) {
328
+ const type = entry.event_type || 'unknown';
329
+ const payload = entry.payload || {};
330
+ if (type === 'escalation_raised') return `Escalation: ${payload.reason || payload.blocked_on || 'unknown'}`;
331
+ if (type === 'gate_pending') return `Gate pending: ${payload.gate_id || 'unknown'}`;
332
+ if (type === 'gate_approved') return `Gate approved: ${payload.gate_id || 'unknown'}`;
333
+ if (type === 'gate_failed') return `Gate failed: ${payload.gate_id || 'unknown'}`;
334
+ if (type === 'run_blocked') return `Run blocked: ${payload.reason || payload.blocked_on || 'unknown'}`;
335
+ if (type === 'human_escalation_raised') return `Human escalation: ${payload.reason || 'operator required'}`;
336
+ if (type === 'budget_exceeded_warn') return `Budget warning: ${payload.message || 'approaching limit'}`;
337
+ if (type === 'escalation_resolved') return `Escalation resolved`;
338
+ return type;
339
+ }
340
+
341
+ function getAuditTrail(query) {
342
+ const projects = registry.list();
343
+ const events = [];
344
+
345
+ for (const project of projects) {
346
+ const axDir = join(project.root, '.agentxchain');
347
+
348
+ try {
349
+ const ledger = readJsonlFile(axDir, 'decision-ledger.jsonl') || [];
350
+ for (const entry of ledger) {
351
+ const decisionType = entry.decision || entry.type || '';
352
+ if (!GOVERNANCE_DECISION_TYPES.has(decisionType)) continue;
353
+ events.push({
354
+ timestamp: entry.timestamp || null,
355
+ event_type: decisionType,
356
+ severity: classifySeverity(decisionType),
357
+ source: 'decision_ledger',
358
+ project_id: project.id,
359
+ project_name: project.name,
360
+ run_id: entry.run_id || null,
361
+ phase: entry.phase || null,
362
+ role: entry.role || null,
363
+ summary: buildDecisionSummary(entry),
364
+ detail: entry,
365
+ });
366
+ }
367
+ } catch { /* skip */ }
368
+
369
+ try {
370
+ const hooks = readJsonlFile(axDir, 'hook-audit.jsonl') || [];
371
+ for (const entry of hooks) {
372
+ if (entry.verdict !== 'block' && entry.verdict !== 'warn') continue;
373
+ events.push({
374
+ timestamp: entry.timestamp || null,
375
+ event_type: `hook_${entry.verdict}`,
376
+ severity: entry.verdict === 'block' ? 'high' : 'medium',
377
+ source: 'hook_audit',
378
+ project_id: project.id,
379
+ project_name: project.name,
380
+ run_id: entry.run_id || null,
381
+ phase: entry.phase || null,
382
+ role: null,
383
+ summary: `Hook "${entry.hook_name || 'unknown'}" ${entry.verdict}: ${entry.message || entry.event || ''}`,
384
+ detail: entry,
385
+ });
386
+ }
387
+ } catch { /* skip */ }
388
+
389
+ try {
390
+ const evts = readJsonlFile(axDir, 'events.jsonl') || [];
391
+ for (const entry of evts) {
392
+ if (!GOVERNANCE_EVENT_TYPES.has(entry.event_type)) continue;
393
+ events.push({
394
+ timestamp: entry.timestamp || null,
395
+ event_type: entry.event_type,
396
+ severity: classifySeverity(entry.event_type),
397
+ source: 'events',
398
+ project_id: project.id,
399
+ project_name: project.name,
400
+ run_id: entry.run_id || null,
401
+ phase: entry.phase || null,
402
+ role: entry.turn?.role_id || null,
403
+ summary: buildEventSummary(entry),
404
+ detail: entry,
405
+ });
406
+ }
407
+ } catch { /* skip */ }
408
+ }
409
+
410
+ let filtered = events;
411
+ if (query?.project) filtered = filtered.filter(e => e.project_id === query.project);
412
+ if (query?.severity) filtered = filtered.filter(e => e.severity === query.severity);
413
+ if (query?.event_type) filtered = filtered.filter(e => e.event_type === query.event_type);
414
+ if (query?.source) filtered = filtered.filter(e => e.source === query.source);
415
+
416
+ filtered.sort((a, b) => {
417
+ const ta = a.timestamp || '';
418
+ const tb = b.timestamp || '';
419
+ return tb.localeCompare(ta);
420
+ });
421
+
422
+ const limit = parseInt(query?.limit, 10) || 50;
423
+ const offset = parseInt(query?.offset, 10) || 0;
424
+ return { data: filtered.slice(offset, offset + limit), total: filtered.length };
425
+ }
426
+
427
+ return { getOverview, getRuns, getDecisions, getRunHistory, getAuditTrail };
428
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Project Registry — file-backed registry mapping project IDs to root directories.
3
+ *
4
+ * Persists to <primaryRoot>/.agentxchain/org-registry.json.
5
+ * The primary project is always registered and cannot be unregistered.
6
+ *
7
+ * @module project-registry
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
+ import { join, resolve, basename } from 'node:path';
12
+ import { createHash } from 'node:crypto';
13
+
14
+ /**
15
+ * Derive a deterministic project ID from a normalized absolute path.
16
+ * Uses first 12 hex chars of SHA-256.
17
+ */
18
+ function deriveProjectId(absolutePath) {
19
+ const normalized = resolve(absolutePath);
20
+ return createHash('sha256').update(normalized).digest('hex').slice(0, 12);
21
+ }
22
+
23
+ /**
24
+ * Create a project registry persisted in the primary project's .agentxchain/ directory.
25
+ * @param {string} primaryRoot - primary project root (always registered)
26
+ * @returns {ProjectRegistry}
27
+ */
28
+ export function createProjectRegistry(primaryRoot) {
29
+ const normalizedPrimary = resolve(primaryRoot);
30
+ const axDir = join(normalizedPrimary, '.agentxchain');
31
+ const registryPath = join(axDir, 'org-registry.json');
32
+
33
+ /** @type {Map<string, RegistryEntry>} */
34
+ const entries = new Map();
35
+
36
+ // Always register the primary project
37
+ const primaryId = deriveProjectId(normalizedPrimary);
38
+ entries.set(primaryId, {
39
+ id: primaryId,
40
+ name: basename(normalizedPrimary),
41
+ root: normalizedPrimary,
42
+ is_primary: true,
43
+ registered_at: Date.now(),
44
+ });
45
+
46
+ // Try to load existing registry from disk
47
+ load();
48
+
49
+ function load() {
50
+ try {
51
+ if (!existsSync(registryPath)) return;
52
+ const raw = readFileSync(registryPath, 'utf8').trim();
53
+ if (!raw) return;
54
+ const data = JSON.parse(raw);
55
+ if (data.version !== 1 || !Array.isArray(data.projects)) return;
56
+ for (const proj of data.projects) {
57
+ if (!proj.id || !proj.root) continue;
58
+ entries.set(proj.id, {
59
+ id: proj.id,
60
+ name: proj.name || basename(proj.root),
61
+ root: proj.root,
62
+ is_primary: proj.id === primaryId,
63
+ registered_at: proj.registered_at || Date.now(),
64
+ });
65
+ }
66
+ // Ensure primary is always present
67
+ if (!entries.has(primaryId)) {
68
+ entries.set(primaryId, {
69
+ id: primaryId,
70
+ name: basename(normalizedPrimary),
71
+ root: normalizedPrimary,
72
+ is_primary: true,
73
+ registered_at: Date.now(),
74
+ });
75
+ }
76
+ } catch {
77
+ // Graceful: corrupt file → keep primary-only registry
78
+ }
79
+ }
80
+
81
+ function save() {
82
+ try {
83
+ if (!existsSync(axDir)) {
84
+ mkdirSync(axDir, { recursive: true });
85
+ }
86
+ const data = {
87
+ version: 1,
88
+ projects: Array.from(entries.values()),
89
+ };
90
+ writeFileSync(registryPath, JSON.stringify(data, null, 2));
91
+ } catch {
92
+ // Best-effort persistence
93
+ }
94
+ }
95
+
96
+ function register(root, name) {
97
+ const normalizedRoot = resolve(root);
98
+
99
+ // Validate: must be a governed project
100
+ if (!existsSync(join(normalizedRoot, 'agentxchain.json'))) {
101
+ throw new Error(`no agentxchain.json found at ${normalizedRoot}`);
102
+ }
103
+
104
+ const id = deriveProjectId(normalizedRoot);
105
+ const existing = entries.get(id);
106
+
107
+ if (existing) {
108
+ // Idempotent: update name only
109
+ if (name) existing.name = name;
110
+ save();
111
+ return { ...existing };
112
+ }
113
+
114
+ const entry = {
115
+ id,
116
+ name: name || basename(normalizedRoot),
117
+ root: normalizedRoot,
118
+ is_primary: id === primaryId,
119
+ registered_at: Date.now(),
120
+ };
121
+ entries.set(id, entry);
122
+ save();
123
+ return { ...entry };
124
+ }
125
+
126
+ function unregister(projectId) {
127
+ const entry = entries.get(projectId);
128
+ if (!entry) return false;
129
+ // Primary project cannot be unregistered
130
+ if (entry.is_primary) return false;
131
+ entries.delete(projectId);
132
+ save();
133
+ return true;
134
+ }
135
+
136
+ function list() {
137
+ return Array.from(entries.values())
138
+ .sort((a, b) => a.name.localeCompare(b.name))
139
+ .map(e => ({ ...e }));
140
+ }
141
+
142
+ function get(projectId) {
143
+ const entry = entries.get(projectId);
144
+ return entry ? { ...entry } : null;
145
+ }
146
+
147
+ return { register, unregister, list, get, save, load };
148
+ }