@jmanuelcorral/openteam 0.1.21 → 0.1.22

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 (40) hide show
  1. package/README.md +29 -31
  2. package/dist/cli/dashboardServe.d.ts +15 -8
  3. package/dist/cli/dashboardServe.d.ts.map +1 -1
  4. package/dist/cli.js +889 -276
  5. package/dist/commands/dashboard.d.ts +4 -4
  6. package/dist/commands/dashboard.d.ts.map +1 -1
  7. package/dist/commands/orchestratorAgent.d.ts.map +1 -1
  8. package/dist/contract/opencode.d.ts +1 -1
  9. package/dist/contract/opencode.d.ts.map +1 -1
  10. package/dist/dashboard/render.d.ts.map +1 -1
  11. package/dist/dashboard/state.d.ts +14 -6
  12. package/dist/dashboard/state.d.ts.map +1 -1
  13. package/dist/dashboard/types.d.ts +14 -3
  14. package/dist/dashboard/types.d.ts.map +1 -1
  15. package/dist/index.d.ts +8 -9
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +476 -802
  18. package/dist/orchestrator/coordinator.d.ts +14 -2
  19. package/dist/orchestrator/coordinator.d.ts.map +1 -1
  20. package/dist/plugin/capture.d.ts +46 -0
  21. package/dist/plugin/capture.d.ts.map +1 -0
  22. package/dist/plugin/hooks.d.ts +2 -2
  23. package/dist/plugin/hooks.d.ts.map +1 -1
  24. package/dist/plugin/toolcalls.d.ts +21 -0
  25. package/dist/plugin/toolcalls.d.ts.map +1 -0
  26. package/dist/telemetry/aggregate.d.ts +89 -0
  27. package/dist/telemetry/aggregate.d.ts.map +1 -0
  28. package/dist/telemetry/decisions.d.ts +25 -0
  29. package/dist/telemetry/decisions.d.ts.map +1 -0
  30. package/dist/telemetry/eventLog.d.ts +63 -0
  31. package/dist/telemetry/eventLog.d.ts.map +1 -0
  32. package/dist/telemetry/events.d.ts +217 -0
  33. package/dist/telemetry/events.d.ts.map +1 -0
  34. package/dist/web/snapshot.d.ts +13 -7
  35. package/dist/web/snapshot.d.ts.map +1 -1
  36. package/dist/web/start.d.ts +10 -7
  37. package/dist/web/start.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/web/activity.d.ts +0 -12
  40. package/dist/web/activity.d.ts.map +0 -1
