@jmanuelcorral/openteam 0.1.41 → 0.2.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 (117) hide show
  1. package/.opencode/openteam.example.json +6 -1
  2. package/README.es.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/capabilities/types.d.ts +4 -4
  5. package/dist/classifier/types.d.ts +3 -3
  6. package/dist/cli.js +430 -36
  7. package/dist/commands/doctor.d.ts +2 -0
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/graph.d.ts +36 -2
  10. package/dist/commands/graph.d.ts.map +1 -1
  11. package/dist/config/graphFeatureGate.d.ts +96 -0
  12. package/dist/config/graphFeatureGate.d.ts.map +1 -0
  13. package/dist/config/migrations.d.ts.map +1 -1
  14. package/dist/config/resolve.d.ts +20 -1
  15. package/dist/config/resolve.d.ts.map +1 -1
  16. package/dist/config/schema.d.ts +40 -19
  17. package/dist/config/schema.d.ts.map +1 -1
  18. package/dist/console/assets.d.ts +9 -1
  19. package/dist/console/assets.d.ts.map +1 -1
  20. package/dist/console/opencodeClient.d.ts +6 -0
  21. package/dist/console/opencodeClient.d.ts.map +1 -1
  22. package/dist/console/protocol.d.ts +8 -0
  23. package/dist/console/protocol.d.ts.map +1 -1
  24. package/dist/console/render.d.ts +25 -0
  25. package/dist/console/render.d.ts.map +1 -1
  26. package/dist/context/redaction.d.ts +23 -0
  27. package/dist/context/redaction.d.ts.map +1 -1
  28. package/dist/context/schema.d.ts +3 -3
  29. package/dist/contract/opencode.d.ts +2 -2
  30. package/dist/contract/opencode.d.ts.map +1 -1
  31. package/dist/graph/certificate.d.ts +505 -0
  32. package/dist/graph/certificate.d.ts.map +1 -0
  33. package/dist/graph/ids.d.ts +9 -0
  34. package/dist/graph/ids.d.ts.map +1 -1
  35. package/dist/graph/privacyPolicy.d.ts.map +1 -1
  36. package/dist/graph/reducer.d.ts.map +1 -1
  37. package/dist/graph/replay.d.ts.map +1 -1
  38. package/dist/graph/reviewerPolicy.d.ts.map +1 -1
  39. package/dist/graph/schema.d.ts +33 -33
  40. package/dist/graph/schema.d.ts.map +1 -1
  41. package/dist/graph/soakLedger.d.ts +144 -0
  42. package/dist/graph/soakLedger.d.ts.map +1 -0
  43. package/dist/graph/types.d.ts +2 -0
  44. package/dist/graph/types.d.ts.map +1 -1
  45. package/dist/graph/validate.d.ts.map +1 -1
  46. package/dist/index.d.ts +47 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2351 -180
  49. package/dist/local/registry.d.ts.map +1 -1
  50. package/dist/memory/types.d.ts +2 -2
  51. package/dist/orchestrator/graphFacade.d.ts +39 -0
  52. package/dist/orchestrator/graphFacade.d.ts.map +1 -0
  53. package/dist/orchestrator/graphIngress.d.ts +136 -0
  54. package/dist/orchestrator/graphIngress.d.ts.map +1 -0
  55. package/dist/orchestrator/graphShadow.d.ts +121 -0
  56. package/dist/orchestrator/graphShadow.d.ts.map +1 -0
  57. package/dist/orchestrator/ledger.d.ts +14 -2
  58. package/dist/orchestrator/ledger.d.ts.map +1 -1
  59. package/dist/orchestrator/opencodeSessionAdapter.d.ts.map +1 -1
  60. package/dist/orchestrator/roles.d.ts +3 -3
  61. package/dist/orchestrator/sdd.d.ts +55 -1
  62. package/dist/orchestrator/sdd.d.ts.map +1 -1
  63. package/dist/orchestrator/sddCompiler.d.ts +6 -2
  64. package/dist/orchestrator/sddCompiler.d.ts.map +1 -1
  65. package/dist/orchestrator/worktreeAdapter.d.ts +39 -0
  66. package/dist/orchestrator/worktreeAdapter.d.ts.map +1 -0
  67. package/dist/orchestrator/worktreeDispatch.d.ts +84 -0
  68. package/dist/orchestrator/worktreeDispatch.d.ts.map +1 -0
  69. package/dist/orchestrator/worktreeReconciler.d.ts +74 -0
  70. package/dist/orchestrator/worktreeReconciler.d.ts.map +1 -0
  71. package/dist/plugin/graphShadowIngress.d.ts +165 -0
  72. package/dist/plugin/graphShadowIngress.d.ts.map +1 -0
  73. package/dist/plugin/graphTool.d.ts +63 -0
  74. package/dist/plugin/graphTool.d.ts.map +1 -0
  75. package/dist/plugin/hooks.d.ts +6 -0
  76. package/dist/plugin/hooks.d.ts.map +1 -1
  77. package/dist/plugin/legacyRunProducer.d.ts +60 -0
  78. package/dist/plugin/legacyRunProducer.d.ts.map +1 -0
  79. package/dist/plugin/reportRunSchema.d.ts +60 -0
  80. package/dist/plugin/reportRunSchema.d.ts.map +1 -0
  81. package/dist/plugin/sessionTracker.d.ts +42 -0
  82. package/dist/plugin/sessionTracker.d.ts.map +1 -0
  83. package/dist/plugin/shadowPipeline.d.ts +49 -0
  84. package/dist/plugin/shadowPipeline.d.ts.map +1 -0
  85. package/dist/plugin/soakObserver.d.ts +73 -0
  86. package/dist/plugin/soakObserver.d.ts.map +1 -0
  87. package/dist/storage/graph/codec.d.ts.map +1 -1
  88. package/dist/storage/graph/migrateLegacyBriefs.d.ts.map +1 -1
  89. package/dist/storage/graph/migration.d.ts +91 -0
  90. package/dist/storage/graph/migration.d.ts.map +1 -0
  91. package/dist/storage/graph/provider.d.ts +1 -0
  92. package/dist/storage/graph/provider.d.ts.map +1 -1
  93. package/dist/storage/graph/recovery.d.ts.map +1 -1
  94. package/dist/storage/graph/runRegistry.d.ts.map +1 -1
  95. package/dist/storage/graph/soakRecorder.d.ts +59 -0
  96. package/dist/storage/graph/soakRecorder.d.ts.map +1 -0
  97. package/dist/storage/graph/workspaceLease.d.ts.map +1 -1
  98. package/dist/storage/graph/worktreeRegistry.d.ts +29 -0
  99. package/dist/storage/graph/worktreeRegistry.d.ts.map +1 -0
  100. package/dist/storage/graph/writer.d.ts +15 -0
  101. package/dist/storage/graph/writer.d.ts.map +1 -1
  102. package/dist/storage/path.d.ts.map +1 -1
  103. package/dist/telemetry/events.d.ts +12 -12
  104. package/dist/telemetry/graphRuntimeHealth.d.ts +53 -0
  105. package/dist/telemetry/graphRuntimeHealth.d.ts.map +1 -0
  106. package/dist/telemetry/graphView.d.ts +76 -0
  107. package/dist/telemetry/graphView.d.ts.map +1 -0
  108. package/dist/telemetry/types.d.ts +4 -4
  109. package/dist/web/console.d.ts +6 -0
  110. package/dist/web/console.d.ts.map +1 -1
  111. package/dist/web/graphRoute.d.ts +36 -0
  112. package/dist/web/graphRoute.d.ts.map +1 -0
  113. package/dist/web/server.d.ts +21 -2
  114. package/dist/web/server.d.ts.map +1 -1
  115. package/dist/web/start.d.ts +11 -0
  116. package/dist/web/start.d.ts.map +1 -1
  117. package/package.json +7 -5
