instar 1.3.818 → 1.3.820

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 (92) hide show
  1. package/dashboard/glance.js +504 -6
  2. package/dashboard/index.html +158 -652
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +498 -3
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +69 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/core/BackupManager.d.ts +10 -0
  10. package/dist/core/BackupManager.d.ts.map +1 -1
  11. package/dist/core/BackupManager.js +16 -0
  12. package/dist/core/BackupManager.js.map +1 -1
  13. package/dist/core/BoundedJsonlAudit.d.ts +37 -0
  14. package/dist/core/BoundedJsonlAudit.d.ts.map +1 -0
  15. package/dist/core/BoundedJsonlAudit.js +87 -0
  16. package/dist/core/BoundedJsonlAudit.js.map +1 -0
  17. package/dist/core/JudgmentProvenanceLog.d.ts +144 -0
  18. package/dist/core/JudgmentProvenanceLog.d.ts.map +1 -0
  19. package/dist/core/JudgmentProvenanceLog.js +301 -0
  20. package/dist/core/JudgmentProvenanceLog.js.map +1 -0
  21. package/dist/core/MachineIdentity.d.ts.map +1 -1
  22. package/dist/core/MachineIdentity.js +2 -0
  23. package/dist/core/MachineIdentity.js.map +1 -1
  24. package/dist/core/OwnerDarkLadder.d.ts +108 -0
  25. package/dist/core/OwnerDarkLadder.d.ts.map +1 -0
  26. package/dist/core/OwnerDarkLadder.js +229 -0
  27. package/dist/core/OwnerDarkLadder.js.map +1 -0
  28. package/dist/core/PostUpdateMigrator.d.ts +10 -0
  29. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  30. package/dist/core/PostUpdateMigrator.js +106 -0
  31. package/dist/core/PostUpdateMigrator.js.map +1 -1
  32. package/dist/core/SpawnAdmission.d.ts +218 -0
  33. package/dist/core/SpawnAdmission.d.ts.map +1 -0
  34. package/dist/core/SpawnAdmission.js +440 -0
  35. package/dist/core/SpawnAdmission.js.map +1 -0
  36. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  37. package/dist/core/devGatedFeatures.js +24 -0
  38. package/dist/core/devGatedFeatures.js.map +1 -1
  39. package/dist/core/machineCoherenceManifest.d.ts.map +1 -1
  40. package/dist/core/machineCoherenceManifest.js +30 -0
  41. package/dist/core/machineCoherenceManifest.js.map +1 -1
  42. package/dist/core/types.d.ts +87 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/monitoring/DuplicateSessionReconciler.d.ts +225 -0
  46. package/dist/monitoring/DuplicateSessionReconciler.d.ts.map +1 -0
  47. package/dist/monitoring/DuplicateSessionReconciler.js +544 -0
  48. package/dist/monitoring/DuplicateSessionReconciler.js.map +1 -0
  49. package/dist/monitoring/FailureAnalyzer.d.ts.map +1 -1
  50. package/dist/monitoring/FailureAnalyzer.js +40 -1
  51. package/dist/monitoring/FailureAnalyzer.js.map +1 -1
  52. package/dist/monitoring/FailureLedger.d.ts +9 -0
  53. package/dist/monitoring/FailureLedger.d.ts.map +1 -1
  54. package/dist/monitoring/FailureLedger.js +21 -4
  55. package/dist/monitoring/FailureLedger.js.map +1 -1
  56. package/dist/scaffold/templates.d.ts.map +1 -1
  57. package/dist/scaffold/templates.js +2 -1
  58. package/dist/scaffold/templates.js.map +1 -1
  59. package/dist/server/AgentServer.d.ts +5 -0
  60. package/dist/server/AgentServer.d.ts.map +1 -1
  61. package/dist/server/AgentServer.js +18 -3
  62. package/dist/server/AgentServer.js.map +1 -1
  63. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  64. package/dist/server/CapabilityIndex.js +9 -0
  65. package/dist/server/CapabilityIndex.js.map +1 -1
  66. package/dist/server/fileRoutes.d.ts +16 -0
  67. package/dist/server/fileRoutes.d.ts.map +1 -1
  68. package/dist/server/fileRoutes.js +46 -0
  69. package/dist/server/fileRoutes.js.map +1 -1
  70. package/dist/server/middleware.d.ts +32 -0
  71. package/dist/server/middleware.d.ts.map +1 -1
  72. package/dist/server/middleware.js +34 -0
  73. package/dist/server/middleware.js.map +1 -1
  74. package/dist/server/routes.d.ts +7 -0
  75. package/dist/server/routes.d.ts.map +1 -1
  76. package/dist/server/routes.js +99 -0
  77. package/dist/server/routes.js.map +1 -1
  78. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  79. package/dist/testing/selfActionRegistry.js +89 -0
  80. package/dist/testing/selfActionRegistry.js.map +1 -1
  81. package/package.json +1 -1
  82. package/scripts/lib/self-action-detect.mjs +2 -0
  83. package/skills/instar-dev/templates/side-effects-artifact.md +8 -0
  84. package/skills/spec-converge/SKILL.md +1 -0
  85. package/skills/spec-converge/scripts/write-convergence-tag.mjs +71 -0
  86. package/src/data/builtin-manifest.json +65 -65
  87. package/src/scaffold/templates.ts +2 -1
  88. package/upgrades/1.3.819.md +88 -0
  89. package/upgrades/1.3.820.md +25 -0
  90. package/upgrades/side-effects/glance-p4-sweep.md +112 -0
  91. package/upgrades/side-effects/ownership-gated-spawn-increment-1.md +166 -0
  92. package/upgrades/1.3.818.md +0 -25
