agentxchain 2.155.73 → 2.156.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 (38) 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 +147 -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 +188 -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 +9 -0
  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/turn-checkpoint.js +21 -0
  35. package/src/lib/turn-result-validator.js +5 -0
  36. package/src/lib/validation.js +11 -3
  37. package/src/lib/verification-replay.js +125 -4
  38. package/src/lib/vision-reader.js +7 -2
@@ -87,6 +87,7 @@ import { auditCommand } from '../src/commands/audit.js';
87
87
  import { restoreCommand } from '../src/commands/restore.js';
88
88
  import { restartCommand } from '../src/commands/restart.js';
89
89
  import { reportCommand } from '../src/commands/report.js';
90
+ import { ciReportCommand } from '../src/commands/ci-report.js';
90
91
  import {
91
92
  pluginInstallCommand,
92
93
  pluginListCommand,
@@ -131,6 +132,7 @@ import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/c
131
132
  import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
132
133
  import { workflowKitDescribeCommand } from '../src/commands/workflow-kit.js';
133
134
  import { serveMcpCommand } from '../src/commands/serve-mcp.js';
135
+ import { serveCommand } from '../src/commands/serve.js';
134
136
 
135
137
  const __dirname = dirname(fileURLToPath(import.meta.url));
136
138
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -193,6 +195,13 @@ program
193
195
  .option('--format <format>', 'Output format: text, json, markdown, or html', 'text')
194
196
  .action(reportCommand);
195
197
 
198
+ program
199
+ .command('ci-report')
200
+ .description('Report governed run results in CI-native formats (GitHub Actions annotations, JUnit XML)')
201
+ .option('--input <path>', 'Export artifact path, or use current project if omitted')
202
+ .option('--format <format>', 'Output format: auto, github-actions, junit-xml, json', 'auto')
203
+ .action(ciReportCommand);
204
+
196
205
  program
197
206
  .command('start')
198
207
  .description('Launch legacy v3 agents in your IDE')
@@ -607,6 +616,7 @@ verifyCmd
607
616
  .description('Replay a staged turn\'s declared machine-evidence commands and compare exit codes')
608
617
  .option('-j, --json', 'Output as JSON')
609
618
  .option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
619
+ .option('--execute', 'Execute the agent-declared machine-evidence commands (operator opt-in; default is fail-safe — commands are not executed)')
610
620
  .action(verifyTurnCommand);
611
621
 
612
622
  verifyCmd
@@ -643,6 +653,7 @@ replayCmd
643
653
  .description('Replay an accepted turn\'s declared machine-evidence commands from history')
644
654
  .option('-j, --json', 'Output as JSON')
645
655
  .option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
656
+ .option('--execute', 'Execute the agent-declared machine-evidence commands (operator opt-in; default is fail-safe — commands are not executed)')
646
657
  .action(replayTurnCommand);
647
658
 
648
659
  replayCmd
@@ -749,6 +760,7 @@ program
749
760
  .option('--verbose', 'Stream local_cli subprocess output while the turn is running')
750
761
  .option('--stream', 'Stream live subprocess output to terminal (alias for --verbose)')
751
762
  .option('--auto-reject', 'Auto-reject and retry on validation failure')
763
+ .option('--no-checkpoint', 'Skip git checkpoint after acceptance (workspace stays dirty)')
752
764
  .action(stepCommand);
753
765
 
754
766
  program
@@ -803,6 +815,15 @@ program
803
815
  .option('--root <path>', 'Project root directory (default: cwd)')
804
816
  .action(serveMcpCommand);
805
817
 
818
+ program
819
+ .command('serve')
820
+ .description('Start hosted runner HTTP server')
821
+ .option('--port <number>', 'Server port', '4100')
822
+ .option('--host <address>', 'Bind address', '127.0.0.1')
823
+ .option('--project <path>', 'Project root')
824
+ .option('--projects <paths>', 'Additional project roots (comma-separated)')
825
+ .action(serveCommand);
826
+
806
827
  program
807
828
  .command('dashboard')
808
829
  .description('Open the live governance dashboard for the current repo/workspace')
@@ -1022,6 +1043,7 @@ intakeCmd
1022
1043
  .option('--intent <id>', 'Intent ID to approve')
1023
1044
  .option('--approver <name>', 'Name of the approving authority', 'operator')
1024
1045
  .option('--reason <text>', 'Reason for approval')
1046
+ .option('--force-scope', 'Bypass scope overlap guard')
1025
1047
  .option('-j, --json', 'Output as JSON')
1026
1048
  .action(intakeApproveCommand);
1027
1049
 
package/dashboard/app.js CHANGED
@@ -22,6 +22,10 @@ import { render as renderRunHistory } from './components/run-history.js';
22
22
  import { render as renderTimeouts } from './components/timeouts.js';
23
23
  import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
24
24
  import { render as renderWatch } from './components/watch.js';
25
+ import { render as renderOrgOverview } from './components/org-overview.js';
26
+ import { render as renderOrgRuns } from './components/org-runs.js';
27
+ import { render as renderOrgHistory } from './components/org-history.js';
28
+ import { render as renderOrgAuditTrail } from './components/org-audit-trail.js';
25
29
  import {
26
30
  buildLiveMeta,
27
31
  createLiveEventFromMessage,
@@ -29,6 +33,10 @@ import {
29
33
  } from './live-observer.js';
30
34
 
31
35
  const VIEWS = {
36
+ 'org-overview': { fetch: ['orgOverview'], render: renderOrgOverview },
37
+ 'org-runs': { fetch: ['orgRuns'], render: renderOrgRuns },
38
+ 'org-history': { fetch: ['orgHistory'], render: renderOrgHistory },
39
+ 'org-audit-trail': { fetch: ['orgAuditTrail'], render: renderOrgAuditTrail },
32
40
  timeline: { fetch: ['state', 'continuity', 'history', 'events', 'audit', 'annotations', 'connectors', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderTimeline },
33
41
  delegations: { fetch: ['state', 'history'], render: renderDelegations },
34
42
  ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger', 'repoDecisionsSummary'], render: renderLedger },
@@ -77,9 +85,15 @@ const API_MAP = {
77
85
  coordinatorTimeouts: '/api/coordinator/timeouts',
78
86
  gateActions: '/api/gate-actions',
79
87
  events: '/api/events?type=turn_conflicted&limit=10',
88
+ orgOverview: '/v1/org/overview',
89
+ orgRuns: '/v1/org/runs',
90
+ orgHistory: '/v1/org/history',
91
+ orgAuditTrail: '/v1/org/audit-trail',
80
92
  };
81
93
 
82
94
  const viewState = {
95
+ 'org-runs': { project: 'all', phase: 'all', status: 'all' },
96
+ 'org-audit-trail': { project: 'all', severity: 'all', source: 'all' },
83
97
  ledger: {
84
98
  agent: 'all',
85
99
  query: '',
@@ -193,6 +207,20 @@ function buildRenderData(viewName, data) {
193
207
  liveMeta,
194
208
  };
195
209
  }
210
+ if (viewName === 'org-runs') {
211
+ return {
212
+ ...data,
213
+ filter: viewState['org-runs'],
214
+ liveMeta,
215
+ };
216
+ }
217
+ if (viewName === 'org-audit-trail') {
218
+ return {
219
+ ...data,
220
+ filter: viewState['org-audit-trail'],
221
+ liveMeta,
222
+ };
223
+ }
196
224
  return {
197
225
  ...data,
198
226
  liveMeta,
@@ -455,6 +483,32 @@ document.addEventListener('change', (event) => {
455
483
  renderView('hooks', activeViewData);
456
484
  }
457
485
  }
486
+
487
+ if (view === 'org-runs') {
488
+ if (control === 'org-runs-project') {
489
+ viewState['org-runs'].project = event.target.value;
490
+ renderView('org-runs', activeViewData);
491
+ } else if (control === 'org-runs-phase') {
492
+ viewState['org-runs'].phase = event.target.value;
493
+ renderView('org-runs', activeViewData);
494
+ } else if (control === 'org-runs-status') {
495
+ viewState['org-runs'].status = event.target.value;
496
+ renderView('org-runs', activeViewData);
497
+ }
498
+ }
499
+
500
+ if (view === 'org-audit-trail') {
501
+ if (control === 'audit-trail-project') {
502
+ viewState['org-audit-trail'].project = event.target.value;
503
+ renderView('org-audit-trail', activeViewData);
504
+ } else if (control === 'audit-trail-severity') {
505
+ viewState['org-audit-trail'].severity = event.target.value;
506
+ renderView('org-audit-trail', activeViewData);
507
+ } else if (control === 'audit-trail-source') {
508
+ viewState['org-audit-trail'].source = event.target.value;
509
+ renderView('org-audit-trail', activeViewData);
510
+ }
511
+ }
458
512
  });
459
513
 
460
514
  // ── Turn expand toggle ──────────────────────────────────────────────────
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Org Audit Trail component — renders unified governance audit trail timeline.
3
+ *
4
+ * Pure render function: takes data from /v1/org/audit-trail, returns HTML string.
5
+ * Follows ledger.js and hooks.js patterns for filter bars and severity badges.
6
+ */
7
+
8
+ function esc(str) {
9
+ if (str == null) return '';
10
+ return String(str)
11
+ .replace(/&/g, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
16
+ }
17
+
18
+ function badge(label, color = 'var(--text-dim)') {
19
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
20
+ }
21
+
22
+ function severityBadge(severity) {
23
+ switch (severity) {
24
+ case 'high': return badge('HIGH', 'var(--red)');
25
+ case 'medium': return badge('MED', 'var(--yellow)');
26
+ case 'low': return badge('LOW', 'var(--text-dim)');
27
+ default: return badge(severity || '?', 'var(--text-dim)');
28
+ }
29
+ }
30
+
31
+ function sourceBadge(source) {
32
+ switch (source) {
33
+ case 'decision_ledger': return badge('ledger', 'var(--accent)');
34
+ case 'hook_audit': return badge('hook', '#38bdf8');
35
+ case 'events': return badge('event', 'var(--text-dim)');
36
+ default: return badge(source || '?', 'var(--text-dim)');
37
+ }
38
+ }
39
+
40
+ function formatTimestamp(iso) {
41
+ if (!iso) return '—';
42
+ try {
43
+ const d = new Date(iso);
44
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
45
+ + ', ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
46
+ } catch {
47
+ return esc(iso);
48
+ }
49
+ }
50
+
51
+ function severityBorderColor(severity) {
52
+ switch (severity) {
53
+ case 'high': return 'var(--red)';
54
+ case 'medium': return 'var(--yellow)';
55
+ default: return 'var(--text-dim)';
56
+ }
57
+ }
58
+
59
+ function renderFilterBar(events, filter) {
60
+ const projectSet = new Set(events.map(e => e.project_name).filter(Boolean));
61
+ const projectOptions = ['all', ...Array.from(projectSet).sort()];
62
+ const severityOptions = ['all', 'high', 'medium', 'low'];
63
+ const sourceOptions = ['all', 'decision_ledger', 'hook_audit', 'events'];
64
+
65
+ function options(list, selected) {
66
+ return list.map(v =>
67
+ `<option value="${esc(v)}"${v === selected ? ' selected' : ''}>${esc(v)}</option>`
68
+ ).join('');
69
+ }
70
+
71
+ return `<div class="filter-bar">
72
+ <label class="filter-control">
73
+ Project
74
+ <select data-view-control="audit-trail-project">
75
+ ${options(projectOptions, filter?.project || 'all')}
76
+ </select>
77
+ </label>
78
+ <label class="filter-control">
79
+ Severity
80
+ <select data-view-control="audit-trail-severity">
81
+ ${options(severityOptions, filter?.severity || 'all')}
82
+ </select>
83
+ </label>
84
+ <label class="filter-control">
85
+ Source
86
+ <select data-view-control="audit-trail-source">
87
+ ${options(sourceOptions, filter?.source || 'all')}
88
+ </select>
89
+ </label>
90
+ </div>`;
91
+ }
92
+
93
+ function applyFilter(events, filter) {
94
+ let filtered = events;
95
+ if (filter?.project && filter.project !== 'all') {
96
+ filtered = filtered.filter(e => e.project_name === filter.project);
97
+ }
98
+ if (filter?.severity && filter.severity !== 'all') {
99
+ filtered = filtered.filter(e => e.severity === filter.severity);
100
+ }
101
+ if (filter?.source && filter.source !== 'all') {
102
+ filtered = filtered.filter(e => e.source === filter.source);
103
+ }
104
+ return filtered;
105
+ }
106
+
107
+ function renderEventEntry(event) {
108
+ const borderColor = severityBorderColor(event.severity);
109
+ const contextParts = [];
110
+ if (event.phase) contextParts.push(`Phase: ${esc(event.phase)}`);
111
+ if (event.role) contextParts.push(`Role: ${esc(event.role)}`);
112
+ if (event.run_id) contextParts.push(`Run: ${esc(event.run_id.length > 16 ? event.run_id.slice(0, 16) + '…' : event.run_id)}`);
113
+ const contextLine = contextParts.length > 0
114
+ ? `<div style="font-size:11px;color:var(--text-dim);margin-top:2px">${contextParts.join(' ')}</div>`
115
+ : '';
116
+
117
+ return `<div class="turn-card" style="border-left:3px solid ${borderColor};padding:10px 14px">
118
+ <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
119
+ ${severityBadge(event.severity)}
120
+ ${badge(event.event_type, 'var(--text-dim)')}
121
+ <span style="font-size:12px;color:var(--accent)">${esc(event.project_name)}</span>
122
+ <span style="font-size:11px;color:var(--text-dim);margin-left:auto">${formatTimestamp(event.timestamp)}</span>
123
+ </div>
124
+ <div style="font-size:13px;color:var(--text);margin-top:4px">${esc(event.summary)}</div>
125
+ ${contextLine}
126
+ </div>`;
127
+ }
128
+
129
+ /**
130
+ * @param {{ orgAuditTrail: { data: object[], total: number }, liveMeta: object, filter?: object }} data
131
+ * @returns {string} HTML string
132
+ */
133
+ export function render(data) {
134
+ const auditData = data?.orgAuditTrail;
135
+ const events = auditData?.data || [];
136
+ const total = auditData?.total ?? events.length;
137
+ const filter = data?.filter || {};
138
+
139
+ if (!events.length && !filter.project && !filter.severity && !filter.source) {
140
+ return `<div class="placeholder">
141
+ <h2>Governance Audit Trail</h2>
142
+ <p>No governance events found across registered projects. Events appear when policy violations, hook blocks, escalations, or gate transitions occur.</p>
143
+ </div>`;
144
+ }
145
+
146
+ const filtered = applyFilter(events, filter);
147
+
148
+ let html = `<div class="section">`;
149
+ html += `<h3>Governance Audit Trail (${total} events)</h3>`;
150
+ html += renderFilterBar(events, filter);
151
+ html += `<div class="turn-list">`;
152
+ for (const event of filtered) {
153
+ html += renderEventEntry(event);
154
+ }
155
+ if (filtered.length === 0) {
156
+ html += `<div style="text-align:center;color:var(--text-dim);padding:20px">No events match filters</div>`;
157
+ }
158
+ html += `</div>`;
159
+ html += `</div>`;
160
+ return html;
161
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Org Run History component — renders full-fidelity cross-project run history.
3
+ *
4
+ * Pure render function: takes data from /v1/org/history, returns HTML string.
5
+ * Follows run-history.js patterns for badges, duration, cost formatting.
6
+ */
7
+
8
+ function esc(str) {
9
+ if (str == null) return '';
10
+ return String(str)
11
+ .replace(/&/g, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
16
+ }
17
+
18
+ function badge(label, color = 'var(--text-dim)') {
19
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
20
+ }
21
+
22
+ function statusBadge(status) {
23
+ switch (status) {
24
+ case 'completed': return badge('completed', 'var(--green)');
25
+ case 'blocked': return badge('blocked', 'var(--yellow)');
26
+ case 'failed': return badge('failed', 'var(--red)');
27
+ default: return badge(status || 'unknown', 'var(--text-dim)');
28
+ }
29
+ }
30
+
31
+ function formatDuration(ms) {
32
+ if (ms == null) return '—';
33
+ if (ms < 1000) return `${ms}ms`;
34
+ const seconds = Math.floor(ms / 1000);
35
+ if (seconds < 60) return `${seconds}s`;
36
+ const minutes = Math.floor(seconds / 60);
37
+ const remainingSeconds = seconds % 60;
38
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
39
+ const hours = Math.floor(minutes / 60);
40
+ const remainingMinutes = minutes % 60;
41
+ return `${hours}h ${remainingMinutes}m`;
42
+ }
43
+
44
+ function formatCost(usd) {
45
+ if (usd == null) return '—';
46
+ return `$${Number(usd).toFixed(2)}`;
47
+ }
48
+
49
+ function formatDate(iso) {
50
+ if (!iso) return '—';
51
+ try {
52
+ const d = new Date(iso);
53
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
54
+ + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
55
+ } catch {
56
+ return esc(iso);
57
+ }
58
+ }
59
+
60
+ function truncateId(id, len = 16) {
61
+ if (!id) return '—';
62
+ return id.length > len ? id.slice(0, len) + '…' : id;
63
+ }
64
+
65
+ function renderGateResults(gateResults) {
66
+ if (!gateResults || typeof gateResults !== 'object') return '';
67
+ const entries = Object.entries(gateResults);
68
+ if (entries.length === 0) return '';
69
+ return entries.map(([gate, status]) => {
70
+ const icon = status === 'passed' ? '✓' : 'pending';
71
+ const color = status === 'passed' ? 'var(--green)' : 'var(--text-dim)';
72
+ return `<span style="color:${color};margin-right:8px">${esc(gate)} ${icon}</span>`;
73
+ }).join('');
74
+ }
75
+
76
+ function renderRunCard(entry) {
77
+ const phases = Array.isArray(entry.phases_completed) && entry.phases_completed.length > 0
78
+ ? entry.phases_completed.map(p => esc(p)).join(' → ')
79
+ : '—';
80
+
81
+ const headline = entry.retrospective?.headline
82
+ ? `<div style="font-style:italic;color:var(--text-dim);margin-top:4px;font-size:12px">"${esc(entry.retrospective.headline)}"</div>`
83
+ : '';
84
+
85
+ const gates = renderGateResults(entry.gate_results);
86
+ const gatesRow = gates
87
+ ? `<div style="margin-top:4px;font-size:12px">Gates: ${gates}</div>`
88
+ : '';
89
+
90
+ const borderColor = entry.status === 'blocked' ? 'var(--yellow)' : entry.status === 'completed' ? 'var(--border)' : 'var(--border)';
91
+
92
+ return `<div class="turn-card" style="border-left:3px solid ${borderColor}">
93
+ <div class="turn-header">
94
+ <span class="mono" style="font-size:12px;color:var(--text-dim)" title="${esc(entry.run_id)}">${esc(truncateId(entry.run_id))}</span>
95
+ <span style="color:var(--accent);font-size:12px">${esc(entry.project_name)}</span>
96
+ ${statusBadge(entry.status)}
97
+ <span style="font-size:12px;color:var(--text-dim)">${formatDuration(entry.duration_ms)}</span>
98
+ <span style="font-size:12px;color:var(--text-dim)">${formatCost(entry.total_cost_usd)}</span>
99
+ </div>
100
+ <div style="font-size:12px;color:var(--text-dim);margin-top:4px">Phases: ${phases}</div>
101
+ ${headline}
102
+ ${gatesRow}
103
+ </div>`;
104
+ }
105
+
106
+ /**
107
+ * @param {{ orgHistory: { data: object[], total: number }, liveMeta: object }} data
108
+ * @returns {string} HTML string
109
+ */
110
+ export function render(data) {
111
+ const historyData = data?.orgHistory;
112
+ const records = historyData?.data || [];
113
+ const total = historyData?.total ?? records.length;
114
+
115
+ if (!records.length) {
116
+ return `<div class="placeholder">
117
+ <h2>Org Run History</h2>
118
+ <p>No cross-project run history available. Complete governed runs to see full-fidelity history.</p>
119
+ </div>`;
120
+ }
121
+
122
+ const completed = records.filter(r => r.status === 'completed').length;
123
+ const blocked = records.filter(r => r.status === 'blocked').length;
124
+
125
+ let html = `<div class="section">`;
126
+ html += `<h3>Org Run History</h3>`;
127
+ html += `<div class="run-header"><div class="run-meta">`;
128
+ html += `<span class="turn-count">Total: ${total} run${total !== 1 ? 's' : ''}</span>`;
129
+ if (completed > 0) html += badge(`${completed} completed`, 'var(--green)');
130
+ if (blocked > 0) html += badge(`${blocked} blocked`, 'var(--yellow)');
131
+ html += `</div></div>`;
132
+
133
+ html += `<div class="turn-list">`;
134
+ for (const entry of records) {
135
+ html += renderRunCard(entry);
136
+ }
137
+ html += `</div>`;
138
+ html += `</div>`;
139
+ return html;
140
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Org Overview component — renders organization-level metrics and project cards.
3
+ *
4
+ * Pure render function: takes data, returns HTML string.
5
+ * Follows the same pattern as timeline.js and run-history.js.
6
+ */
7
+
8
+ function esc(str) {
9
+ if (str == null) return '';
10
+ return String(str)
11
+ .replace(/&/g, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
16
+ }
17
+
18
+ function badge(label, color = 'var(--text-dim)') {
19
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
20
+ }
21
+
22
+ function statusBadge(status) {
23
+ switch (status) {
24
+ case 'active': return badge('active', 'var(--green)');
25
+ case 'blocked': return badge('blocked', 'var(--yellow)');
26
+ case 'completed': return badge('completed', 'var(--green)');
27
+ case 'failed': return badge('failed', 'var(--red)');
28
+ default: return badge(status || 'none', 'var(--text-dim)');
29
+ }
30
+ }
31
+
32
+ function phaseBadge(phase) {
33
+ switch (phase) {
34
+ case 'planning': return badge('planning', 'var(--accent)');
35
+ case 'implementation': return badge('implementation', '#38bdf8');
36
+ case 'qa': return badge('qa', 'var(--yellow)');
37
+ default: return badge(phase || 'none', 'var(--text-dim)');
38
+ }
39
+ }
40
+
41
+ function formatUsd(amount) {
42
+ return '$' + (amount || 0).toFixed(2);
43
+ }
44
+
45
+ function renderMetricCard(value, label) {
46
+ return `<div class="org-metric-card">
47
+ <div class="org-metric-value">${esc(String(value))}</div>
48
+ <div class="org-metric-label">${esc(label)}</div>
49
+ </div>`;
50
+ }
51
+
52
+ function renderProjectCard(project) {
53
+ const s = project.state || {};
54
+ const hasRun = s.run_id != null;
55
+ const gateList = (project.pending_gates || []).map(g => esc(g)).join(', ');
56
+
57
+ return `<div class="turn-card${s.status === 'active' ? ' active' : ''}">
58
+ <div class="turn-header">
59
+ <strong>${esc(project.name)}</strong>
60
+ ${s.status ? statusBadge(s.status) : ''}
61
+ ${s.phase ? phaseBadge(s.phase) : ''}
62
+ </div>
63
+ ${hasRun
64
+ ? `<div class="turn-status">
65
+ <span class="mono">${esc(s.run_id)}</span>
66
+ </div>
67
+ <div class="turn-detail">
68
+ <span class="detail-label">Turns:</span> ${s.active_turns || 0}
69
+ &nbsp;&middot;&nbsp;
70
+ <span class="detail-label">Cost:</span> ${formatUsd(s.budget_spent_usd)}
71
+ </div>`
72
+ : `<div class="turn-status">No active run</div>`
73
+ }
74
+ ${gateList
75
+ ? `<div class="turn-detail"><span class="detail-label">Pending gates:</span> ${gateList}</div>`
76
+ : ''
77
+ }
78
+ ${project.error
79
+ ? `<div class="turn-detail risks">${esc(project.error)}</div>`
80
+ : ''
81
+ }
82
+ </div>`;
83
+ }
84
+
85
+ /**
86
+ * @param {{ orgOverview: object, liveMeta: object }} data
87
+ * @returns {string} HTML string
88
+ */
89
+ export function render(data) {
90
+ const overview = data?.orgOverview?.data || data?.orgOverview || null;
91
+
92
+ if (!overview) {
93
+ return `<div class="placeholder">
94
+ <h2>Org Overview</h2>
95
+ <p>No organization data available. Start the hosted runner with <code>--projects</code> to register multiple projects.</p>
96
+ </div>`;
97
+ }
98
+
99
+ const projects = overview.projects || [];
100
+
101
+ const css = `<style>
102
+ .org-metrics {
103
+ display: grid;
104
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
105
+ gap: 12px;
106
+ margin-bottom: 20px;
107
+ }
108
+ .org-metric-card {
109
+ background: var(--surface);
110
+ border: 1px solid var(--border);
111
+ border-radius: 6px;
112
+ padding: 16px;
113
+ text-align: center;
114
+ }
115
+ .org-metric-value { font-size: 24px; font-weight: 700; color: var(--text); }
116
+ .org-metric-label { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
117
+ .org-projects-grid {
118
+ display: grid;
119
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
120
+ gap: 12px;
121
+ }
122
+ </style>`;
123
+
124
+ const metricsHtml = `<div class="org-metrics">
125
+ ${renderMetricCard(overview.total_projects, 'Projects')}
126
+ ${renderMetricCard(overview.active_runs, 'Active Runs')}
127
+ ${renderMetricCard(overview.pending_gates, 'Pending Gates')}
128
+ ${renderMetricCard(overview.total_decisions, 'Total Decisions')}
129
+ ${renderMetricCard(formatUsd(overview.total_cost_usd), 'Total Cost')}
130
+ </div>`;
131
+
132
+ const projectsHtml = projects.length > 0
133
+ ? `<div class="org-projects-grid">${projects.map(renderProjectCard).join('')}</div>`
134
+ : `<div class="placeholder compact"><p>No projects registered.</p></div>`;
135
+
136
+ return `${css}
137
+ <div class="section">
138
+ <h3>Org Overview</h3>
139
+ ${metricsHtml}
140
+ </div>
141
+ <div class="section">
142
+ <h3>Projects (${projects.length})</h3>
143
+ ${projectsHtml}
144
+ </div>`;
145
+ }