package/dist/cli.js CHANGED
@@ -33,6 +33,50 @@ function eventSessionID(event) {
33
33
  }
34
34
  return str(props.sessionID) ?? str(asRecord(props.part)?.sessionID) ?? str(asRecord(props.info)?.sessionID);
35
35
  }
36
+ function partToFrame(part) {
37
+ if (part.type === "text") {
38
+ const text = str(part.text);
39
+ return text === undefined ? undefined : { kind: "text", text };
40
+ }
41
+ if (part.type === "tool") {
42
+ const tool = str(part.tool) ?? "tool";
43
+ const status = str(asRecord(part.state)?.status) ?? "run";
44
+ return { kind: "tool", text: `⚙ ${tool} · ${status}` };
45
+ }
46
+ return;
47
+ }
48
+ function messagesToFrames(raw, sessionID) {
49
+ if (!Array.isArray(raw)) {
50
+ return [];
51
+ }
52
+ const frames = [];
53
+ for (const entry of raw) {
54
+ const record = asRecord(entry);
55
+ if (record === undefined) {
56
+ continue;
57
+ }
58
+ const info = asRecord(record.info);
59
+ const entrySession = str(info?.sessionID) ?? str(record.sessionID);
60
+ if (entrySession !== undefined && entrySession !== sessionID) {
61
+ continue;
62
+ }
63
+ const parts = record.parts;
64
+ if (!Array.isArray(parts)) {
65
+ continue;
66
+ }
67
+ for (const rawPart of parts) {
68
+ const part = asRecord(rawPart);
69
+ if (part === undefined) {
70
+ continue;
71
+ }
72
+ const frame = partToFrame(part);
73
+ if (frame !== undefined) {
74
+ frames.push(frame);
75
+ }
76
+ }
77
+ }
78
+ return frames;
79
+ }
36
80
  function eventToSessionFrame(event, sessionID) {
37
81
  if (eventSessionID(event) !== sessionID) {
38
82
  return;
@@ -41,19 +85,7 @@ function eventToSessionFrame(event, sessionID) {
41
85
  switch (event.type) {
42
86
  case "message.part.updated": {
43
87
  const part = asRecord(props.part);
44
- if (part === undefined) {
45
- return;
46
- }
47
- if (part.type === "text") {
48
- const text = str(part.text);
49
- return text === undefined ? undefined : { kind: "text", text };
50
- }
51
- if (part.type === "tool") {
52
- const tool = str(part.tool) ?? "tool";
53
- const status = str(asRecord(part.state)?.status) ?? "run";
54
- return { kind: "tool", text: `⚙ ${tool} · ${status}` };
55
- }
56
- return;
88
+ return part === undefined ? undefined : partToFrame(part);
57
89
  }
58
90
  case "session.idle":
59
91
  return { kind: "status", text: "● sesión lista" };
@@ -163,6 +195,16 @@ function createConsoleClient(deps) {
163
195
  async respondPermission(sessionID, permissionID, response) {
164
196
  await post(`/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(permissionID)}`, { response });
165
197
  },
198
+ async listMessages(sessionID) {
199
+ const res = await doFetch(`${base}/session/${encodeURIComponent(sessionID)}/message`, { headers: { accept: "application/json", ...authHeaders } });
200
+ let raw;
201
+ try {
202
+ raw = await res.json();
203
+ } catch {
204
+ return [];
205
+ }
206
+ return messagesToFrames(raw, sessionID);
207
+ },
166
208
  async streamFrames(sessionID, onFrame, signal) {
167
209
  const res = await doFetch(`${base}/global/event`, {
168
210
  headers: { accept: "text/event-stream", ...authHeaders },
@@ -631,7 +673,11 @@ var BaselineModeSchema = z3.enum(["auto", "pinned"]);
631
673
  var GraphModeSchema = z3.enum(["off", "shadow", "active"]);
632
674
  var GraphConfigSchema = z3.object({
633
675
  mode: GraphModeSchema.default("off"),
634
- killSwitch: z3.boolean().default(false)
676
+ killSwitch: z3.boolean().default(false),
677
+ operatorApproval: z3.boolean().default(false),
678
+ worktrees: z3.object({
679
+ enabled: z3.boolean().default(false)
680
+ }).default({ enabled: false })
635
681
  });
636
682
  var ConsoleHostSchema = z3.enum(["127.0.0.1", "localhost"]);
637
683
  var ConsoleConfigSchema = z3.object({
@@ -645,7 +691,10 @@ var ConsoleConfigSchema = z3.object({
645
691
  terminal: z3.object({
646
692
  enabled: z3.boolean().default(true),
647
693
  pty: z3.boolean().optional()
648
- }).default({ enabled: true })
694
+ }).default({ enabled: true }),
695
+ graphView: z3.object({
696
+ enabled: z3.boolean().default(false)
697
+ }).optional()
649
698
  }).default({
650
699
  host: "127.0.0.1",
651
700
  port: 4599,
@@ -1299,6 +1348,51 @@ code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px
1299
1348
  .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
1300
1349
  .pty .pty-start:disabled{opacity:.5;cursor:default}
1301
1350
  `;
1351
+ var GRAPH_STYLE = `
1352
+ .graph-wrap{overflow:auto;background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:8px}
1353
+ svg.graph{display:block}
1354
+ .gedge{stroke:#3a4250;stroke-width:1.5;marker-end:url(#garrow)}
1355
+ .gnode rect{fill:var(--panel);stroke:var(--line);stroke-width:1.5;rx:8}
1356
+ .gnode .gname{fill:var(--fg);font:600 12px ui-monospace,Consolas,monospace}
1357
+ .gnode .grole{fill:var(--muted);font-size:11px}
1358
+ .gnode .gstate{font-size:10px;text-transform:uppercase;letter-spacing:.05em}
1359
+ .gnode.state-pending rect{stroke:#6e7681}
1360
+ .gnode.state-pending .gstate{fill:#6e7681}
1361
+ .gnode.state-ready rect{stroke:var(--local)}
1362
+ .gnode.state-ready .gstate{fill:var(--local)}
1363
+ .gnode.state-active rect{stroke:var(--frontier)}
1364
+ .gnode.state-active .gstate{fill:var(--frontier)}
1365
+ .gnode.state-succeeded rect{stroke:var(--good)}
1366
+ .gnode.state-succeeded .gstate{fill:var(--good)}
1367
+ .gnode.state-failed rect{stroke:#f85149}
1368
+ .gnode.state-failed .gstate{fill:#f85149}
1369
+ .gnode.state-cancelled rect{stroke:#484f58}
1370
+ .gnode.state-cancelled .gstate{fill:#484f58}
1371
+ .gbadge{font-size:10px;font-family:ui-monospace,Consolas,monospace}
1372
+ .gbadge.badge-retry{fill:var(--frontier)}
1373
+ .gbadge.badge-error{fill:#f85149}
1374
+ .gbadge.badge-cancelled{fill:#484f58}
1375
+ .gbadge.badge-verdict.verdict-approved{fill:var(--good)}
1376
+ .gbadge.badge-verdict.verdict-rejected{fill:#f85149}
1377
+ .gbadge.badge-verdict.verdict-inconclusive{fill:var(--frontier)}
1378
+ .gbadge.badge-lineage{fill:var(--local)}
1379
+ .gbadge.badge-artifact{fill:var(--muted)}
1380
+ .gnode[data-session]{cursor:pointer}
1381
+ .gnode[data-session]:hover rect{stroke-width:2.5}
1382
+ .graph-health{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
1383
+ .gchip{font-size:11px;padding:2px 10px;border-radius:10px;border:1px solid var(--line);color:var(--muted)}
1384
+ .gchip.on{font-weight:600}
1385
+ .gchip.gh-recovery.on,.gchip.gh-unknown.on{color:var(--frontier);border-color:var(--frontier)}
1386
+ .gchip.gh-conflict.on,.gchip.gh-kill.on{color:#f85149;border-color:#f85149}
1387
+ .graph-drill{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
1388
+ .graph-drill h3{font-size:13px;margin:0 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
1389
+ .graph-drill .drilllog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:280px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
1390
+ .graph-drill .drilllog .tl{padding:1px 0}
1391
+ .graph-drill .drilllog .tl.tool{color:var(--frontier)}
1392
+ .graph-drill .drilllog .tl.status{color:var(--muted)}
1393
+ .graph-drill .drilllog .tl.permission{color:var(--frontier);font-weight:600}
1394
+ .graph-drill .drilllog .tl.error{color:#f85149}
1395
+ `;
1302
1396
  var CONSOLE_CLIENT_JS = `
1303
1397
  (function(){
1304
1398
  var authTok='';
@@ -1424,6 +1518,47 @@ var CONSOLE_CLIENT_JS = `
1424
1518
  ');startBtn.disabled=false}
1425
1519
  }
1426
1520
  }
1521
+ function wireGraphDrill(){
1522
+ var root=document.documentElement;
1523
+ if(root.getAttribute('data-drilldown')!=='1'){return}
1524
+ var token=root.getAttribute('data-token');
1525
+ if(!token){return}
1526
+ var drill=document.querySelector('.graph-drill');
1527
+ if(!drill){return}
1528
+ var gview=drill.closest('.view');
1529
+ if(!gview){return}
1530
+ var log=drill.querySelector('.drilllog');
1531
+ var title=drill.querySelector('.drill-title');
1532
+ if(!log){return}
1533
+ var es=null;
1534
+ function append(cls,text){
1535
+ var line=document.createElement('div');
1536
+ line.className='tl '+cls;line.textContent=text;
1537
+ log.appendChild(line);log.scrollTop=log.scrollHeight;
1538
+ }
1539
+ function open(id){
1540
+ if(es){try{es.close()}catch(_){}}
1541
+ log.textContent='';drill.hidden=false;
1542
+ if(title){title.textContent='Conversación · '+id}
1543
+ var tk='token='+encodeURIComponent(token);
1544
+ var base='/console/session/'+encodeURIComponent(id);
1545
+ fetch(base+'/messages?'+tk).then(function(r){return r.ok?r.json():Promise.reject(r.status)})
1546
+ .then(function(d){(d&&d.frames||[]).forEach(function(f){append(f.kind,f.text)})})
1547
+ .catch(function(s){append('error','✖ no se pudo cargar la conversación ('+s+')')});
1548
+ if('EventSource' in window){
1549
+ try{
1550
+ es=new EventSource(base+'/stream?'+tk);
1551
+ es.addEventListener('frame',function(e){try{var f=JSON.parse(e.data);append(f.kind,f.text)}catch(_){}});
1552
+ es.onerror=function(){/* reconecta solo */};
1553
+ }catch(_){/* ignore */}
1554
+ }
1555
+ }
1556
+ gview.addEventListener('click',function(e){
1557
+ var t=e.target;
1558
+ while(t&&t!==gview&&!(t.getAttribute&&t.getAttribute('data-session'))){t=t.parentNode}
1559
+ if(t&&t.getAttribute&&t.getAttribute('data-session')){open(t.getAttribute('data-session'))}
1560
+ });
1561
+ }
1427
1562
  var bar=document.getElementById('tabbar');
1428
1563
  if(bar){
1429
1564
  bar.addEventListener('click',function(e){
@@ -1435,6 +1570,7 @@ var CONSOLE_CLIENT_JS = `
1435
1570
  var saved='all';
1436
1571
  try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
1437
1572
  if(!selectTab(saved)){selectTab('all')}
1573
+ wireGraphDrill();
1438
1574
  })();
1439
1575
  `;
1440
1576
 
@@ -1684,14 +1820,15 @@ var STATUS_LABELS = {
1684
1820
  function statusBadge(status) {
1685
1821
  return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
1686
1822
  }
1687
- function tabBar(sessions) {
1823
+ function tabBar(sessions, graphEnabled) {
1688
1824
  const allTab = '<button type="button" class="tab" data-tab="all">Todas</button>';
1825
+ const graphTab = graphEnabled ? '<button type="button" class="tab" data-tab="graph">Grafo</button>' : "";
1689
1826
  const tabs = sessions.map((tab) => {
1690
1827
  const id = escapeHtml(tab.sessionID);
1691
1828
  const short = escapeHtml(shortSessionID(tab.sessionID));
1692
1829
  return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
1693
1830
  }).join("");
1694
- return `<nav class="tabs" id="tabbar">${allTab}${tabs}</nav>`;
1831
+ return `<nav class="tabs" id="tabbar">${allTab}${graphTab}${tabs}</nav>`;
1695
1832
  }
1696
1833
  function shortSessionID(sessionID) {
1697
1834
  return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
@@ -1753,10 +1890,183 @@ function sessionView(tab, terminalEnabled, ptyEnabled) {
1753
1890
  ].join("");
1754
1891
  return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
1755
1892
  }
1893
+ var GRAPH_COL_W = 220;
1894
+ var GRAPH_ROW_H = 96;
1895
+ var GRAPH_NODE_W = 180;
1896
+ var GRAPH_NODE_H = 64;
1897
+ var GRAPH_PAD = 20;
1898
+ function computeLayers(projection) {
1899
+ const layer = new Map;
1900
+ for (const node of projection.nodes) {
1901
+ layer.set(node.nodeID, 0);
1902
+ }
1903
+ for (let pass = 0;pass < projection.nodes.length; pass += 1) {
1904
+ let changed = false;
1905
+ for (const edge of projection.edges) {
1906
+ const from = layer.get(edge.from);
1907
+ const to = layer.get(edge.to);
1908
+ if (from === undefined || to === undefined) {
1909
+ continue;
1910
+ }
1911
+ if (from + 1 > to) {
1912
+ layer.set(edge.to, from + 1);
1913
+ changed = true;
1914
+ }
1915
+ }
1916
+ if (!changed) {
1917
+ break;
1918
+ }
1919
+ }
1920
+ return layer;
1921
+ }
1922
+ function layoutNodes(projection) {
1923
+ const layers = computeLayers(projection);
1924
+ const byLayer = [];
1925
+ for (const node of projection.nodes) {
1926
+ const l = layers.get(node.nodeID) ?? 0;
1927
+ let bucket = byLayer[l];
1928
+ if (bucket === undefined) {
1929
+ bucket = [];
1930
+ byLayer[l] = bucket;
1931
+ }
1932
+ bucket.push(node.nodeID);
1933
+ }
1934
+ const pos = new Map;
1935
+ let maxLayer = 0;
1936
+ let maxRows = 0;
1937
+ byLayer.forEach((ids, l) => {
1938
+ if (l > maxLayer) {
1939
+ maxLayer = l;
1940
+ }
1941
+ if (ids.length > maxRows) {
1942
+ maxRows = ids.length;
1943
+ }
1944
+ ids.forEach((id, row) => {
1945
+ pos.set(id, {
1946
+ x: GRAPH_PAD + l * GRAPH_COL_W,
1947
+ y: GRAPH_PAD + row * GRAPH_ROW_H
1948
+ });
1949
+ });
1950
+ });
1951
+ const width = GRAPH_PAD * 2 + maxLayer * GRAPH_COL_W + GRAPH_NODE_W;
1952
+ const height = GRAPH_PAD * 2 + Math.max(0, maxRows - 1) * GRAPH_ROW_H + GRAPH_NODE_H;
1953
+ return { pos, width, height };
1954
+ }
1955
+ function nodeBadges(node) {
1956
+ const badges = [];
1957
+ if (node.attempts > 1) {
1958
+ badges.push({ cls: "badge-retry", text: `⟳${node.attempts}` });
1959
+ }
1960
+ if (node.lastErrorClass !== undefined) {
1961
+ badges.push({
1962
+ cls: `badge-error err-${node.lastErrorClass}`,
1963
+ text: `⚠${node.lastErrorClass}`
1964
+ });
1965
+ }
1966
+ if (node.state === "cancelled") {
1967
+ badges.push({ cls: "badge-cancelled", text: "⊘" });
1968
+ }
1969
+ if (node.review !== undefined) {
1970
+ const symbol = node.review.verdict === "approved" ? "✓" : node.review.verdict === "rejected" ? "✗" : "?";
1971
+ badges.push({
1972
+ cls: `badge-verdict verdict-${node.review.verdict}`,
1973
+ text: node.review.reassigned ? `${symbol}↔` : symbol
1974
+ });
1975
+ }
1976
+ if (node.revisionOf !== undefined) {
1977
+ badges.push({
1978
+ cls: "badge-lineage",
1979
+ text: `↺${escapeHtml(node.revisionOf)}`
1980
+ });
1981
+ }
1982
+ if (node.artifact !== undefined) {
1983
+ badges.push({ cls: "badge-artifact", text: "⧉" });
1984
+ }
1985
+ return badges;
1986
+ }
1987
+ function nodeSvg(node, at) {
1988
+ const badges = nodeBadges(node).map((badge, index) => `<text class="gbadge ${badge.cls}" x="${10 + index * 34}" y="58">${badge.text}</text>`).join("");
1989
+ const sessionAttr = node.sessionRef === undefined ? "" : ` data-session="${escapeHtml(node.sessionRef)}"`;
1990
+ return [
1991
+ `<g class="gnode state-${node.state}" data-node="${escapeHtml(node.nodeID)}" data-state="${node.state}" data-attempts="${node.attempts}"${sessionAttr} transform="translate(${at.x},${at.y})">`,
1992
+ `<rect width="${GRAPH_NODE_W}" height="${GRAPH_NODE_H}"></rect>`,
1993
+ `<text class="gname" x="10" y="20">${escapeHtml(node.nodeID)}</text>`,
1994
+ `<text class="grole" x="10" y="36">${escapeHtml(node.role)}</text>`,
1995
+ `<text class="gstate" x="10" y="50">${node.state}</text>`,
1996
+ badges,
1997
+ "</g>"
1998
+ ].join("");
1999
+ }
2000
+ function edgeSvg(pos, from, to) {
2001
+ const a = pos.get(from);
2002
+ const b = pos.get(to);
2003
+ if (a === undefined || b === undefined) {
2004
+ return "";
2005
+ }
2006
+ const x1 = a.x + GRAPH_NODE_W;
2007
+ const y1 = a.y + GRAPH_NODE_H / 2;
2008
+ const x2 = b.x;
2009
+ const y2 = b.y + GRAPH_NODE_H / 2;
2010
+ return `<line class="gedge" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" data-from="${escapeHtml(from)}" data-to="${escapeHtml(to)}"></line>`;
2011
+ }
2012
+ function renderGraphView(projection) {
2013
+ if (projection === null) {
2014
+ return '<p class="muted">Sin run de grafo activo.</p>';
2015
+ }
2016
+ if (projection.nodes.length === 0) {
2017
+ return '<p class="muted">El run no tiene nodos.</p>';
2018
+ }
2019
+ const { pos, width, height } = layoutNodes(projection);
2020
+ const edges = projection.edges.map((edge) => edgeSvg(pos, edge.from, edge.to)).join("");
2021
+ const nodes = projection.nodes.map((node) => {
2022
+ const at = pos.get(node.nodeID);
2023
+ return at === undefined ? "" : nodeSvg(node, at);
2024
+ }).join("");
2025
+ return [
2026
+ `<div class="graph-wrap"><svg class="graph" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="Grafo de agentes">`,
2027
+ '<defs><marker id="garrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L8 4 L0 8 z" fill="#3a4250"></path></marker></defs>',
2028
+ edges,
2029
+ nodes,
2030
+ "</svg></div>"
2031
+ ].join("");
2032
+ }
2033
+ function healthBanner(health) {
2034
+ const chip = (cls, on, label) => `<span class="gchip ${cls} ${on ? "on" : "off"}">${label}</span>`;
2035
+ return [
2036
+ '<div class="graph-health" role="status">',
2037
+ chip("gh-recovery", health.recoveryRequired, health.recoveryRequired ? "recuperación requerida" : "sin recuperación"),
2038
+ chip("gh-unknown", health.unknownEffects > 0, `efectos desconocidos: ${health.unknownEffects}`),
2039
+ chip("gh-stale", health.staleCompletions > 0, `completaciones tardías: ${health.staleCompletions}`),
2040
+ chip("gh-conflict", health.conflicts > 0, `conflictos lease/worktree: ${health.conflicts}`),
2041
+ chip("gh-kill", health.killSwitch, health.killSwitch ? "kill switch activo" : "kill switch inactivo"),
2042
+ "</div>"
2043
+ ].join("");
2044
+ }
2045
+ function drillPanel() {
2046
+ return [
2047
+ '<aside class="graph-drill" hidden>',
2048
+ '<h3 class="drill-title">Conversación</h3>',
2049
+ '<div class="drilllog" aria-live="polite"></div>',
2050
+ "</aside>"
2051
+ ].join("");
2052
+ }
2053
+ function graphViewPanel(projection, drilldownEnabled) {
2054
+ return [
2055
+ '<div class="view" data-view="graph" hidden>',
2056
+ '<section class="panel" id="panel-graph">',
2057
+ "<h2>Grafo de agentes</h2>",
2058
+ projection === null ? "" : healthBanner(projection.health),
2059
+ renderGraphView(projection),
2060
+ drilldownEnabled ? drillPanel() : "",
2061
+ "</section>",
2062
+ "</div>"
2063
+ ].join("");
2064
+ }
1756
2065
  function renderConsoleHtml(state, options = {}) {
1757
2066
  const refreshMs = options.refreshMs ?? 2000;
1758
2067
  const terminalEnabled = options.terminalEnabled === true;
1759
2068
  const ptyEnabled = options.ptyEnabled === true;
2069
+ const graphEnabled = options.graphView !== undefined;
1760
2070
  const allBody = [
1761
2071
  summaryPanel(state.cost),
1762
2072
  totalsPanel(state.totals),
@@ -1770,10 +2080,12 @@ function renderConsoleHtml(state, options = {}) {
1770
2080
  activityPanel(state.activity)
1771
2081
  ].join("");
1772
2082
  const allView = `<div class="view" data-view="all">${allBody}</div>`;
2083
+ const graphDrilldown = graphEnabled && options.drilldownEnabled === true;
2084
+ const graphView = graphEnabled ? graphViewPanel(options.graphView?.projection ?? null, graphDrilldown) : "";
1773
2085
  const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
1774
- const body = allView + sessionViews;
2086
+ const body = allView + graphView + sessionViews;
1775
2087
  const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
1776
- const tokenAttr = terminalEnabled && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}" data-terminal="1"${ptyEnabled ? ' data-pty="1"' : ""}` : "";
2088
+ const tokenAttr = (terminalEnabled || graphDrilldown) && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}"${terminalEnabled ? ' data-terminal="1"' : ""}${ptyEnabled ? ' data-pty="1"' : ""}${graphDrilldown ? ' data-drilldown="1"' : ""}` : "";
1777
2089
  return [
1778
2090
  "<!doctype html>",
1779
2091
  `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}"${tokenAttr}>`,
@@ -1781,14 +2093,14 @@ function renderConsoleHtml(state, options = {}) {
1781
2093
  '<meta charset="utf-8">',
1782
2094
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
1783
2095
  "<title>openteam console</title>",
1784
- `<style>${CONSOLE_STYLE}</style>`,
2096
+ `<style>${CONSOLE_STYLE}${graphEnabled ? GRAPH_STYLE : ""}</style>`,
1785
2097
  "</head>",
1786
2098
  "<body>",
1787
2099
  "<header>",
1788
2100
  "<h1>openteam console</h1>",
1789
2101
  `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
1790
2102
  "</header>",
1791
- tabBar(state.sessionsDetail),
2103
+ tabBar(state.sessionsDetail, graphEnabled),
1792
2104
  `<main>${body}</main>`,
1793
2105
  `<script>${CONSOLE_CLIENT_JS}</script>`,
1794
2106
  "</body>",
@@ -2120,7 +2432,7 @@ async function readJsonBody(request, limit) {
2120
2432
  });
2121
2433
  }
2122
2434
  function parseConsolePath(path) {
2123
- const match = /^\/console\/session\/([^/]+)\/(stream|input|permission|pty)$/.exec(path);
2435
+ const match = /^\/console\/session\/([^/]+)\/(stream|input|permission|pty|messages)$/.exec(path);
2124
2436
  if (match === null) {
2125
2437
  return;
2126
2438
  }
@@ -2150,6 +2462,15 @@ data: {}
2150
2462
  response.end();
2151
2463
  }
2152
2464
  }
2465
+ async function handleMessages(_request, response, sessionID, deps) {
2466
+ const endpoint = await deps.resolveEndpoint(sessionID);
2467
+ if (endpoint === undefined) {
2468
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
2469
+ return;
2470
+ }
2471
+ const frames = await deps.createClient(endpoint).listMessages(sessionID);
2472
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ frames }));
2473
+ }
2153
2474
  async function handleInput(request, response, sessionID, deps) {
2154
2475
  const endpoint = await deps.resolveEndpoint(sessionID);
2155
2476
  if (endpoint === undefined) {
@@ -2217,7 +2538,9 @@ async function handleConsoleRoute(request, response, url, deps) {
2217
2538
  response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "not found" }));
2218
2539
  return true;
2219
2540
  }
2220
- if (!deps.terminalEnabled) {
2541
+ const readOnlyAction = route.action === "stream" || route.action === "messages";
2542
+ const channelEnabled = deps.terminalEnabled || readOnlyAction && deps.drilldownEnabled === true;
2543
+ if (!channelEnabled) {
2221
2544
  response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "terminal disabled" }));
2222
2545
  return true;
2223
2546
  }
@@ -2229,6 +2552,10 @@ async function handleConsoleRoute(request, response, url, deps) {
2229
2552
  await handleStream(request, response, route.sessionID, deps);
2230
2553
  return true;
2231
2554
  }
2555
+ if (route.action === "messages" && request.method === "GET") {
2556
+ await handleMessages(request, response, route.sessionID, deps);
2557
+ return true;
2558
+ }
2232
2559
  if (route.action === "input" && request.method === "POST") {
2233
2560
  await handleInput(request, response, route.sessionID, deps);
2234
2561
  return true;
@@ -2245,10 +2572,15 @@ async function handleConsoleRoute(request, response, url, deps) {
2245
2572
  return true;
2246
2573
  }
2247
2574
 
2248
- // src/web/storageRoute.ts
2575
+ // src/web/graphRoute.ts
2249
2576
  var JSON_HEADERS2 = { "content-type": "application/json" };
2250
- var READ_OPS = new Set(["read", "exists", "list", "isDirectory", "stat"]);
2251
- var WRITE_OPS = new Set(["write", "append", "mkdir", "delete"]);
2577
+ var GRAPH_SSE_EVENT = "graph";
2578
+ function graphSseMessage(json) {
2579
+ return `event: ${GRAPH_SSE_EVENT}
2580
+ data: ${json}
2581
+
2582
+ `;
2583
+ }
2252
2584
  function bearerFromHeader2(request) {
2253
2585
  const header = request.headers.authorization;
2254
2586
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
@@ -2261,6 +2593,39 @@ function authorized2(request, query, token) {
2261
2593
  const provided = tokenFromQuery(query) ?? bearerFromHeader2(request);
2262
2594
  return provided !== undefined && safeEqualToken(provided, token);
2263
2595
  }
2596
+ async function handleGraphRoute(request, response, url, deps) {
2597
+ if (url.pathname !== "/api/graph") {
2598
+ return false;
2599
+ }
2600
+ if (request.method !== "GET") {
2601
+ response.writeHead(405, JSON_HEADERS2).end(JSON.stringify({ error: "method not allowed" }));
2602
+ return true;
2603
+ }
2604
+ if (deps.remoteToken !== undefined && !authorized2(request, url.searchParams, deps.remoteToken)) {
2605
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
2606
+ return true;
2607
+ }
2608
+ const view = await deps.readView();
2609
+ response.writeHead(200, JSON_HEADERS2).end(JSON.stringify(view));
2610
+ return true;
2611
+ }
2612
+
2613
+ // src/web/storageRoute.ts
2614
+ var JSON_HEADERS3 = { "content-type": "application/json" };
2615
+ var READ_OPS = new Set(["read", "exists", "list", "isDirectory", "stat"]);
2616
+ var WRITE_OPS = new Set(["write", "append", "mkdir", "delete"]);
2617
+ function bearerFromHeader3(request) {
2618
+ const header = request.headers.authorization;
2619
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
2620
+ return;
2621
+ }
2622
+ const value = header.slice("Bearer ".length).trim();
2623
+ return value.length === 0 ? undefined : value;
2624
+ }
2625
+ function authorized3(request, query, token) {
2626
+ const provided = tokenFromQuery(query) ?? bearerFromHeader3(request);
2627
+ return provided !== undefined && safeEqualToken(provided, token);
2628
+ }
2264
2629
  function pathAllowed(path, allowPrefixes) {
2265
2630
  if (allowPrefixes === undefined) {
2266
2631
  return true;
@@ -2300,7 +2665,7 @@ async function readJsonBody2(request, limit) {
2300
2665
  });
2301
2666
  }
2302
2667
  function sendJson(response, status, body) {
2303
- response.writeHead(status, JSON_HEADERS2).end(JSON.stringify(body));
2668
+ response.writeHead(status, JSON_HEADERS3).end(JSON.stringify(body));
2304
2669
  }
2305
2670
  async function handleRead(response, op, path, storage) {
2306
2671
  if (op === "read") {
@@ -2374,7 +2739,7 @@ async function handleStorageRoute(request, response, url, deps) {
2374
2739
  sendJson(response, 404, { error: "not found" });
2375
2740
  return true;
2376
2741
  }
2377
- if (!authorized2(request, url.searchParams, deps.token)) {
2742
+ if (!authorized3(request, url.searchParams, deps.token)) {
2378
2743
  sendJson(response, 401, { error: "unauthorized" });
2379
2744
  return true;
2380
2745
  }
@@ -2415,12 +2780,12 @@ var SSE_HEADERS2 = {
2415
2780
  "cache-control": "no-cache",
2416
2781
  connection: "keep-alive"
2417
2782
  };
2418
- var JSON_HEADERS3 = { "content-type": "application/json" };
2783
+ var JSON_HEADERS4 = { "content-type": "application/json" };
2419
2784
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
2420
2785
  function isAddressInUse(error) {
2421
2786
  return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2422
2787
  }
2423
- function bearerFromHeader3(request) {
2788
+ function bearerFromHeader4(request) {
2424
2789
  const header = request.headers.authorization;
2425
2790
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
2426
2791
  return;
@@ -2429,16 +2794,19 @@ function bearerFromHeader3(request) {
2429
2794
  return value.length === 0 ? undefined : value;
2430
2795
  }
2431
2796
  function readAuthorized(request, url, token) {
2432
- const provided = tokenFromQuery(url.searchParams) ?? bearerFromHeader3(request);
2797
+ const provided = tokenFromQuery(url.searchParams) ?? bearerFromHeader4(request);
2433
2798
  return provided !== undefined && safeEqualToken(provided, token);
2434
2799
  }
2435
2800
  async function renderState(deps) {
2801
+ const graphView = deps.graphApi === undefined ? undefined : { projection: await deps.graphApi.readView() };
2436
2802
  return renderConsoleHtml(buildConsoleState(await deps.readSnapshot()), {
2437
2803
  refreshMs: deps.config.refreshMs,
2804
+ ...graphView !== undefined ? { graphView } : {},
2438
2805
  ...deps.console !== undefined ? {
2439
2806
  consoleToken: deps.console.token,
2440
2807
  terminalEnabled: deps.console.terminalEnabled,
2441
- ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {}
2808
+ ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {},
2809
+ ...deps.console.drilldownEnabled !== undefined ? { drilldownEnabled: deps.console.drilldownEnabled } : {}
2442
2810
  } : {}
2443
2811
  });
2444
2812
  }
@@ -2484,13 +2852,21 @@ async function createConsoleServer(deps) {
2484
2852
  resolveEndpoint,
2485
2853
  createClient: deps.console.createClient,
2486
2854
  ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {},
2855
+ ...deps.console.drilldownEnabled !== undefined ? { drilldownEnabled: deps.console.drilldownEnabled } : {},
2487
2856
  ...deps.console.createPtyClient !== undefined ? { createPtyClient: deps.console.createPtyClient } : {}
2488
2857
  };
2858
+ const graphDeps = deps.graphApi === undefined ? undefined : {
2859
+ readView: deps.graphApi.readView,
2860
+ ...deps.storageApi?.token !== undefined ? { remoteToken: deps.storageApi.token } : {}
2861
+ };
2489
2862
  const handle = async (request, response) => {
2490
2863
  const url2 = new URL(request.url ?? "/", "http://localhost");
2491
2864
  if (consoleDeps !== undefined && await handleConsoleRoute(request, response, url2, consoleDeps)) {
2492
2865
  return;
2493
2866
  }
2867
+ if (graphDeps !== undefined && await handleGraphRoute(request, response, url2, graphDeps)) {
2868
+ return;
2869
+ }
2494
2870
  if (deps.storageApi !== undefined && await handleStorageRoute(request, response, url2, deps.storageApi)) {
2495
2871
  return;
2496
2872
  }
@@ -2501,7 +2877,7 @@ async function createConsoleServer(deps) {
2501
2877
  const path = url2.pathname;
2502
2878
  const remoteToken = deps.storageApi?.token;
2503
2879
  if (remoteToken !== undefined && (path === "/" || path === "/index.html" || path === "/api/state" || path === "/events") && !readAuthorized(request, url2, remoteToken)) {
2504
- response.writeHead(401, JSON_HEADERS3).end(JSON.stringify({ error: "unauthorized" }));
2880
+ response.writeHead(401, JSON_HEADERS4).end(JSON.stringify({ error: "unauthorized" }));
2505
2881
  return;
2506
2882
  }
2507
2883
  if (path === "/" || path === "/index.html") {
@@ -2509,11 +2885,11 @@ async function createConsoleServer(deps) {
2509
2885
  return;
2510
2886
  }
2511
2887
  if (path === "/api/state") {
2512
- response.writeHead(200, JSON_HEADERS3).end(await stateJson(deps));
2888
+ response.writeHead(200, JSON_HEADERS4).end(await stateJson(deps));
2513
2889
  return;
2514
2890
  }
2515
2891
  if (path === "/healthz") {
2516
- response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
2892
+ response.writeHead(200, JSON_HEADERS4).end(JSON.stringify({ ok: true }));
2517
2893
  return;
2518
2894
  }
2519
2895
  if (path === "/favicon.ico") {
@@ -2523,6 +2899,9 @@ async function createConsoleServer(deps) {
2523
2899
  if (path === "/events") {
2524
2900
  response.writeHead(200, SSE_HEADERS2);
2525
2901
  response.write(sseMessage(await stateJson(deps)));
2902
+ if (deps.graphApi !== undefined) {
2903
+ response.write(graphSseMessage(JSON.stringify(await deps.graphApi.readView())));
2904
+ }
2526
2905
  clients.add(response);
2527
2906
  request.on("close", () => {
2528
2907
  clients.delete(response);
@@ -2563,8 +2942,12 @@ async function createConsoleServer(deps) {
2563
2942
  return;
2564
2943
  }
2565
2944
  const message = sseMessage(await stateJson(deps));
2945
+ const graphMessage = deps.graphApi !== undefined ? graphSseMessage(JSON.stringify(await deps.graphApi.readView())) : undefined;
2566
2946
  for (const response of clients) {
2567
2947
  response.write(message);
2948
+ if (graphMessage !== undefined) {
2949
+ response.write(graphMessage);
2950
+ }
2568
2951
  }
2569
2952
  };
2570
2953
  const close = () => {
@@ -2895,12 +3278,14 @@ async function createConsoleRuntime(deps) {
2895
3278
  ...deps.session !== undefined ? { session: deps.session } : {}
2896
3279
  });
2897
3280
  const createServer = deps.serve ?? createConsoleServer;
3281
+ const graphApi = deps.config.graphView?.enabled === true && deps.graphView !== undefined ? { readView: deps.graphView.readView } : undefined;
2898
3282
  const server = await createServer({
2899
3283
  readSnapshot,
2900
3284
  config: deps.config,
2901
3285
  ...deps.log !== undefined ? { log: deps.log } : {},
2902
3286
  ...deps.console !== undefined ? { console: deps.console } : {},
2903
- ...deps.storageApi !== undefined ? { storageApi: deps.storageApi } : {}
3287
+ ...deps.storageApi !== undefined ? { storageApi: deps.storageApi } : {},
3288
+ ...graphApi !== undefined ? { graphApi } : {}
2904
3289
  });
2905
3290
  const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
2906
3291
  const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
@@ -4041,6 +4426,15 @@ function renderDoctor(input) {
4041
4426
  if (reachable === 0) {
4042
4427
  lines.push(" ⚠ Ningún runtime local alcanzable: todo el tráfico irá a frontier.");
4043
4428
  }
4429
+ if (input.graphHealth !== undefined) {
4430
+ const h = input.graphHealth;
4431
+ lines.push(" graph health:");
4432
+ lines.push(` recuperación requerida: ${h.recoveryRequired ? "sí" : "no"}`);
4433
+ lines.push(` efectos desconocidos: ${h.unknownEffects}`);
4434
+ lines.push(` completaciones tardías: ${h.staleCompletions}`);
4435
+ lines.push(` conflictos: ${h.conflicts}`);
4436
+ lines.push(` kill switch: ${h.killSwitch ? "activo" : "inactivo"}`);
4437
+ }
4044
4438
  return lines.join(`
4045
4439
  `);
4046
4440
  }