package/dist/cli.js CHANGED
@@ -2,31 +2,16 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execFile } from "node:child_process";
5
- import { access, mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
5
+ import { access, mkdir as mkdir2, readdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
6
6
  import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
 
9
9
  // src/web/start.ts
10
10
  import { watch as fsWatch } from "node:fs";
11
11
 
12
- // src/web/activity.ts
13
- function createActivityBuffer(max = 100) {
14
- const entries = [];
15
- return {
16
- push(entry) {
17
- entries.push(entry);
18
- if (entries.length > max) {
19
- entries.splice(0, entries.length - max);
20
- }
21
- },
22
- list() {
23
- return [...entries];
24
- }
25
- };
26
- }
27
-
28
12
  // src/web/paths.ts
29
13
  var DEFAULT_BACKLOG_PATH = ".opencode/openteam-backlog.md";
14
+ var DEFAULT_DECISIONS_PATH = ".opencode/openteam-decisions.md";
30
15
 
31
16
  // src/web/server.ts
32
17
  import {
@@ -49,6 +34,12 @@ function usd(value) {
49
34
  function pct(value) {
50
35
  return `${value.toFixed(2)}%`;
51
36
  }
37
+ function ms(value) {
38
+ return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`;
39
+ }
40
+ function avgMs(stats) {
41
+ return stats.count === 0 ? "—" : ms(stats.totalDurationMs / stats.count);
42
+ }
52
43
  function agentModel(agent) {
53
44
  return agent.model === undefined ? "hereda default" : `${agent.model.providerID}/${agent.model.modelID}`;
54
45
  }
@@ -169,6 +160,101 @@ function activityPanel(activity) {
169
160
  "</section>"
170
161
  ].join("");
171
162
  }
163
+ function totalsPanel(totals) {
164
+ const t = totals.toolcalls;
165
+ return [
166
+ '<section class="panel" id="panel-totals">',
167
+ "<h2>Telemetría real (todas las sesiones)</h2>",
168
+ '<div class="grid">',
169
+ `<div class="stat"><span class="k">Sesiones</span><span class="v">${totals.sessions}</span></div>`,
170
+ `<div class="stat"><span class="k">Mensajes</span><span class="v">${totals.messageCount}</span></div>`,
171
+ `<div class="stat"><span class="k">Coste real</span><span class="v">${usd(totals.realCostUSD)}</span></div>`,
172
+ `<div class="stat"><span class="k">Ahorro est.</span><span class="v good">${usd(totals.estimatedSavingsUSD)}</span></div>`,
173
+ `<div class="stat"><span class="k">Tokens in</span><span class="v">${totals.tokensIn}</span></div>`,
174
+ `<div class="stat"><span class="k">Tokens out</span><span class="v">${totals.tokensOut}</span></div>`,
175
+ `<div class="stat"><span class="k">Reasoning</span><span class="v">${totals.tokensReasoning}</span></div>`,
176
+ `<div class="stat"><span class="k">Cache r/w</span><span class="v">${totals.tokensCacheRead}/${totals.tokensCacheWrite}</span></div>`,
177
+ `<div class="stat"><span class="k">Toolcalls</span><span class="v">${t.count} <span class="good">${t.ok}✓</span> <span class="frontier">${t.failed}✗</span></span></div>`,
178
+ `<div class="stat"><span class="k">Dur. media tool</span><span class="v">${avgMs(t)}</span></div>`,
179
+ "</div>",
180
+ "</section>"
181
+ ].join("");
182
+ }
183
+ function sessionRow(session) {
184
+ const models = session.models.map(escapeHtml).join("<br>") || "—";
185
+ return [
186
+ "<tr>",
187
+ `<td class="hash">${escapeHtml(session.sessionID)}</td>`,
188
+ `<td>${models}</td>`,
189
+ `<td><span class="local">${session.localCount}</span>/<span class="frontier">${session.frontierCount}</span></td>`,
190
+ `<td>${usd(session.realCostUSD)}</td>`,
191
+ `<td>${session.tokensIn}/${session.tokensOut}</td>`,
192
+ `<td>${session.toolcalls.count} (${avgMs(session.toolcalls)})</td>`,
193
+ "</tr>"
194
+ ].join("");
195
+ }
196
+ function sessionsPanel(sessions) {
197
+ if (sessions.length === 0) {
198
+ return [
199
+ '<section class="panel" id="panel-sessions">',
200
+ "<h2>Sesiones</h2>",
201
+ '<p class="muted">Sin sesiones registradas todavía.</p>',
202
+ "</section>"
203
+ ].join("");
204
+ }
205
+ const rows = sessions.map(sessionRow).join("");
206
+ return [
207
+ '<section class="panel" id="panel-sessions">',
208
+ "<h2>Sesiones</h2>",
209
+ "<table><thead><tr><th>Sesión</th><th>Modelos</th><th>L/F</th><th>Coste</th><th>Tok in/out</th><th>Tools</th></tr></thead>",
210
+ `<tbody>${rows}</tbody></table>`,
211
+ "</section>"
212
+ ].join("");
213
+ }
214
+ function decisionRow(decision) {
215
+ const who = decision.agent === undefined ? "" : `<span class="who">${escapeHtml(decision.agent)}</span> `;
216
+ const tags = decision.tags === undefined || decision.tags.length === 0 ? "" : ` ${decision.tags.map((tag) => `<span class="tag">#${escapeHtml(tag)}</span>`).join(" ")}`;
217
+ return `<li class="decision"><span class="kind">decision</span> ${who}${escapeHtml(decision.summary)}${tags}</li>`;
218
+ }
219
+ function decisionsPanel(decisions) {
220
+ if (decisions.length === 0) {
221
+ return [
222
+ '<section class="panel" id="panel-decisions">',
223
+ "<h2>Decisiones</h2>",
224
+ '<p class="muted">Sin decisiones registradas (el scribe escribe en <code>openteam-decisions.md</code>).</p>',
225
+ "</section>"
226
+ ].join("");
227
+ }
228
+ const rows = decisions.map(decisionRow).join("");
229
+ return [
230
+ '<section class="panel" id="panel-decisions">',
231
+ "<h2>Decisiones</h2>",
232
+ `<ul class="timeline">${rows}</ul>`,
233
+ "</section>"
234
+ ].join("");
235
+ }
236
+ function meetingRow(meeting) {
237
+ const roles = meeting.roles.map(escapeHtml).join(", ");
238
+ const purpose = meeting.purpose === undefined ? "" : `: ${escapeHtml(meeting.purpose)}`;
239
+ return `<li class="agent"><span class="kind">reunión</span> <span class="who">${roles}</span>${purpose}</li>`;
240
+ }
241
+ function meetingsPanel(meetings) {
242
+ if (meetings.length === 0) {
243
+ return [
244
+ '<section class="panel" id="panel-meetings">',
245
+ "<h2>Reuniones</h2>",
246
+ '<p class="muted">Sin reuniones multi-agente todavía.</p>',
247
+ "</section>"
248
+ ].join("");
249
+ }
250
+ const rows = meetings.map(meetingRow).join("");
251
+ return [
252
+ '<section class="panel" id="panel-meetings">',
253
+ "<h2>Reuniones</h2>",
254
+ `<ul class="timeline">${rows}</ul>`,
255
+ "</section>"
256
+ ].join("");
257
+ }
172
258
  var STYLE = `
173
259
  :root{color-scheme:dark;--bg:#0f1115;--panel:#171a21;--fg:#e6e8eb;--muted:#8b929c;--good:#3fb950;--local:#58a6ff;--frontier:#d29922;--line:#262b34}
174
260
  *{box-sizing:border-box}
@@ -198,6 +284,7 @@ ul.items li,ul.timeline li{padding:4px 0;border-bottom:1px solid var(--line)}
198
284
  ul.items li.done{color:var(--muted)}
199
285
  ul.items .box{font-family:monospace}
200
286
  .who{color:var(--local)}
287
+ .tag{color:var(--frontier);font-size:12px}
201
288
  .kind{display:inline-block;min-width:66px;color:var(--muted);font-size:12px}
202
289
  code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px 5px;border-radius:4px}
203
290
  `;
@@ -224,10 +311,14 @@ function renderDashboardHtml(state, options = {}) {
224
311
  const refreshMs = options.refreshMs ?? 2000;
225
312
  const body = [
226
313
  summaryPanel(state.cost),
314
+ totalsPanel(state.totals),
315
+ sessionsPanel(state.sessions),
227
316
  loopPanel(state.loop),
228
317
  teamPanel(state.team),
229
318
  tierPanel(state.cost),
230
319
  routesPanel(state.recentRoutes),
320
+ decisionsPanel(state.decisions),
321
+ meetingsPanel(state.meetings),
231
322
  activityPanel(state.activity)
232
323
  ].join("");
233
324
  const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
@@ -297,31 +388,612 @@ function summarizeCostRecords(records) {
297
388
  tier.estimatedUSD += record.estimatedCostUSD;
298
389
  tier.savingsUSD += record.estimatedSavingsUSD;
299
390
  }
300
- report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
301
- return report;
391
+ report.savingsPct = report.totalBaselineUSD > 0 ? report.totalSavingsUSD / report.totalBaselineUSD * 100 : 0;
392
+ return report;
393
+ }
394
+ function usd2(value) {
395
+ return `$${value.toFixed(5)}`;
396
+ }
397
+ function renderReport(report) {
398
+ if (report.count === 0) {
399
+ return "openteam report: sin registros de telemetría todavía.";
400
+ }
401
+ const lines = [
402
+ "openteam report:",
403
+ ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
404
+ ` coste real: ${usd2(report.totalEstimatedUSD)}`,
405
+ ` baseline: ${usd2(report.totalBaselineUSD)}`,
406
+ ` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
407
+ ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
408
+ " por tier:"
409
+ ];
410
+ for (const tier of TIERS2) {
411
+ const summary = report.byTier[tier];
412
+ lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
413
+ }
414
+ return lines.join(`
415
+ `);
416
+ }
417
+
418
+ // src/telemetry/aggregate.ts
419
+ function emptyToolcallStats() {
420
+ return { count: 0, ok: 0, failed: 0, totalDurationMs: 0, byTool: {} };
421
+ }
422
+ function emptySession(sessionID, ts) {
423
+ return {
424
+ sessionID,
425
+ firstTs: ts,
426
+ lastTs: ts,
427
+ routeCount: 0,
428
+ localCount: 0,
429
+ frontierCount: 0,
430
+ estimatedCostUSD: 0,
431
+ estimatedSavingsUSD: 0,
432
+ realCostUSD: 0,
433
+ messageCount: 0,
434
+ tokensIn: 0,
435
+ tokensOut: 0,
436
+ tokensReasoning: 0,
437
+ tokensCacheRead: 0,
438
+ tokensCacheWrite: 0,
439
+ toolcalls: emptyToolcallStats(),
440
+ models: [],
441
+ agents: []
442
+ };
443
+ }
444
+ function pushUnique(list, value) {
445
+ if (value !== undefined && !list.includes(value)) {
446
+ list.push(value);
447
+ }
448
+ }
449
+ function applyRoute(session, event) {
450
+ session.routeCount += 1;
451
+ if (event.routeKind === "local") {
452
+ session.localCount += 1;
453
+ } else {
454
+ session.frontierCount += 1;
455
+ }
456
+ session.estimatedCostUSD += event.estimatedCostUSD;
457
+ session.estimatedSavingsUSD += event.estimatedSavingsUSD;
458
+ pushUnique(session.models, `${event.selected.providerID}/${event.selected.modelID}`);
459
+ pushUnique(session.agents, event.agent);
460
+ }
461
+ function applyMessage(session, event) {
462
+ session.messageCount += 1;
463
+ session.realCostUSD += event.costUSD;
464
+ session.tokensIn += event.tokensIn;
465
+ session.tokensOut += event.tokensOut;
466
+ session.tokensReasoning += event.tokensReasoning;
467
+ session.tokensCacheRead += event.tokensCacheRead;
468
+ session.tokensCacheWrite += event.tokensCacheWrite;
469
+ pushUnique(session.models, `${event.providerID}/${event.modelID}`);
470
+ pushUnique(session.agents, event.agent);
471
+ }
472
+ function applyToolUsage(stats, event) {
473
+ stats.count += 1;
474
+ stats.totalDurationMs += event.durationMs;
475
+ if (event.ok) {
476
+ stats.ok += 1;
477
+ } else {
478
+ stats.failed += 1;
479
+ }
480
+ }
481
+ function applyToolcall(session, event) {
482
+ applyToolUsage(session.toolcalls, event);
483
+ const byTool = session.toolcalls.byTool[event.tool] ?? {
484
+ count: 0,
485
+ ok: 0,
486
+ failed: 0,
487
+ totalDurationMs: 0
488
+ };
489
+ applyToolUsage(byTool, event);
490
+ session.toolcalls.byTool[event.tool] = byTool;
491
+ pushUnique(session.agents, event.agent);
492
+ }
493
+ function mergeTool(target, source) {
494
+ target.count += source.count;
495
+ target.ok += source.ok;
496
+ target.failed += source.failed;
497
+ target.totalDurationMs += source.totalDurationMs;
498
+ }
499
+ function accumulateTotals(totals, session) {
500
+ totals.sessions += 1;
501
+ totals.routeCount += session.routeCount;
502
+ totals.localCount += session.localCount;
503
+ totals.frontierCount += session.frontierCount;
504
+ totals.estimatedCostUSD += session.estimatedCostUSD;
505
+ totals.estimatedSavingsUSD += session.estimatedSavingsUSD;
506
+ totals.realCostUSD += session.realCostUSD;
507
+ totals.messageCount += session.messageCount;
508
+ totals.tokensIn += session.tokensIn;
509
+ totals.tokensOut += session.tokensOut;
510
+ totals.tokensReasoning += session.tokensReasoning;
511
+ totals.tokensCacheRead += session.tokensCacheRead;
512
+ totals.tokensCacheWrite += session.tokensCacheWrite;
513
+ mergeTool(totals.toolcalls, session.toolcalls);
514
+ for (const [tool, usage] of Object.entries(session.toolcalls.byTool)) {
515
+ const target = totals.toolcalls.byTool[tool] ?? {
516
+ count: 0,
517
+ ok: 0,
518
+ failed: 0,
519
+ totalDurationMs: 0
520
+ };
521
+ mergeTool(target, usage);
522
+ totals.toolcalls.byTool[tool] = target;
523
+ }
524
+ }
525
+ function toDecision(event) {
526
+ const view = {
527
+ ts: event.ts,
528
+ sessionID: event.sessionID,
529
+ summary: event.summary
530
+ };
531
+ if (event.agent !== undefined) {
532
+ view.agent = event.agent;
533
+ }
534
+ if (event.tags !== undefined) {
535
+ view.tags = [...event.tags];
536
+ }
537
+ return view;
538
+ }
539
+ function toMeeting(event) {
540
+ const view = {
541
+ ts: event.ts,
542
+ sessionID: event.sessionID,
543
+ batchID: event.batchID,
544
+ roles: [...event.roles]
545
+ };
546
+ if (event.purpose !== undefined) {
547
+ view.purpose = event.purpose;
548
+ }
549
+ if (event.decisionIDs !== undefined) {
550
+ view.decisionIDs = [...event.decisionIDs];
551
+ }
552
+ return view;
553
+ }
554
+ function toActivity(event) {
555
+ const view = {
556
+ ts: event.ts,
557
+ sessionID: event.sessionID,
558
+ kind: event.kind,
559
+ summary: event.summary
560
+ };
561
+ if (event.agent !== undefined) {
562
+ view.agent = event.agent;
563
+ }
564
+ return view;
565
+ }
566
+ function byTsDesc(list) {
567
+ return [...list].sort((a, b) => b.ts - a.ts);
568
+ }
569
+ function aggregateEvents(events) {
570
+ const sessions = new Map;
571
+ const decisions = [];
572
+ const meetings = [];
573
+ const activity = [];
574
+ const sessionFor = (sessionID, ts) => {
575
+ const existing = sessions.get(sessionID);
576
+ if (existing === undefined) {
577
+ const created = emptySession(sessionID, ts);
578
+ sessions.set(sessionID, created);
579
+ return created;
580
+ }
581
+ existing.firstTs = Math.min(existing.firstTs, ts);
582
+ existing.lastTs = Math.max(existing.lastTs, ts);
583
+ return existing;
584
+ };
585
+ for (const event of events) {
586
+ switch (event.type) {
587
+ case "route":
588
+ applyRoute(sessionFor(event.sessionID, event.ts), event);
589
+ break;
590
+ case "message":
591
+ applyMessage(sessionFor(event.sessionID, event.ts), event);
592
+ break;
593
+ case "toolcall":
594
+ applyToolcall(sessionFor(event.sessionID, event.ts), event);
595
+ break;
596
+ case "decision":
597
+ decisions.push(toDecision(event));
598
+ break;
599
+ case "meeting":
600
+ meetings.push(toMeeting(event));
601
+ break;
602
+ case "activity":
603
+ activity.push(toActivity(event));
604
+ break;
605
+ }
606
+ }
607
+ const totals = {
608
+ sessions: 0,
609
+ routeCount: 0,
610
+ localCount: 0,
611
+ frontierCount: 0,
612
+ estimatedCostUSD: 0,
613
+ estimatedSavingsUSD: 0,
614
+ realCostUSD: 0,
615
+ messageCount: 0,
616
+ tokensIn: 0,
617
+ tokensOut: 0,
618
+ tokensReasoning: 0,
619
+ tokensCacheRead: 0,
620
+ tokensCacheWrite: 0,
621
+ toolcalls: emptyToolcallStats()
622
+ };
623
+ const summaries = [...sessions.values()].sort((a, b) => b.lastTs - a.lastTs);
624
+ for (const summary of summaries) {
625
+ summary.models.sort();
626
+ summary.agents.sort();
627
+ accumulateTotals(totals, summary);
628
+ }
629
+ return {
630
+ sessions: summaries,
631
+ totals,
632
+ decisions: byTsDesc(decisions),
633
+ meetings: byTsDesc(meetings),
634
+ activity: byTsDesc(activity)
635
+ };
636
+ }
637
+
638
+ // src/telemetry/events.ts
639
+ import { z as z3 } from "zod";
640
+
641
+ // src/capabilities/types.ts
642
+ import { z } from "zod";
643
+ var CapabilityTierSchema = z.union([
644
+ z.literal(0),
645
+ z.literal(1),
646
+ z.literal(2),
647
+ z.literal(3),
648
+ z.literal(4),
649
+ z.literal(5)
650
+ ]);
651
+ var ComplexityTierSchema = z.enum([
652
+ "trivial",
653
+ "simple",
654
+ "moderate",
655
+ "hard"
656
+ ]);
657
+ var ModelCapabilityProfileSchema = z.object({
658
+ ref: z.object({
659
+ providerID: z.string().min(1),
660
+ modelID: z.string().min(1)
661
+ }),
662
+ kind: z.enum(["local", "frontier", "router"]),
663
+ contextWindow: z.number().int().positive(),
664
+ maxOutputTokens: z.number().int().positive(),
665
+ supportsToolCalling: z.boolean(),
666
+ supportsVision: z.boolean(),
667
+ reasoningTier: CapabilityTierSchema,
668
+ codeQualityTier: CapabilityTierSchema,
669
+ costPer1M: z.object({
670
+ inputUSD: z.number().min(0),
671
+ outputUSD: z.number().min(0)
672
+ }),
673
+ availability: z.enum(["available", "degraded", "unavailable"])
674
+ });
675
+
676
+ // src/config/schema.ts
677
+ import { z as z2 } from "zod";
678
+ var ModelRefSchema = z2.object({
679
+ providerID: z2.string().min(1),
680
+ modelID: z2.string().min(1)
681
+ });
682
+ var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
683
+ var PrivacyModeSchema = z2.enum([
684
+ "forceLocalOnSensitive",
685
+ "consentBeforeFrontier",
686
+ "off"
687
+ ]);
688
+ var BaselineModeSchema = z2.enum(["auto", "pinned"]);
689
+ var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
690
+ var DashboardConfigSchema = z2.object({
691
+ enabled: z2.boolean().default(false),
692
+ host: DashboardHostSchema.default("127.0.0.1"),
693
+ port: z2.number().int().min(1024).max(65535).default(4599),
694
+ autoPortFallback: z2.boolean().default(true),
695
+ refreshMs: z2.number().int().min(250).default(2000),
696
+ recentRoutes: z2.number().int().positive().default(50),
697
+ openBrowser: z2.boolean().default(false)
698
+ }).default({
699
+ enabled: false,
700
+ host: "127.0.0.1",
701
+ port: 4599,
702
+ autoPortFallback: true,
703
+ refreshMs: 2000,
704
+ recentRoutes: 50,
705
+ openBrowser: false
706
+ });
707
+ var defaultLocalModel = {
708
+ providerID: "ollama",
709
+ modelID: "qwen3:8b"
710
+ };
711
+ var defaultFrontierModel = {
712
+ providerID: "anthropic",
713
+ modelID: "claude-sonnet-4-5"
714
+ };
715
+ var LocalRuntimeSchema = z2.object({
716
+ id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
717
+ enabled: z2.boolean().default(true),
718
+ baseURL: z2.string().url().optional(),
719
+ discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
720
+ defaultModel: ModelRefSchema
721
+ });
722
+ var OpenTeamConfigSchema = z2.object({
723
+ baseline: z2.object({
724
+ mode: BaselineModeSchema.default("auto"),
725
+ pinnedModel: ModelRefSchema.nullable().default(null),
726
+ hardDefault: ModelRefSchema.default(defaultFrontierModel)
727
+ }).default({
728
+ mode: "auto",
729
+ pinnedModel: null,
730
+ hardDefault: defaultFrontierModel
731
+ }),
732
+ router: z2.object({
733
+ mode: RouterModeSchema.default("balanced"),
734
+ localDefault: ModelRefSchema.default(defaultLocalModel),
735
+ trivialPromptMaxChars: z2.number().int().positive().default(280),
736
+ frontierPromptMinChars: z2.number().int().positive().default(2000)
737
+ }).default({
738
+ mode: "balanced",
739
+ localDefault: defaultLocalModel,
740
+ trivialPromptMaxChars: 280,
741
+ frontierPromptMinChars: 2000
742
+ }),
743
+ local: z2.object({
744
+ runtimes: z2.array(LocalRuntimeSchema).min(1).default([
745
+ {
746
+ id: "ollama",
747
+ enabled: true,
748
+ baseURL: "http://localhost:11434/v1",
749
+ defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
750
+ }
751
+ ])
752
+ }).default({
753
+ runtimes: [
754
+ {
755
+ id: "ollama",
756
+ enabled: true,
757
+ baseURL: "http://localhost:11434/v1",
758
+ defaultModel: defaultLocalModel
759
+ }
760
+ ]
761
+ }),
762
+ budgets: z2.object({
763
+ sessionUSD: z2.number().positive().optional(),
764
+ monthlyUSD: z2.number().positive().optional(),
765
+ frontierTokensPerSession: z2.number().int().positive().optional(),
766
+ hardStopOnBudgetExhaustion: z2.boolean().default(false)
767
+ }).default({ hardStopOnBudgetExhaustion: false }),
768
+ privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
769
+ dashboard: DashboardConfigSchema
770
+ });
771
+
772
+ // src/telemetry/events.ts
773
+ var EVENT_SCHEMA_VERSION = 1;
774
+ var EventBaseSchema = z3.object({
775
+ v: z3.literal(EVENT_SCHEMA_VERSION),
776
+ ts: z3.number().finite(),
777
+ sessionID: z3.string().min(1)
778
+ });
779
+ var RouteEventSchema = EventBaseSchema.extend({
780
+ type: z3.literal("route"),
781
+ promptHash: z3.string().min(1),
782
+ promptChars: z3.number().int().min(0),
783
+ tier: ComplexityTierSchema,
784
+ routeKind: z3.enum(["local", "frontier"]),
785
+ selected: ModelRefSchema,
786
+ rationale: z3.string(),
787
+ estimatedCostUSD: z3.number().finite().min(0),
788
+ baselineCostUSD: z3.number().finite().min(0),
789
+ estimatedSavingsUSD: z3.number().finite(),
790
+ budgetAction: z3.string().min(1),
791
+ tokensIn: z3.number().int().min(0).optional(),
792
+ tokensOut: z3.number().int().min(0).optional(),
793
+ decisionID: z3.string().min(1).optional(),
794
+ agent: z3.string().min(1).optional(),
795
+ success: z3.boolean().optional(),
796
+ batchID: z3.string().min(1).optional(),
797
+ failureReason: z3.string().min(1).optional(),
798
+ failureStage: z3.string().min(1).optional()
799
+ });
800
+ var MessageEventSchema = EventBaseSchema.extend({
801
+ type: z3.literal("message"),
802
+ providerID: z3.string().min(1),
803
+ modelID: z3.string().min(1),
804
+ agent: z3.string().min(1).optional(),
805
+ mode: z3.string().min(1).optional(),
806
+ messageID: z3.string().min(1).optional(),
807
+ costUSD: z3.number().finite().min(0),
808
+ tokensIn: z3.number().int().min(0),
809
+ tokensOut: z3.number().int().min(0),
810
+ tokensReasoning: z3.number().int().min(0),
811
+ tokensCacheRead: z3.number().int().min(0),
812
+ tokensCacheWrite: z3.number().int().min(0),
813
+ durationMs: z3.number().int().min(0)
814
+ });
815
+ var ToolcallEventSchema = EventBaseSchema.extend({
816
+ type: z3.literal("toolcall"),
817
+ tool: z3.string().min(1),
818
+ callID: z3.string().min(1),
819
+ agent: z3.string().min(1).optional(),
820
+ durationMs: z3.number().int().min(0),
821
+ ok: z3.boolean(),
822
+ title: z3.string().optional()
823
+ });
824
+ var MeetingEventSchema = EventBaseSchema.extend({
825
+ type: z3.literal("meeting"),
826
+ batchID: z3.string().min(1),
827
+ purpose: z3.string().optional(),
828
+ roles: z3.array(z3.string().min(1)),
829
+ decisionIDs: z3.array(z3.string().min(1)).optional()
830
+ });
831
+ var DecisionEventSchema = EventBaseSchema.extend({
832
+ type: z3.literal("decision"),
833
+ agent: z3.string().min(1).optional(),
834
+ summary: z3.string().min(1),
835
+ tags: z3.array(z3.string().min(1)).optional()
836
+ });
837
+ var ActivityEventSchema = EventBaseSchema.extend({
838
+ type: z3.literal("activity"),
839
+ kind: z3.enum(["route", "agent", "decision", "commit"]),
840
+ agent: z3.string().min(1).optional(),
841
+ summary: z3.string().min(1)
842
+ });
843
+ var OpenTeamEventSchema = z3.discriminatedUnion("type", [
844
+ RouteEventSchema,
845
+ MessageEventSchema,
846
+ ToolcallEventSchema,
847
+ MeetingEventSchema,
848
+ DecisionEventSchema,
849
+ ActivityEventSchema
850
+ ]);
851
+
852
+ // src/telemetry/types.ts
853
+ import { z as z4 } from "zod";
854
+ var CostRecordSchema = z4.object({
855
+ ts: z4.number().finite(),
856
+ sessionID: z4.string().min(1).optional(),
857
+ promptHash: z4.string().min(1),
858
+ promptChars: z4.number().int().min(0),
859
+ tier: ComplexityTierSchema,
860
+ routeKind: z4.enum(["local", "frontier"]),
861
+ selected: ModelRefSchema,
862
+ rationale: z4.string(),
863
+ estimatedCostUSD: z4.number().finite().min(0),
864
+ baselineCostUSD: z4.number().finite().min(0),
865
+ estimatedSavingsUSD: z4.number().finite(),
866
+ budgetAction: z4.string().min(1),
867
+ tokensIn: z4.number().int().min(0).optional(),
868
+ tokensOut: z4.number().int().min(0).optional()
869
+ });
870
+
871
+ // src/telemetry/read.ts
872
+ var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
873
+ function parseCostRecordsJsonl(text) {
874
+ const records = [];
875
+ for (const line of text.split(`
876
+ `)) {
877
+ const trimmed = line.trim();
878
+ if (trimmed.length === 0) {
879
+ continue;
880
+ }
881
+ let candidate;
882
+ try {
883
+ candidate = JSON.parse(trimmed);
884
+ } catch {
885
+ continue;
886
+ }
887
+ const parsed = CostRecordSchema.safeParse(candidate);
888
+ if (parsed.success) {
889
+ records.push(parsed.data);
890
+ }
891
+ }
892
+ return records;
893
+ }
894
+
895
+ // src/telemetry/eventLog.ts
896
+ var DEFAULT_SESSIONS_DIR = ".opencode/openteam/sessions";
897
+ var LEGACY_SESSION_ID = "legacy";
898
+ function defaultJoin(dir, file) {
899
+ return dir.endsWith("/") ? `${dir}${file}` : `${dir}/${file}`;
900
+ }
901
+ function parseEventsJsonl(text) {
902
+ const events = [];
903
+ for (const line of text.split(`
904
+ `)) {
905
+ const trimmed = line.trim();
906
+ if (trimmed.length === 0) {
907
+ continue;
908
+ }
909
+ let candidate;
910
+ try {
911
+ candidate = JSON.parse(trimmed);
912
+ } catch {
913
+ continue;
914
+ }
915
+ const parsed = OpenTeamEventSchema.safeParse(candidate);
916
+ if (parsed.success) {
917
+ events.push(parsed.data);
918
+ }
919
+ }
920
+ return events;
302
921
  }
303
- function usd2(value) {
304
- return `$${value.toFixed(5)}`;
922
+ function costRecordToRouteEvent(record) {
923
+ const event = {
924
+ v: EVENT_SCHEMA_VERSION,
925
+ type: "route",
926
+ ts: record.ts,
927
+ sessionID: record.sessionID ?? LEGACY_SESSION_ID,
928
+ promptHash: record.promptHash,
929
+ promptChars: record.promptChars,
930
+ tier: record.tier,
931
+ routeKind: record.routeKind,
932
+ selected: record.selected,
933
+ rationale: record.rationale,
934
+ estimatedCostUSD: record.estimatedCostUSD,
935
+ baselineCostUSD: record.baselineCostUSD,
936
+ estimatedSavingsUSD: record.estimatedSavingsUSD,
937
+ budgetAction: record.budgetAction
938
+ };
939
+ if (record.tokensIn !== undefined) {
940
+ event.tokensIn = record.tokensIn;
941
+ }
942
+ if (record.tokensOut !== undefined) {
943
+ event.tokensOut = record.tokensOut;
944
+ }
945
+ return event;
946
+ }
947
+ function routeEventToCostRecord(event) {
948
+ const record = {
949
+ ts: event.ts,
950
+ sessionID: event.sessionID,
951
+ promptHash: event.promptHash,
952
+ promptChars: event.promptChars,
953
+ tier: event.tier,
954
+ routeKind: event.routeKind,
955
+ selected: event.selected,
956
+ rationale: event.rationale,
957
+ estimatedCostUSD: event.estimatedCostUSD,
958
+ baselineCostUSD: event.baselineCostUSD,
959
+ estimatedSavingsUSD: event.estimatedSavingsUSD,
960
+ budgetAction: event.budgetAction
961
+ };
962
+ if (event.tokensIn !== undefined) {
963
+ record.tokensIn = event.tokensIn;
964
+ }
965
+ if (event.tokensOut !== undefined) {
966
+ record.tokensOut = event.tokensOut;
967
+ }
968
+ return record;
305
969
  }
306
- function renderReport(report) {
307
- if (report.count === 0) {
308
- return "openteam report: sin registros de telemetría todavía.";
970
+ async function readRouteCostRecords(dir, deps) {
971
+ const events = await readSessionEvents(dir, deps);
972
+ const records = [];
973
+ for (const event of events) {
974
+ if (event.type === "route") {
975
+ records.push(routeEventToCostRecord(event));
976
+ }
309
977
  }
310
- const lines = [
311
- "openteam report:",
312
- ` decisiones: ${report.count} (local ${report.localCount} · frontier ${report.frontierCount})`,
313
- ` coste real: ${usd2(report.totalEstimatedUSD)}`,
314
- ` baseline: ${usd2(report.totalBaselineUSD)}`,
315
- ` ahorro: ${usd2(report.totalSavingsUSD)} (${report.savingsPct.toFixed(2)}%)`,
316
- ` tokens: in ${report.tokensIn} · out ${report.tokensOut}`,
317
- " por tier:"
318
- ];
319
- for (const tier of TIERS2) {
320
- const summary = report.byTier[tier];
321
- lines.push(` ${tier.padEnd(9)} ${summary.count} · coste ${usd2(summary.estimatedUSD)} · ahorro ${usd2(summary.savingsUSD)}`);
978
+ return records;
979
+ }
980
+ async function readSessionEvents(dir, deps) {
981
+ const join = deps.join ?? defaultJoin;
982
+ const files = (await deps.listFiles(dir)).filter((file) => file.endsWith(".jsonl"));
983
+ const perFile = await Promise.all(files.map(async (file) => {
984
+ const text = await deps.readText(join(dir, file));
985
+ return text === undefined ? [] : parseEventsJsonl(text);
986
+ }));
987
+ const events = perFile.flat();
988
+ if (deps.legacyTelemetryPath !== undefined) {
989
+ const legacyText = await deps.readText(deps.legacyTelemetryPath);
990
+ if (legacyText !== undefined) {
991
+ for (const record of parseCostRecordsJsonl(legacyText)) {
992
+ events.push(costRecordToRouteEvent(record));
993
+ }
994
+ }
322
995
  }
323
- return lines.join(`
324
- `);
996
+ return events.sort((a, b) => a.ts - b.ts);
325
997
  }
326
998
 
327
999
  // src/dashboard/backlog.ts
@@ -375,6 +1047,8 @@ function buildLoopSnapshot(backlogPath, items) {
375
1047
  // src/dashboard/state.ts
376
1048
  var DEFAULT_RECENT_ROUTES = 50;
377
1049
  var DEFAULT_ACTIVITY_LIMIT = 100;
1050
+ var DEFAULT_DECISIONS_LIMIT = 100;
1051
+ var DEFAULT_MEETINGS_LIMIT = 50;
378
1052
  function toRouteView(record) {
379
1053
  return {
380
1054
  ts: record.ts,
@@ -387,11 +1061,49 @@ function toRouteView(record) {
387
1061
  budgetAction: record.budgetAction
388
1062
  };
389
1063
  }
1064
+ function isRoute(event) {
1065
+ return event.type === "route";
1066
+ }
390
1067
  function recentRoutes(records, limit) {
391
1068
  return [...records].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit)).map(toRouteView);
392
1069
  }
393
- function recentActivity(entries, limit) {
394
- return [...entries].sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
1070
+ function decisionToActivity(decision) {
1071
+ const entry = {
1072
+ ts: decision.ts,
1073
+ kind: "decision",
1074
+ summary: decision.summary
1075
+ };
1076
+ if (decision.agent !== undefined) {
1077
+ entry.agent = decision.agent;
1078
+ }
1079
+ return entry;
1080
+ }
1081
+ function meetingToActivity(meeting) {
1082
+ const roles = meeting.roles.join(", ");
1083
+ const purpose = meeting.purpose === undefined ? "" : `: ${meeting.purpose}`;
1084
+ return {
1085
+ ts: meeting.ts,
1086
+ kind: "agent",
1087
+ summary: `Reunión (${roles})${purpose}`
1088
+ };
1089
+ }
1090
+ function activityFeed(aggregate, limit) {
1091
+ const entries = [
1092
+ ...aggregate.activity.map((view) => {
1093
+ const entry = {
1094
+ ts: view.ts,
1095
+ kind: view.kind,
1096
+ summary: view.summary
1097
+ };
1098
+ if (view.agent !== undefined) {
1099
+ entry.agent = view.agent;
1100
+ }
1101
+ return entry;
1102
+ }),
1103
+ ...aggregate.decisions.map(decisionToActivity),
1104
+ ...aggregate.meetings.map(meetingToActivity)
1105
+ ];
1106
+ return entries.sort((a, b) => b.ts - a.ts).slice(0, Math.max(0, limit));
395
1107
  }
396
1108
  function loopFrom(backlog) {
397
1109
  if (backlog === undefined) {
@@ -406,13 +1118,21 @@ function loopFrom(backlog) {
406
1118
  function buildDashboardState(inputs) {
407
1119
  const recentRoutesLimit = inputs.recentRoutesLimit ?? DEFAULT_RECENT_ROUTES;
408
1120
  const activityLimit = inputs.activityLimit ?? DEFAULT_ACTIVITY_LIMIT;
1121
+ const decisionsLimit = inputs.decisionsLimit ?? DEFAULT_DECISIONS_LIMIT;
1122
+ const meetingsLimit = inputs.meetingsLimit ?? DEFAULT_MEETINGS_LIMIT;
1123
+ const aggregate = aggregateEvents(inputs.events);
1124
+ const costRecords = inputs.events.filter(isRoute).map(routeEventToCostRecord);
409
1125
  const state = {
410
1126
  generatedAt: inputs.generatedAt,
411
1127
  session: {},
412
- cost: summarizeCostRecords(inputs.costRecords),
413
- recentRoutes: recentRoutes(inputs.costRecords, recentRoutesLimit),
1128
+ cost: summarizeCostRecords(costRecords),
1129
+ totals: aggregate.totals,
1130
+ sessions: aggregate.sessions,
1131
+ recentRoutes: recentRoutes(costRecords, recentRoutesLimit),
1132
+ decisions: aggregate.decisions.slice(0, Math.max(0, decisionsLimit)),
1133
+ meetings: aggregate.meetings.slice(0, Math.max(0, meetingsLimit)),
414
1134
  team: [...inputs.team],
415
- activity: recentActivity(inputs.activity, activityLimit)
1135
+ activity: activityFeed(aggregate, activityLimit)
416
1136
  };
417
1137
  if (inputs.session?.id !== undefined) {
418
1138
  state.session.id = inputs.session.id;
@@ -703,216 +1423,95 @@ function renderAgentList(classifications, context) {
703
1423
  `);
704
1424
  }
705
1425
 
706
- // src/telemetry/read.ts
707
- import { readFile } from "node:fs/promises";
708
-
709
- // src/telemetry/types.ts
710
- import { z as z3 } from "zod";
711
-
712
- // src/capabilities/types.ts
713
- import { z } from "zod";
714
- var CapabilityTierSchema = z.union([
715
- z.literal(0),
716
- z.literal(1),
717
- z.literal(2),
718
- z.literal(3),
719
- z.literal(4),
720
- z.literal(5)
721
- ]);
722
- var ComplexityTierSchema = z.enum([
723
- "trivial",
724
- "simple",
725
- "moderate",
726
- "hard"
727
- ]);
728
- var ModelCapabilityProfileSchema = z.object({
729
- ref: z.object({
730
- providerID: z.string().min(1),
731
- modelID: z.string().min(1)
732
- }),
733
- kind: z.enum(["local", "frontier", "router"]),
734
- contextWindow: z.number().int().positive(),
735
- maxOutputTokens: z.number().int().positive(),
736
- supportsToolCalling: z.boolean(),
737
- supportsVision: z.boolean(),
738
- reasoningTier: CapabilityTierSchema,
739
- codeQualityTier: CapabilityTierSchema,
740
- costPer1M: z.object({
741
- inputUSD: z.number().min(0),
742
- outputUSD: z.number().min(0)
743
- }),
744
- availability: z.enum(["available", "degraded", "unavailable"])
745
- });
746
-
747
- // src/config/schema.ts
748
- import { z as z2 } from "zod";
749
- var ModelRefSchema = z2.object({
750
- providerID: z2.string().min(1),
751
- modelID: z2.string().min(1)
752
- });
753
- var RouterModeSchema = z2.enum(["economy", "balanced", "quality"]);
754
- var PrivacyModeSchema = z2.enum([
755
- "forceLocalOnSensitive",
756
- "consentBeforeFrontier",
757
- "off"
758
- ]);
759
- var BaselineModeSchema = z2.enum(["auto", "pinned"]);
760
- var DashboardHostSchema = z2.enum(["127.0.0.1", "localhost"]);
761
- var DashboardConfigSchema = z2.object({
762
- enabled: z2.boolean().default(false),
763
- host: DashboardHostSchema.default("127.0.0.1"),
764
- port: z2.number().int().min(1024).max(65535).default(4599),
765
- autoPortFallback: z2.boolean().default(true),
766
- refreshMs: z2.number().int().min(250).default(2000),
767
- recentRoutes: z2.number().int().positive().default(50),
768
- openBrowser: z2.boolean().default(false)
769
- }).default({
770
- enabled: false,
771
- host: "127.0.0.1",
772
- port: 4599,
773
- autoPortFallback: true,
774
- refreshMs: 2000,
775
- recentRoutes: 50,
776
- openBrowser: false
777
- });
778
- var defaultLocalModel = {
779
- providerID: "ollama",
780
- modelID: "qwen3:8b"
781
- };
782
- var defaultFrontierModel = {
783
- providerID: "anthropic",
784
- modelID: "claude-sonnet-4-5"
785
- };
786
- var LocalRuntimeSchema = z2.object({
787
- id: z2.enum(["ollama", "lmstudio", "foundry-local"]),
788
- enabled: z2.boolean().default(true),
789
- baseURL: z2.string().url().optional(),
790
- discovery: z2.enum(["cli", "sdk", "manual"]).optional(),
791
- defaultModel: ModelRefSchema
792
- });
793
- var OpenTeamConfigSchema = z2.object({
794
- baseline: z2.object({
795
- mode: BaselineModeSchema.default("auto"),
796
- pinnedModel: ModelRefSchema.nullable().default(null),
797
- hardDefault: ModelRefSchema.default(defaultFrontierModel)
798
- }).default({
799
- mode: "auto",
800
- pinnedModel: null,
801
- hardDefault: defaultFrontierModel
802
- }),
803
- router: z2.object({
804
- mode: RouterModeSchema.default("balanced"),
805
- localDefault: ModelRefSchema.default(defaultLocalModel),
806
- trivialPromptMaxChars: z2.number().int().positive().default(280),
807
- frontierPromptMinChars: z2.number().int().positive().default(2000)
808
- }).default({
809
- mode: "balanced",
810
- localDefault: defaultLocalModel,
811
- trivialPromptMaxChars: 280,
812
- frontierPromptMinChars: 2000
813
- }),
814
- local: z2.object({
815
- runtimes: z2.array(LocalRuntimeSchema).min(1).default([
816
- {
817
- id: "ollama",
818
- enabled: true,
819
- baseURL: "http://localhost:11434/v1",
820
- defaultModel: { providerID: "ollama", modelID: "qwen3:8b" }
821
- }
822
- ])
823
- }).default({
824
- runtimes: [
825
- {
826
- id: "ollama",
827
- enabled: true,
828
- baseURL: "http://localhost:11434/v1",
829
- defaultModel: defaultLocalModel
830
- }
831
- ]
832
- }),
833
- budgets: z2.object({
834
- sessionUSD: z2.number().positive().optional(),
835
- monthlyUSD: z2.number().positive().optional(),
836
- frontierTokensPerSession: z2.number().int().positive().optional(),
837
- hardStopOnBudgetExhaustion: z2.boolean().default(false)
838
- }).default({ hardStopOnBudgetExhaustion: false }),
839
- privacyMode: PrivacyModeSchema.default("forceLocalOnSensitive"),
840
- dashboard: DashboardConfigSchema
841
- });
842
-
843
- // src/telemetry/types.ts
844
- var CostRecordSchema = z3.object({
845
- ts: z3.number().finite(),
846
- sessionID: z3.string().min(1).optional(),
847
- promptHash: z3.string().min(1),
848
- promptChars: z3.number().int().min(0),
849
- tier: ComplexityTierSchema,
850
- routeKind: z3.enum(["local", "frontier"]),
851
- selected: ModelRefSchema,
852
- rationale: z3.string(),
853
- estimatedCostUSD: z3.number().finite().min(0),
854
- baselineCostUSD: z3.number().finite().min(0),
855
- estimatedSavingsUSD: z3.number().finite(),
856
- budgetAction: z3.string().min(1),
857
- tokensIn: z3.number().int().min(0).optional(),
858
- tokensOut: z3.number().int().min(0).optional()
859
- });
860
-
861
- // src/telemetry/read.ts
862
- var DEFAULT_TELEMETRY_PATH = ".opencode/openteam-telemetry.jsonl";
863
- function parseCostRecordsJsonl(text) {
864
- const records = [];
1426
+ // src/telemetry/decisions.ts
1427
+ var DECISIONS_SESSION_ID = "decisions";
1428
+ var BULLET_RE = /^\s*[-*]\s+(.+?)\s*$/;
1429
+ var DATE_RE = /^(\d{4}-\d{2}-\d{2})\b\s*/;
1430
+ var AGENT_RE = /^\[([^\]]+)\]\s*/;
1431
+ var TAG_RE = /#([A-Za-z0-9][\w-]*)/g;
1432
+ function parseDate(value) {
1433
+ const parsed = Date.parse(`${value}T00:00:00Z`);
1434
+ return Number.isNaN(parsed) ? undefined : parsed;
1435
+ }
1436
+ function parseDecisionsMarkdown(text, options = {}) {
1437
+ const sessionID = options.sessionID ?? DECISIONS_SESSION_ID;
1438
+ const baseTs = options.baseTs ?? 0;
1439
+ const events = [];
1440
+ let index = 0;
865
1441
  for (const line of text.split(`
866
1442
  `)) {
867
- const trimmed = line.trim();
868
- if (trimmed.length === 0) {
1443
+ const bullet = BULLET_RE.exec(line);
1444
+ if (bullet === null) {
869
1445
  continue;
870
1446
  }
871
- let candidate;
872
- try {
873
- candidate = JSON.parse(trimmed);
874
- } catch {
1447
+ let content = bullet[1] ?? "";
1448
+ let ts = baseTs + index;
1449
+ const dateMatch = DATE_RE.exec(content);
1450
+ if (dateMatch?.[1] !== undefined) {
1451
+ const parsed = parseDate(dateMatch[1]);
1452
+ if (parsed !== undefined) {
1453
+ ts = parsed;
1454
+ content = content.slice(dateMatch[0].length);
1455
+ }
1456
+ }
1457
+ let agent;
1458
+ const agentMatch = AGENT_RE.exec(content);
1459
+ if (agentMatch?.[1] !== undefined) {
1460
+ agent = agentMatch[1].trim();
1461
+ content = content.slice(agentMatch[0].length);
1462
+ }
1463
+ const tags = [];
1464
+ for (const tagMatch of content.matchAll(TAG_RE)) {
1465
+ const tag = tagMatch[1];
1466
+ if (tag !== undefined && !tags.includes(tag)) {
1467
+ tags.push(tag);
1468
+ }
1469
+ }
1470
+ const summary = content.replace(TAG_RE, "").replace(/\s+/g, " ").trim();
1471
+ if (summary.length === 0) {
875
1472
  continue;
876
1473
  }
877
- const parsed = CostRecordSchema.safeParse(candidate);
878
- if (parsed.success) {
879
- records.push(parsed.data);
1474
+ const event = {
1475
+ v: EVENT_SCHEMA_VERSION,
1476
+ type: "decision",
1477
+ ts,
1478
+ sessionID,
1479
+ summary
1480
+ };
1481
+ if (agent !== undefined && agent.length > 0) {
1482
+ event.agent = agent;
880
1483
  }
881
- }
882
- return records;
883
- }
884
- function isMissingFile(error) {
885
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
886
- }
887
- async function readCostRecords(path, deps = {}) {
888
- const readFileFn = deps.readFile ?? readFile;
889
- try {
890
- const content = await readFileFn(path, "utf8");
891
- return parseCostRecordsJsonl(content);
892
- } catch (error) {
893
- if (isMissingFile(error)) {
894
- return [];
1484
+ if (tags.length > 0) {
1485
+ event.tags = tags;
895
1486
  }
896
- throw error;
1487
+ events.push(event);
1488
+ index += 1;
897
1489
  }
1490
+ return events;
898
1491
  }
899
1492
 
900
1493
  // src/web/snapshot.ts
901
- function createSnapshotReader(deps, paths, context) {
1494
+ function createSnapshotReader(deps, paths, context = {}) {
1495
+ const readEventsDeps = {
1496
+ listFiles: deps.listFiles,
1497
+ readText: deps.readText,
1498
+ ...paths.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: paths.legacyTelemetryPath } : {}
1499
+ };
902
1500
  return async () => {
903
- const [telemetryText, backlogText, agentFiles, lastCommit] = await Promise.all([
904
- deps.readText(paths.telemetryPath),
1501
+ const [sessionEvents, decisionsText, backlogText, agentFiles, lastCommit] = await Promise.all([
1502
+ readSessionEvents(paths.sessionsDir, readEventsDeps),
1503
+ deps.readText(paths.decisionsPath),
905
1504
  deps.readText(paths.backlogPath),
906
1505
  deps.listAgentFiles(paths.agentDir),
907
1506
  deps.gitLastCommit?.() ?? Promise.resolve(undefined)
908
1507
  ]);
909
- const costRecords = parseCostRecordsJsonl(telemetryText ?? "");
1508
+ const decisionEvents = parseDecisionsMarkdown(decisionsText ?? "");
1509
+ const events = [...sessionEvents, ...decisionEvents];
910
1510
  const team = agentFiles.map(parseAgentFile);
911
1511
  const inputs = {
912
1512
  generatedAt: deps.now(),
913
- costRecords,
914
- team,
915
- activity: context.activity()
1513
+ events,
1514
+ team
916
1515
  };
917
1516
  if (backlogText !== undefined) {
918
1517
  const backlog = {
@@ -936,7 +1535,7 @@ function createSnapshotReader(deps, paths, context) {
936
1535
 
937
1536
  // src/web/watch.ts
938
1537
  function watchSources(paths, onChange, deps, debounceMs = 250) {
939
- const setTimer = deps.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
1538
+ const setTimer = deps.setTimer ?? ((cb, ms2) => setTimeout(cb, ms2));
940
1539
  const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
941
1540
  const watchers = [];
942
1541
  let pending;
@@ -973,18 +1572,20 @@ function startDashboard(deps) {
973
1572
  }
974
1573
  async function createDashboardRuntime(deps) {
975
1574
  const backlogPath = deps.backlogPath ?? DEFAULT_BACKLOG_PATH;
976
- const activity = createActivityBuffer(deps.config.recentRoutes);
1575
+ const decisionsPath = deps.decisionsPath ?? DEFAULT_DECISIONS_PATH;
977
1576
  const readSnapshot = createSnapshotReader({
978
1577
  readText: deps.readText,
1578
+ listFiles: deps.listFiles,
979
1579
  listAgentFiles: deps.listAgentFiles,
980
1580
  now: deps.now,
981
1581
  ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {}
982
1582
  }, {
983
- telemetryPath: deps.telemetryPath,
1583
+ sessionsDir: deps.sessionsDir,
1584
+ decisionsPath,
984
1585
  backlogPath,
985
- agentDir: deps.agentDir
1586
+ agentDir: deps.agentDir,
1587
+ ...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {}
986
1588
  }, {
987
- activity: () => activity.list(),
988
1589
  recentRoutesLimit: deps.config.recentRoutes,
989
1590
  ...deps.session !== undefined ? { session: deps.session } : {}
990
1591
  });
@@ -995,16 +1596,12 @@ async function createDashboardRuntime(deps) {
995
1596
  ...deps.log !== undefined ? { log: deps.log } : {}
996
1597
  });
997
1598
  const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
998
- const watcher = watchSources([deps.telemetryPath, backlogPath, deps.agentDir], () => {
1599
+ const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {
999
1600
  server.notify();
1000
1601
  }, { watch: watchFn });
1001
1602
  return {
1002
1603
  url: server.url,
1003
1604
  port: server.port,
1004
- pushActivity(entry) {
1005
- activity.push(entry);
1006
- server.notify();
1007
- },
1008
1605
  close() {
1009
1606
  watcher.close();
1010
1607
  server.close();
@@ -1015,6 +1612,7 @@ async function createDashboardRuntime(deps) {
1015
1612
  // src/cli/dashboardServe.ts
1016
1613
  function parseDashboardServeArgs(rest) {
1017
1614
  return {
1615
+ status: rest.includes("--status"),
1018
1616
  serve: rest.includes("--serve"),
1019
1617
  open: rest.includes("--open")
1020
1618
  };
@@ -1026,10 +1624,13 @@ async function runDashboardServe(deps, options) {
1026
1624
  try {
1027
1625
  runtime = await start({
1028
1626
  config: { ...config.dashboard, enabled: true },
1029
- telemetryPath: deps.telemetryPath,
1627
+ sessionsDir: deps.sessionsDir,
1628
+ decisionsPath: deps.decisionsPath,
1030
1629
  backlogPath: deps.backlogPath,
1031
1630
  agentDir: deps.agentDir,
1631
+ ...deps.legacyTelemetryPath !== undefined ? { legacyTelemetryPath: deps.legacyTelemetryPath } : {},
1032
1632
  readText: deps.readText,
1633
+ listFiles: deps.listFiles,
1033
1634
  listAgentFiles: deps.listAgentFiles,
1034
1635
  now: deps.now,
1035
1636
  ...deps.gitLastCommit !== undefined ? { gitLastCommit: deps.gitLastCommit } : {},
@@ -1868,29 +2469,19 @@ function autoBaseline(config) {
1868
2469
  // src/commands/dashboard.ts
1869
2470
  function renderDashboardStatus(dashboard) {
1870
2471
  const url = `http://${dashboard.host}:${dashboard.port}`;
1871
- if (!dashboard.enabled) {
1872
- return [
1873
- "Dashboard: deshabilitado.",
1874
- "",
1875
- 'Para activarlo dentro de opencode, pon "dashboard": { "enabled": true }',
1876
- "en .opencode/openteam.json y recarga opencode. Se servirá (loopback) en:",
1877
- ` ${url}`,
1878
- "",
1879
- "O véelo ahora mismo sin editar la config:",
1880
- " openteam dashboard --serve (añade --open para abrir el navegador)"
1881
- ].join(`
1882
- `);
1883
- }
1884
2472
  return [
1885
- "Dashboard: habilitado.",
2473
+ "Dashboard (multi-sesión, se lanza desde la CLI):",
1886
2474
  ` URL: ${url}`,
1887
2475
  ` Refresco: ${dashboard.refreshMs} ms (SSE + polling)`,
1888
2476
  ` Rutas: últimas ${dashboard.recentRoutes}`,
1889
2477
  dashboard.autoPortFallback ? " Puerto: con fallback automático si está ocupado" : " Puerto: fijo (sin fallback)",
1890
2478
  "",
1891
- "Nota: solo escucha en loopback y nunca expone prompts (solo hashes).",
1892
- "El servidor corre dentro de la sesión de opencode; se cierra al salir.",
1893
- "Para verlo fuera de opencode: openteam dashboard --serve"
2479
+ "Lánzalo con:",
2480
+ " openteam dashboard (Ctrl+C para parar; --open abre el navegador)",
2481
+ "",
2482
+ "Agrega TODAS las sesiones de opencode que escriben eventos en",
2483
+ " .opencode/openteam/sessions/*.jsonl",
2484
+ "Solo escucha en loopback y nunca expone prompts (solo hashes)."
1894
2485
  ].join(`
1895
2486
  `);
1896
2487
  }
@@ -2027,7 +2618,12 @@ function buildOrchestratorAgent(frontier, options = {}) {
2027
2618
  "- **scribe** — memoria silenciosa del equipo. Registra decisiones y",
2028
2619
  " aprendizajes en un log compartido (`.opencode/openteam-decisions.md`) sin",
2029
2620
  " ejecutar cambios de código. Modelo **local**; permisos de solo lectura más",
2030
- " edición de ese log.",
2621
+ " edición de ese log. **Formato parseable** para el dashboard: una decisión",
2622
+ " por línea como item de lista Markdown, ya **redactada** (sin prompts ni",
2623
+ " datos sensibles):",
2624
+ " `- YYYY-MM-DD [agente] Resumen breve de la decisión #tag1 #tag2`",
2625
+ " La fecha, el `[agente]` y los `#tags` son opcionales; el resumen es",
2626
+ " obligatorio.",
2031
2627
  "- **ralph** — automatización y triage. Monitoriza el trabajo pendiente",
2032
2628
  " (issues/tareas), lo prioriza, coordina la ejecución delegando en los",
2033
2629
  " especialistas y **escala al humano** ante bloqueos, riesgos o aprobaciones.",
@@ -2157,8 +2753,8 @@ var HELP = [
2157
2753
  " openteam baseline auto Baseline cheapest-capable (modo auto)",
2158
2754
  " openteam doctor Diagnóstico de runtimes y config",
2159
2755
  " openteam agents Lista los agentes y el LLM (local/frontier) de cada uno",
2160
- " openteam dashboard Estado y URL del dashboard web (loopback)",
2161
- " openteam dashboard --serve Levanta el dashboard web (Ctrl+C para parar; --open abre el navegador)",
2756
+ " openteam dashboard Lanza el dashboard web multi-sesión (Ctrl+C para parar; --open abre el navegador)",
2757
+ " openteam dashboard --status Muestra la config del dashboard sin lanzarlo",
2162
2758
  " openteam report Resumen de coste/ahorro (telemetría)",
2163
2759
  " openteam yolo status Muestra si el modo YOLO está activo",
2164
2760
  " openteam yolo on Activa YOLO (opencode auto-aprueba todo)",
@@ -2843,7 +3439,7 @@ function createGitLastCommit(exec) {
2843
3439
  // src/cli.ts
2844
3440
  var execFileAsync = promisify(execFile);
2845
3441
  var AGENT_DIR = dirname2(ORCHESTRATOR_AGENT_PATH);
2846
- function isMissingFile2(error) {
3442
+ function isMissingFile(error) {
2847
3443
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2848
3444
  }
2849
3445
  var nodeExec = async (command, args) => {
@@ -2861,10 +3457,10 @@ var nodeExec = async (command, args) => {
2861
3457
  };
2862
3458
  async function loadConfig(path) {
2863
3459
  try {
2864
- const content = await readFile2(path, "utf8");
3460
+ const content = await readFile(path, "utf8");
2865
3461
  return loadOpenTeamConfig(JSON.parse(content));
2866
3462
  } catch (error) {
2867
- if (isMissingFile2(error)) {
3463
+ if (isMissingFile(error)) {
2868
3464
  return loadOpenTeamConfig({});
2869
3465
  }
2870
3466
  throw error;
@@ -2872,14 +3468,24 @@ async function loadConfig(path) {
2872
3468
  }
2873
3469
  async function readTextOptional(path) {
2874
3470
  try {
2875
- return await readFile2(path, "utf8");
3471
+ return await readFile(path, "utf8");
2876
3472
  } catch (error) {
2877
- if (isMissingFile2(error)) {
3473
+ if (isMissingFile(error)) {
2878
3474
  return;
2879
3475
  }
2880
3476
  throw error;
2881
3477
  }
2882
3478
  }
3479
+ async function listDirFiles(dir) {
3480
+ try {
3481
+ return await readdir(dir);
3482
+ } catch (error) {
3483
+ if (isMissingFile(error)) {
3484
+ return [];
3485
+ }
3486
+ throw error;
3487
+ }
3488
+ }
2883
3489
  async function openBrowser(url) {
2884
3490
  if (process.platform === "win32") {
2885
3491
  await nodeExec("cmd", ["/c", "start", "", url]);
@@ -2901,10 +3507,10 @@ var deps = {
2901
3507
  saveConfig: (config, path) => writeOpenTeamConfigFile(config, path),
2902
3508
  readOpencodeConfig: async (path) => {
2903
3509
  try {
2904
- const content = await readFile2(path, "utf8");
3510
+ const content = await readFile(path, "utf8");
2905
3511
  return JSON.parse(content);
2906
3512
  } catch (error) {
2907
- if (isMissingFile2(error)) {
3513
+ if (isMissingFile(error)) {
2908
3514
  return;
2909
3515
  }
2910
3516
  throw error;
@@ -2924,7 +3530,7 @@ var deps = {
2924
3530
  try {
2925
3531
  entries = await readdir(dir);
2926
3532
  } catch (error) {
2927
- if (isMissingFile2(error)) {
3533
+ if (isMissingFile(error)) {
2928
3534
  return [];
2929
3535
  }
2930
3536
  throw error;
@@ -2932,7 +3538,7 @@ var deps = {
2932
3538
  const mdFiles = entries.filter((entry) => entry.endsWith(".md"));
2933
3539
  return Promise.all(mdFiles.map(async (entry) => ({
2934
3540
  name: entry.replace(/\.md$/, ""),
2935
- contents: await readFile2(join2(dir, entry), "utf8")
3541
+ contents: await readFile(join2(dir, entry), "utf8")
2936
3542
  })));
2937
3543
  },
2938
3544
  probe: (config) => {
@@ -2943,7 +3549,11 @@ var deps = {
2943
3549
  });
2944
3550
  return registry.probe(config.local.runtimes);
2945
3551
  },
2946
- readTelemetry: (path) => readCostRecords(path),
3552
+ readTelemetry: (path) => readRouteCostRecords(DEFAULT_SESSIONS_DIR, {
3553
+ listFiles: listDirFiles,
3554
+ readText: readTextOptional,
3555
+ legacyTelemetryPath: path
3556
+ }),
2947
3557
  configPath: DEFAULT_CONFIG_PATH,
2948
3558
  telemetryPath: DEFAULT_TELEMETRY_PATH,
2949
3559
  opencodeConfigPath: OPENCODE_CONFIG_PATH,
@@ -2983,14 +3593,17 @@ async function main() {
2983
3593
  }
2984
3594
  if (argv[0] === "dashboard") {
2985
3595
  const flags = parseDashboardServeArgs(argv.slice(1));
2986
- if (flags.serve) {
3596
+ if (!flags.status) {
2987
3597
  const result2 = await runDashboardServe({
2988
3598
  loadConfig,
2989
3599
  configPath: DEFAULT_CONFIG_PATH,
2990
- telemetryPath: DEFAULT_TELEMETRY_PATH,
3600
+ sessionsDir: DEFAULT_SESSIONS_DIR,
3601
+ decisionsPath: DEFAULT_DECISIONS_PATH,
2991
3602
  backlogPath: DEFAULT_BACKLOG_PATH,
2992
3603
  agentDir: AGENT_DIR,
3604
+ legacyTelemetryPath: DEFAULT_TELEMETRY_PATH,
2993
3605
  readText: readTextOptional,
3606
+ listFiles: listDirFiles,
2994
3607
  listAgentFiles: deps.listAgentFiles,
2995
3608
  now: () => new Date().toISOString(),
2996
3609
  gitLastCommit: createGitLastCommit(nodeExec),