@@ -44,16 +44,18 @@ export const GLANCE_MAX_TOKEN_LEN = 40; // a longer token is a glued-word budget
44
44
  export const GLANCE_ADOPTED_TABS = [
45
45
  'commitments', 'blockers', // Phases 1–2
46
46
  'machines', 'systems', 'spend', 'routing-map', // Phase 3 (the jargon belt); 'systems' is the Health tab
47
+ // Phase 4 — the sweep: every remaining data-summary view on the floor.
48
+ 'pr-pipeline', 'tokens', 'llm-activity', 'secrets', 'resources', 'initiatives',
47
49
  ];
48
50
 
49
51
  export const GLANCE_GRANDFATHERED = [
50
52
  'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features',
51
- 'integrated-being', 'pr-pipeline', 'projects', 'initiatives', 'tokens',
52
- 'resources', 'llm-activity', 'threadline', 'evidence',
53
+ 'integrated-being', 'projects', 'threadline', 'evidence',
53
54
  'process-health', 'subscriptions', 'preferences-learning', 'mandates',
54
- 'secrets',
55
- ]; // Phase 3 removed machines / systems (Health) / spend / routing-map they now
56
- // build through this component. 'blockers' left in Phase 2.
55
+ ]; // Phase 3 removed machines / systems (Health) / spend / routing-map.
56
+ // Phase 4 removed pr-pipeline / tokens / llm-activity / secrets / resources /
57
+ // initiatives. The remainder are interactive/console/module surfaces where the
58
+ // read-only glance model does not fit — enumerated as ratified exceptions (PR body).
57
59
 
58
60
  // The committed ceiling on grandfathered-tab count. Only ever LOWER this (each
59
61
  // lowering marks a tab retrofitted onto the floor). Never raise it without an
@@ -61,7 +63,10 @@ export const GLANCE_GRANDFATHERED = [
61
63
  // below the floor, the exact regression the ratchet exists to prevent.
62
64
  // Lowered 25 → 24 (Phase 2): Blockers retrofitted.
63
65
  // Lowered 24 → 20 (Phase 3): Machines, Health (systems), Spend, Routing Map retrofitted.
64
- export const GLANCE_GRANDFATHERED_CEILING = 20;
66
+ // Lowered 20 14 (Phase 4 — the sweep): pr-pipeline, tokens, llm-activity, secrets,
67
+ // resources, initiatives retrofitted. The 14 remaining are interactive/console/
68
+ // module surfaces enumerated for operator ratification (see the Phase-4 PR body).
69
+ export const GLANCE_GRANDFATHERED_CEILING = 14;
65
70
 
66
71
  // ── F10 — insider-vocab detection ────────────────────────────────────────────
67
72
  // A readability floor, NOT a secret-redaction boundary (secret handling stays at
@@ -768,6 +773,18 @@ function fmtCount(n) {
768
773
  const v = Number(n);
769
774
  return (Number.isFinite(v) ? v : 0).toLocaleString('en-US');
770
775
  }
776
+ function fmtPct(n) {
777
+ const v = Number(n);
778
+ return (Number.isFinite(v) ? Math.round(v) : 0) + '%';
779
+ }
780
+ function fmtBytes(n) {
781
+ let v = Number(n);
782
+ if (!Number.isFinite(v) || v <= 0) return '0 MB';
783
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
784
+ let i = 0;
785
+ while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
786
+ return (v >= 100 || i === 0 ? Math.round(v) : v.toFixed(1)) + ' ' + units[i];
787
+ }
771
788
 
772
789
  /** Build a Layer-3 record node from an ordered [key, value] list (all XSS-safe). */
773
790
  function recordFields(doc, rows) {
@@ -1365,3 +1382,484 @@ export function routingMapGlanceSpec(doc, map) {
1365
1382
  }));
1366
1383
  return { headline: base.headline, tiles, population: base.population };
1367
1384
  }
1385
+
1386
+ // ═════════════════════════════════════════════════════════════════════════════
1387
+ // PHASE 4 — the sweep: every remaining grandfathered data-summary view brought to
1388
+ // the glance floor (topic 29836). Each builder maps its ONE population to a
1389
+ // jargon-free headline + ≤5 tiles (Layer 1), each tile drills into the rows behind
1390
+ // that number in plain words (Layer 2), each row opens the full record where the
1391
+ // IDs/config/raw detail live (Layer 3). Single-population tabs reuse wireTiles;
1392
+ // mixed-population tabs (tokens, initiatives) wire per-tile modes like spend/routing.
1393
+ // ═════════════════════════════════════════════════════════════════════════════
1394
+
1395
+ // ── PR Pipeline glance (F10/F11) — GET /pr-gate/metrics ──────────────────────
1396
+ // The old front page listed raw PR cards (`PR #n @ <sha>` · eligible/not). On the
1397
+ // floor: a plain headline over Ready-to-merge / Not-ready tiles; the commit sha +
1398
+ // gate reason move to the Layer-3 record.
1399
+
1400
+ /** The single population: the PR-gate metric entries. */
1401
+ export function prPipelinePopulation(metrics) {
1402
+ const entries = metrics && Array.isArray(metrics.entries) ? metrics.entries : [];
1403
+ return entries.filter((e) => e && typeof e === 'object');
1404
+ }
1405
+
1406
+ export function buildPrPipelineGlance(metrics) {
1407
+ const entries = prPipelinePopulation(metrics);
1408
+ const ready = entries.filter((e) => e.eligible === true);
1409
+ const notReady = entries.filter((e) => e.eligible !== true);
1410
+ let headline;
1411
+ if (entries.length === 0) {
1412
+ headline = 'No pull requests are waiting in the merge gate right now.';
1413
+ } else {
1414
+ const noun = entries.length === 1 ? 'open pull request' : 'open pull requests';
1415
+ const verb = ready.length === 1 ? 'is' : 'are';
1416
+ headline = `${ready.length} of ${entries.length} ${noun} ${verb} ready to merge.`;
1417
+ }
1418
+ const tiles = [
1419
+ { key: 'ready', label: 'Ready to merge', value: String(ready.length), tone: ready.length ? 'neutral' : 'muted', rows: ready },
1420
+ { key: 'not-ready', label: 'Not ready yet', value: String(notReady.length), tone: notReady.length ? 'warn' : 'neutral', rows: notReady },
1421
+ ];
1422
+ return { headline, tiles, population: entries };
1423
+ }
1424
+
1425
+ export function prRowText(e) {
1426
+ const n = e.pr_number != null ? `#${e.pr_number}` : '#—';
1427
+ return `Pull request ${n} — ${e.eligible === true ? 'ready to merge' : 'not ready yet'}`;
1428
+ }
1429
+ export function prRecordNode(doc, e, opts = {}) {
1430
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1431
+ return recordFields(doc, [
1432
+ ['Pull request', e.pr_number != null ? `#${e.pr_number}` : '—'],
1433
+ ['Ready to merge', e.eligible === true ? 'yes' : 'no'],
1434
+ ['Why', e.reason || '—'],
1435
+ ['Commit', typeof e.head_sha === 'string' ? e.head_sha.slice(0, 8) : '—'],
1436
+ ['First seen', fmtTs(e.created_at)],
1437
+ ]);
1438
+ }
1439
+ export function prPipelineGlanceSpec(doc, metrics, opts = {}) {
1440
+ const base = buildPrPipelineGlance(metrics);
1441
+ const tiles = wireTiles(doc, base.tiles, prRowText, (d, e) => prRecordNode(d, e, opts));
1442
+ return { headline: base.headline, tiles, population: base.population };
1443
+ }
1444
+
1445
+ // ── LLM Activity glance (F10/F11) — GET /metrics/features ────────────────────
1446
+ // The old front page was an 8-metric row + a wide 11-column per-component table.
1447
+ // On the floor: a plain headline over Components / Calls / Acted / Errors tiles;
1448
+ // the per-component providers, models, fire rate, and latencies move to Layer 3.
1449
+
1450
+ /** The single population: the per-component metrics GET /metrics/features returns. */
1451
+ export function llmActivityPopulation(data) {
1452
+ const features = data && Array.isArray(data.features) ? data.features : [];
1453
+ return features.filter((f) => f && typeof f.feature === 'string');
1454
+ }
1455
+
1456
+ export function buildLlmActivityGlance(data) {
1457
+ const features = llmActivityPopulation(data);
1458
+ const totals = (data && data.totals) || {};
1459
+ const calls = Number(totals.calls) || 0;
1460
+ const fired = Number(totals.fired) || 0;
1461
+ const errors = Number(totals.errors) || 0;
1462
+ const withErrors = features.filter((f) => (Number(f.errors) || 0) > 0);
1463
+ const acted = features.filter((f) => (Number(f.fired) || 0) > 0);
1464
+
1465
+ let headline;
1466
+ if (features.length === 0) {
1467
+ headline = 'No background AI activity has been recorded in this window.';
1468
+ } else {
1469
+ const comp = features.length === 1 ? '1 component' : `${features.length} components`;
1470
+ const errClause = errors === 0 ? 'none hit an error'
1471
+ : errors === 1 ? '1 call hit an error' : `${fmtCount(errors)} calls hit an error`;
1472
+ headline = `${comp} made ${fmtCount(calls)} background AI calls; ${errClause}.`;
1473
+ }
1474
+
1475
+ const tiles = [
1476
+ { key: 'components', label: 'Components', value: String(features.length), tone: 'neutral', rows: features },
1477
+ { key: 'calls', label: 'AI calls', value: fmtCount(calls), tone: 'muted', rows: features.slice().sort((a, b) => (Number(b.calls) || 0) - (Number(a.calls) || 0)) },
1478
+ { key: 'acted', label: 'Acted', value: String(acted.length), tone: 'neutral', rows: acted },
1479
+ { key: 'errors', label: 'Errors', value: fmtCount(errors), tone: errors ? 'warn' : 'neutral', rows: withErrors },
1480
+ ];
1481
+ return { headline, tiles, population: features };
1482
+ }
1483
+
1484
+ function llmProviderPhrase(f) {
1485
+ const fw = Array.isArray(f.frameworks) ? f.frameworks.filter(Boolean) : [];
1486
+ if (fw.length) return fw.map(humanizeToken).join(', ');
1487
+ const models = Array.isArray(f.models) ? f.models.filter(Boolean) : [];
1488
+ const friendly = models.map(friendlyModel).filter(Boolean);
1489
+ return friendly.length ? friendly.join(', ') : 'the built-in model';
1490
+ }
1491
+ export function llmActivityRowText(f) {
1492
+ return `${humanizeToken(f.feature)} — ${fmtCount(Number(f.calls) || 0)} AI calls`;
1493
+ }
1494
+ export function llmActivityRecordNode(doc, f) {
1495
+ const rows = [
1496
+ ['Component', humanizeToken(f.feature)],
1497
+ ['Runs on', llmProviderPhrase(f)],
1498
+ ['AI calls', fmtCount(Number(f.calls) || 0)],
1499
+ ['Acted', fmtCount(Number(f.fired) || 0)],
1500
+ ['Skipped', fmtCount(Number(f.shed) || 0)],
1501
+ ['Errors', fmtCount(Number(f.errors) || 0)],
1502
+ ['Text in', fmtCount(Number(f.tokensIn) || 0)],
1503
+ ['Text out', fmtCount(Number(f.tokensOut) || 0)],
1504
+ ];
1505
+ if (f.p50LatencyMs != null) rows.push(['Typical speed', `${Math.round(Number(f.p50LatencyMs))} ms`]);
1506
+ if (f.p95LatencyMs != null) rows.push(['Slowest 1 in 20', `${Math.round(Number(f.p95LatencyMs))} ms`]);
1507
+ return recordFields(doc, rows);
1508
+ }
1509
+ export function llmActivityGlanceSpec(doc, data) {
1510
+ const base = buildLlmActivityGlance(data);
1511
+ const tiles = wireTiles(doc, base.tiles, llmActivityRowText, llmActivityRecordNode);
1512
+ return { headline: base.headline, tiles, population: base.population };
1513
+ }
1514
+
1515
+ // ── Secrets glance (F10/F11) — GET /secrets/pending ──────────────────────────
1516
+ // The old front page listed pending secret-drop cards with a live countdown. On
1517
+ // the floor: a plain headline over Waiting / Expired tiles; the drop link, topic,
1518
+ // and exact expiry time move to the Layer-3 record. (The token is a request
1519
+ // reference the drop UI already exposes — it lives at Layer 3, not the glance.)
1520
+
1521
+ /** The single population: the pending secret requests GET /secrets/pending returns. */
1522
+ export function secretsPopulation(data) {
1523
+ const pending = data && Array.isArray(data.pending) ? data.pending : [];
1524
+ return pending.filter((p) => p && typeof p === 'object');
1525
+ }
1526
+
1527
+ function secretExpired(p, now) {
1528
+ if (p.expired === true) return true;
1529
+ if (p.expiresAt == null) return false;
1530
+ const t = typeof p.expiresAt === 'number' ? p.expiresAt : Date.parse(p.expiresAt);
1531
+ return Number.isFinite(t) ? t <= now : false;
1532
+ }
1533
+
1534
+ export function buildSecretsGlance(data, now = Date.now()) {
1535
+ const pending = secretsPopulation(data);
1536
+ const expired = pending.filter((p) => secretExpired(p, now));
1537
+ const waiting = pending.filter((p) => !secretExpired(p, now));
1538
+
1539
+ let headline;
1540
+ if (pending.length === 0) {
1541
+ headline = 'No secret requests are waiting right now.';
1542
+ } else if (waiting.length === 0) {
1543
+ headline = 'All secret requests have expired.';
1544
+ } else {
1545
+ const noun = waiting.length === 1 ? 'secret request is' : 'secret requests are';
1546
+ headline = `${waiting.length} ${noun} waiting for you.`;
1547
+ }
1548
+ const tiles = [
1549
+ { key: 'waiting', label: 'Waiting for you', value: String(waiting.length), tone: waiting.length ? 'warn' : 'neutral', rows: waiting },
1550
+ { key: 'expired', label: 'Expired', value: String(expired.length), tone: 'muted', rows: expired },
1551
+ ];
1552
+ return { headline, tiles, population: pending };
1553
+ }
1554
+
1555
+ export function secretRowText(p, now = Date.now()) {
1556
+ const label = sanitizeForDisplay(p.label || 'A secret request', 'label');
1557
+ return `${label} — ${secretExpired(p, now) ? 'expired' : 'waiting for you'}`;
1558
+ }
1559
+ export function secretRecordNode(doc, p, opts = {}) {
1560
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1561
+ const rows = [
1562
+ ['Requested', p.label || '—'],
1563
+ ['In conversation', p.topicId != null ? String(p.topicId) : '—'],
1564
+ ['Asked', fmtTs(p.createdAt)],
1565
+ ['Expires', fmtTs(p.expiresAt)],
1566
+ ];
1567
+ const wrap = recordFields(doc, rows);
1568
+ // Preserve the tab's actions (open / copy / cancel) at Layer 3 — behavior is
1569
+ // untouched, only its placement moved one click down.
1570
+ const actions = el(doc, 'div', 'glance-record-actions');
1571
+ const link = p.tunnelUrl || p.localUrl;
1572
+ if (typeof link === 'string' && /^https?:\/\//.test(link)) {
1573
+ const a = doc.createElement('a');
1574
+ a.className = 'glance-record-action';
1575
+ a.href = link; // a fixed http(s) literal from the server; textContent stays plain
1576
+ a.target = '_blank';
1577
+ a.rel = 'noopener noreferrer';
1578
+ a.textContent = 'Open the secure drop';
1579
+ actions.appendChild(a);
1580
+
1581
+ const copy = doc.createElement('button');
1582
+ copy.type = 'button';
1583
+ copy.className = 'glance-record-action';
1584
+ copy.textContent = 'Copy link';
1585
+ copy.addEventListener('click', () => {
1586
+ try {
1587
+ if (doc.defaultView && doc.defaultView.navigator && doc.defaultView.navigator.clipboard) {
1588
+ doc.defaultView.navigator.clipboard.writeText(link);
1589
+ copy.textContent = 'Copied';
1590
+ }
1591
+ } catch { /* @silent-fallback-ok — clipboard denied, leave the label */ }
1592
+ });
1593
+ actions.appendChild(copy);
1594
+ }
1595
+ if (typeof opts.onCancel === 'function' && p.token) {
1596
+ const cancel = doc.createElement('button');
1597
+ cancel.type = 'button';
1598
+ cancel.className = 'glance-record-action';
1599
+ cancel.textContent = 'Cancel request';
1600
+ cancel.addEventListener('click', () => { cancel.disabled = true; opts.onCancel(p.token); });
1601
+ actions.appendChild(cancel);
1602
+ }
1603
+ if (actions.childNodes.length) wrap.appendChild(actions);
1604
+ return wrap;
1605
+ }
1606
+ export function secretsGlanceSpec(doc, data, opts = {}) {
1607
+ const now = opts.now ?? Date.now();
1608
+ const base = buildSecretsGlance(data, now);
1609
+ const tiles = wireTiles(doc, base.tiles, (p) => secretRowText(p, now), (d, p) => secretRecordNode(d, p, opts));
1610
+ return { headline: base.headline, tiles, population: base.population };
1611
+ }
1612
+
1613
+ // ── Tokens glance (F10/F11) — GET /tokens/summary + /sessions + /orphans ─────
1614
+ // The old front page was a 7-metric grid + a per-session table + an idle list. On
1615
+ // the floor: a plain headline over Text-processed / Recent-conversations / Idle
1616
+ // tiles; the session ids, event counts, and full paths move to the Layer-3 record.
1617
+ // "Tokens" reads as "pieces of text" (matching the Spend tab's plain vocabulary).
1618
+
1619
+ /** A plain, short conversation name from a project path (its last segment). */
1620
+ function projectName(p) {
1621
+ const parts = String(p == null ? '' : p).split('/').filter(Boolean);
1622
+ return sanitizeForDisplay(parts.length ? parts[parts.length - 1] : (p || 'a conversation'), 'label');
1623
+ }
1624
+
1625
+ export function buildTokensGlance(summary, sessions, orphans) {
1626
+ const s = (summary && summary.summary) || summary || {};
1627
+ const active = (Array.isArray(sessions) ? sessions : []).filter((r) => r && typeof r === 'object');
1628
+ const idle = (Array.isArray(orphans) ? orphans : []).filter((o) => o && typeof o === 'object');
1629
+ const total = Number(s.totalTokens) || 0;
1630
+ const byTokens = active.slice().sort((a, b) => (Number(b.totalTokens) || 0) - (Number(a.totalTokens) || 0));
1631
+
1632
+ let headline;
1633
+ if (active.length === 0 && total === 0) {
1634
+ headline = 'No conversation activity has been recorded in this window.';
1635
+ } else {
1636
+ const noun = active.length === 1 ? 'conversation' : 'conversations';
1637
+ const idleClause = idle.length === 0 ? '' : `; ${idle.length} idle`;
1638
+ headline = `I've processed ${fmtCount(total)} pieces of text across ${active.length} ${noun}${idleClause}.`;
1639
+ }
1640
+
1641
+ const tiles = [
1642
+ { key: 'processed', label: 'Text processed', value: fmtCount(total), tone: 'muted', rows: byTokens, mode: 'session' },
1643
+ { key: 'conversations', label: 'Recent conversations', value: String(active.length), tone: 'neutral', rows: active, mode: 'session' },
1644
+ { key: 'idle', label: 'Idle conversations', value: String(idle.length), tone: idle.length ? 'warn' : 'neutral', rows: idle, mode: 'orphan' },
1645
+ ];
1646
+ return { headline, tiles, population: active };
1647
+ }
1648
+
1649
+ export function tokenSessionRowText(r) {
1650
+ return `${projectName(r.projectPath)} — ${fmtCount(Number(r.totalTokens) || 0)} pieces of text`;
1651
+ }
1652
+ export function tokenOrphanRowText(o, opts = {}) {
1653
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1654
+ return `${projectName(o.projectPath)} — idle since ${fmtTs(o.lastTs)}`;
1655
+ }
1656
+ export function tokenSessionRecordNode(doc, r, opts = {}) {
1657
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1658
+ return recordFields(doc, [
1659
+ ['Conversation', projectName(r.projectPath)],
1660
+ ['Full path', r.projectPath || '—'],
1661
+ ['Session', r.sessionId || '—'],
1662
+ ['Pieces of text', fmtCount(Number(r.totalTokens) || 0)],
1663
+ ['Messages', fmtCount(Number(r.eventCount) || 0)],
1664
+ ['Last seen', fmtTs(r.lastTs)],
1665
+ ]);
1666
+ }
1667
+ export function tokenOrphanRecordNode(doc, o, opts = {}) {
1668
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1669
+ return recordFields(doc, [
1670
+ ['Conversation', projectName(o.projectPath)],
1671
+ ['Full path', o.projectPath || '—'],
1672
+ ['Session', o.sessionId || '—'],
1673
+ ['Idle since', fmtTs(o.lastTs)],
1674
+ ]);
1675
+ }
1676
+
1677
+ export function tokensGlanceSpec(doc, summary, sessions, orphans, opts = {}) {
1678
+ const base = buildTokensGlance(summary, sessions, orphans);
1679
+ const tiles = base.tiles.map((t) => ({
1680
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1681
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1682
+ const rows = t.rows || [];
1683
+ if (rows.length === 0) return;
1684
+ const list = el(d, 'div', 'glance-list');
1685
+ for (const item of rows) {
1686
+ const row = d.createElement('button');
1687
+ row.type = 'button';
1688
+ row.className = 'glance-list-row';
1689
+ row.setAttribute('aria-label', 'Open the full record');
1690
+ const text = t.mode === 'orphan' ? tokenOrphanRowText(item, opts) : tokenSessionRowText(item);
1691
+ row.appendChild(el(d, 'span', 'glance-list-summary', text));
1692
+ const recNode = t.mode === 'orphan' ? tokenOrphanRecordNode(d, item, opts) : tokenSessionRecordNode(d, item, opts);
1693
+ row.addEventListener('click', () => openRecord(recNode));
1694
+ list.appendChild(row);
1695
+ }
1696
+ drilldown.appendChild(list);
1697
+ },
1698
+ }));
1699
+ return { headline: base.headline, tiles, population: base.population };
1700
+ }
1701
+
1702
+ // ── Resource Usage glance (F10/F11) — GET /resources/summary ─────────────────
1703
+ // The old front page was a 6-gauge headline + a per-process table + a trend list.
1704
+ // On the floor: a plain headline over CPU-now / Memory-now / Processes tiles; the
1705
+ // per-process averages and peaks move to the Layer-3 record.
1706
+
1707
+ /** Plain, jargon-free name for a resource source id. */
1708
+ function resourceSourceName(source) {
1709
+ const s = String(source == null ? '' : source);
1710
+ if (s === 'agent-server') return 'The server';
1711
+ if (s === 'aggregate') return 'Everything together';
1712
+ if (s.startsWith('session:')) return 'Conversation ' + sanitizeForDisplay(s.slice('session:'.length).slice(0, 8), 'label');
1713
+ return humanizeToken(s) || 'A process';
1714
+ }
1715
+
1716
+ /** The single population: the non-aggregate per-process sources. */
1717
+ export function resourcesPopulation(summary) {
1718
+ const sources = summary && Array.isArray(summary.sources) ? summary.sources : [];
1719
+ return sources.filter((s) => s && typeof s.source === 'string' && s.source !== 'aggregate');
1720
+ }
1721
+
1722
+ export function buildResourcesGlance(summary) {
1723
+ const sources = summary && Array.isArray(summary.sources) ? summary.sources : [];
1724
+ const agg = sources.find((s) => s && s.source === 'aggregate');
1725
+ const processes = resourcesPopulation(summary);
1726
+
1727
+ let headline;
1728
+ if (!agg && processes.length === 0) {
1729
+ headline = 'No resource samples have been collected yet.';
1730
+ } else if (agg) {
1731
+ headline = `Using ${fmtPct(agg.currentCpuPercent)} CPU and ${fmtBytes(agg.currentRssBytes)} of memory right now.`;
1732
+ } else {
1733
+ headline = `Watching ${processes.length} process${processes.length === 1 ? '' : 'es'} right now.`;
1734
+ }
1735
+
1736
+ const byCpu = processes.slice().sort((a, b) => (Number(b.currentCpuPercent) || 0) - (Number(a.currentCpuPercent) || 0));
1737
+ const byMem = processes.slice().sort((a, b) => (Number(b.currentRssBytes) || 0) - (Number(a.currentRssBytes) || 0));
1738
+ const tiles = [
1739
+ { key: 'cpu', label: 'CPU right now', value: agg ? fmtPct(agg.currentCpuPercent) : '—', tone: 'muted', rows: byCpu },
1740
+ { key: 'memory', label: 'Memory right now', value: agg ? fmtBytes(agg.currentRssBytes) : '—', tone: 'muted', rows: byMem },
1741
+ { key: 'processes', label: 'Processes', value: String(processes.length), tone: 'neutral', rows: processes },
1742
+ ];
1743
+ return { headline, tiles, population: processes };
1744
+ }
1745
+
1746
+ export function resourceRowText(s) {
1747
+ return `${resourceSourceName(s.source)} — ${fmtPct(s.currentCpuPercent)} CPU, ${fmtBytes(s.currentRssBytes)}`;
1748
+ }
1749
+ export function resourceRecordNode(doc, s) {
1750
+ return recordFields(doc, [
1751
+ ['Process', resourceSourceName(s.source)],
1752
+ ['CPU right now', fmtPct(s.currentCpuPercent)],
1753
+ ['Average CPU (last hour)', fmtPct(s.avgCpuPercent)],
1754
+ ['Highest CPU (last hour)', fmtPct(s.peakCpuPercent)],
1755
+ ['Memory right now', fmtBytes(s.currentRssBytes)],
1756
+ ['Highest memory (last hour)', fmtBytes(s.peakRssBytes)],
1757
+ ]);
1758
+ }
1759
+ export function resourcesGlanceSpec(doc, summary) {
1760
+ const base = buildResourcesGlance(summary);
1761
+ const tiles = wireTiles(doc, base.tiles, resourceRowText, resourceRecordNode);
1762
+ return { headline: base.headline, tiles, population: base.population };
1763
+ }
1764
+
1765
+ // ── Initiatives glance (F10/F11) — GET /initiatives + /initiatives/digest ────
1766
+ // The old front page was a signal digest + a list of initiative cards. On the
1767
+ // floor: a plain headline over In-progress / Needs-you / Ready / Check-in / Idle
1768
+ // tiles; the phases, descriptions, and timestamps move to the Layer-3 record. The
1769
+ // In-progress tile drills into the (filtered) initiatives; the attention tiles
1770
+ // drill into the matching digest signals.
1771
+
1772
+ const INITIATIVE_STATUS_WORD = {
1773
+ active: 'in progress', completed: 'done', archived: 'archived', abandoned: 'stopped',
1774
+ };
1775
+ const SIGNAL_REASON_WORD = {
1776
+ 'needs-user': 'Waiting on you', 'next-check-due': 'Time for a check-in',
1777
+ 'ready-to-advance': 'Ready to move forward', 'stale': "Hasn't moved in a while",
1778
+ };
1779
+
1780
+ /** The single population for the list tile: the (already server-filtered) items. */
1781
+ export function initiativesPopulation(itemsRes) {
1782
+ const items = itemsRes && Array.isArray(itemsRes.items) ? itemsRes.items : [];
1783
+ return items.filter((i) => i && typeof i === 'object');
1784
+ }
1785
+
1786
+ export function buildInitiativesGlance(itemsRes, digestRes) {
1787
+ const items = initiativesPopulation(itemsRes);
1788
+ const signals = (digestRes && Array.isArray(digestRes.items)) ? digestRes.items.filter(Boolean) : [];
1789
+ const byReason = (r) => signals.filter((s) => s.reason === r);
1790
+ const needsYou = byReason('needs-user');
1791
+ const ready = byReason('ready-to-advance');
1792
+ const checkDue = byReason('next-check-due');
1793
+ const idle = byReason('stale');
1794
+
1795
+ let headline;
1796
+ if (items.length === 0) {
1797
+ headline = 'No initiatives are in flight right now.';
1798
+ } else {
1799
+ const noun = items.length === 1 ? 'initiative' : 'initiatives';
1800
+ const needClause = needsYou.length === 0 ? ''
1801
+ : `; ${needsYou.length} need${needsYou.length === 1 ? 's' : ''} you`;
1802
+ headline = `${items.length} ${noun} in flight${needClause}.`;
1803
+ }
1804
+
1805
+ const tiles = [
1806
+ { key: 'in-progress', label: 'In progress', value: String(items.length), tone: 'neutral', rows: items, mode: 'item' },
1807
+ { key: 'needs-you', label: 'Needs you', value: String(needsYou.length), tone: needsYou.length ? 'warn' : 'neutral', rows: needsYou, mode: 'signal' },
1808
+ { key: 'ready', label: 'Ready to move on', value: String(ready.length), tone: ready.length ? 'neutral' : 'muted', rows: ready, mode: 'signal' },
1809
+ { key: 'check-due', label: 'Check-in due', value: String(checkDue.length), tone: checkDue.length ? 'warn' : 'muted', rows: checkDue, mode: 'signal' },
1810
+ { key: 'idle', label: 'Idle', value: String(idle.length), tone: 'muted', rows: idle, mode: 'signal' },
1811
+ ];
1812
+ return { headline, tiles, population: items };
1813
+ }
1814
+
1815
+ export function initiativeRowText(i) {
1816
+ const title = sanitizeForDisplay(i.title || i.id || 'An initiative', 'summary');
1817
+ return `${title} — ${INITIATIVE_STATUS_WORD[i.status] || (i.status || 'unknown')}`;
1818
+ }
1819
+ export function initiativeRecordNode(doc, i, opts = {}) {
1820
+ const fmtTs = opts.fmtTs || defaultFmtTs;
1821
+ const phases = Array.isArray(i.phases) ? i.phases : [];
1822
+ const done = phases.filter((p) => p && p.status === 'done').length;
1823
+ const rows = [
1824
+ ['Initiative', i.title || i.id || '—'],
1825
+ ['Status', INITIATIVE_STATUS_WORD[i.status] || (i.status || '—')],
1826
+ ['What it is', i.description || '—'],
1827
+ ];
1828
+ if (phases.length) rows.push(['Steps done', `${done} of ${phases.length}`]);
1829
+ if (i.lastTouchedAt) rows.push(['Last worked on', fmtTs(i.lastTouchedAt)]);
1830
+ return recordFields(doc, rows);
1831
+ }
1832
+ export function initiativeSignalRowText(s) {
1833
+ return sanitizeForDisplay(s.title || SIGNAL_REASON_WORD[s.reason] || 'A signal', 'summary');
1834
+ }
1835
+ export function initiativeSignalRecordNode(doc, s) {
1836
+ return recordFields(doc, [
1837
+ ['Initiative', s.title || '—'],
1838
+ ['Why it needs a look', SIGNAL_REASON_WORD[s.reason] || (s.reason || '—')],
1839
+ ['Detail', s.detail || '—'],
1840
+ ]);
1841
+ }
1842
+ export function initiativesGlanceSpec(doc, itemsRes, digestRes, opts = {}) {
1843
+ const base = buildInitiativesGlance(itemsRes, digestRes);
1844
+ const tiles = base.tiles.map((t) => ({
1845
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1846
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1847
+ const rows = t.rows || [];
1848
+ if (rows.length === 0) return;
1849
+ const list = el(d, 'div', 'glance-list');
1850
+ for (const item of rows) {
1851
+ const row = d.createElement('button');
1852
+ row.type = 'button';
1853
+ row.className = 'glance-list-row';
1854
+ row.setAttribute('aria-label', 'Open the full record');
1855
+ const text = t.mode === 'signal' ? initiativeSignalRowText(item) : initiativeRowText(item);
1856
+ row.appendChild(el(d, 'span', 'glance-list-summary', text));
1857
+ const recNode = t.mode === 'signal' ? initiativeSignalRecordNode(d, item) : initiativeRecordNode(d, item, opts);
1858
+ row.addEventListener('click', () => openRecord(recNode));
1859
+ list.appendChild(row);
1860
+ }
1861
+ drilldown.appendChild(list);
1862
+ },
1863
+ }));
1864
+ return { headline: base.headline, tiles, population: base.population };
1865
+ }