instar 1.3.816 → 1.3.818

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 (36) hide show
  1. package/dashboard/glance.js +688 -7
  2. package/dashboard/index.html +164 -649
  3. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  4. package/dist/config/ConfigDefaults.js +1 -0
  5. package/dist/config/ConfigDefaults.js.map +1 -1
  6. package/dist/core/MentorVisibleEcho.d.ts +22 -0
  7. package/dist/core/MentorVisibleEcho.d.ts.map +1 -0
  8. package/dist/core/MentorVisibleEcho.js +78 -0
  9. package/dist/core/MentorVisibleEcho.js.map +1 -0
  10. package/dist/core/PostUpdateMigrator.js +1 -1
  11. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  12. package/dist/core/WriteDomainRegistry.js +13 -0
  13. package/dist/core/WriteDomainRegistry.js.map +1 -1
  14. package/dist/scaffold/templates.js +1 -1
  15. package/dist/scheduler/MentorOnboardingRunner.d.ts +2 -0
  16. package/dist/scheduler/MentorOnboardingRunner.d.ts.map +1 -1
  17. package/dist/scheduler/MentorOnboardingRunner.js +1 -0
  18. package/dist/scheduler/MentorOnboardingRunner.js.map +1 -1
  19. package/dist/server/AgentServer.d.ts.map +1 -1
  20. package/dist/server/AgentServer.js +25 -0
  21. package/dist/server/AgentServer.js.map +1 -1
  22. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  23. package/dist/server/CapabilityIndex.js +1 -0
  24. package/dist/server/CapabilityIndex.js.map +1 -1
  25. package/dist/server/routes.d.ts.map +1 -1
  26. package/dist/server/routes.js +68 -57
  27. package/dist/server/routes.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/data/builtin-manifest.json +64 -64
  30. package/src/scaffold/templates.ts +1 -1
  31. package/upgrades/1.3.817.md +22 -0
  32. package/upgrades/1.3.818.md +25 -0
  33. package/upgrades/eli16/mentor-drive-visible-echo.md +11 -0
  34. package/upgrades/eli16/plain-enrollment-cancel.md +9 -0
  35. package/upgrades/side-effects/mentor-drive-visible-echo.md +86 -0
  36. package/upgrades/side-effects/plain-enrollment-cancel.md +79 -0
@@ -41,22 +41,27 @@ export const GLANCE_MAX_TOKEN_LEN = 40; // a longer token is a glued-word budget
41
41
  // (same discipline as the F3 purpose-line exempt list). The completeness test
42
42
  // asserts adopted ∪ grandfathered == every TAB_REGISTRY id, so a NEW tab in NEITHER
43
43
  // set fails the build; the monotonicity test asserts the grandfather size ≤ ceiling.
44
- export const GLANCE_ADOPTED_TABS = ['commitments', 'blockers'];
44
+ export const GLANCE_ADOPTED_TABS = [
45
+ 'commitments', 'blockers', // Phases 1–2
46
+ 'machines', 'systems', 'spend', 'routing-map', // Phase 3 (the jargon belt); 'systems' is the Health tab
47
+ ];
45
48
 
46
49
  export const GLANCE_GRANDFATHERED = [
47
- 'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features', 'systems',
50
+ 'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features',
48
51
  'integrated-being', 'pr-pipeline', 'projects', 'initiatives', 'tokens',
49
- 'resources', 'llm-activity', 'routing-map', 'spend', 'threadline', 'evidence',
50
- 'process-health', 'subscriptions', 'preferences-learning', 'machines', 'mandates',
52
+ 'resources', 'llm-activity', 'threadline', 'evidence',
53
+ 'process-health', 'subscriptions', 'preferences-learning', 'mandates',
51
54
  'secrets',
52
- ]; // 'blockers' left the list this PR (Phase 2) it now builds through this component.
55
+ ]; // Phase 3 removed machines / systems (Health) / spend / routing-map they now
56
+ // build through this component. 'blockers' left in Phase 2.
53
57
 
54
58
  // The committed ceiling on grandfathered-tab count. Only ever LOWER this (each
55
59
  // lowering marks a tab retrofitted onto the floor). Never raise it without an
56
60
  // operator-signed justification — raising it is how a NEW tab would silently ship
57
61
  // below the floor, the exact regression the ratchet exists to prevent.
58
- // Lowered 25 → 24 (Phase 2): Blockers retrofitted onto the glance floor.
59
- export const GLANCE_GRANDFATHERED_CEILING = 24;
62
+ // Lowered 25 → 24 (Phase 2): Blockers retrofitted.
63
+ // Lowered 24 20 (Phase 3): Machines, Health (systems), Spend, Routing Map retrofitted.
64
+ export const GLANCE_GRANDFATHERED_CEILING = 20;
60
65
 
61
66
  // ── F10 — insider-vocab detection ────────────────────────────────────────────
62
67
  // A readability floor, NOT a secret-redaction boundary (secret handling stays at
@@ -684,3 +689,679 @@ export function blockersGlanceSpec(doc, entries, opts = {}) {
684
689
  }));
685
690
  return { headline: base.headline, tiles, population: base.population };
686
691
  }
692
+
693
+ // ═════════════════════════════════════════════════════════════════════════════
694
+ // PHASE 3 — the jargon belt: Machines, Health, Spend, Routing Map (topic 29836).
695
+ // Each of the four tabs was ≥200 words of insider vocabulary on its front page
696
+ // (guards lines, "paid door / metered", "lane / nature / door"). Rebuilt on the
697
+ // SAME three-layer template: a component-authored, jargon-free headline + ≤5 tiles
698
+ // (Layer 1); each tile drills into the rows behind that number in plain words
699
+ // (Layer 2); each row opens the full record where the IDs/config/raw detail live
700
+ // (Layer 3). Nothing is lost — the insider fields move one or two clicks down.
701
+ // Every builder maps its counts to ONE population so the headline stays honest, and
702
+ // every dynamic value is displayed through the shared sanitizer + textContent.
703
+ // ═════════════════════════════════════════════════════════════════════════════
704
+
705
+ // ── Shared plain-language helpers ────────────────────────────────────────────
706
+ // Acronyms that should stay upper-cased when a token is humanized for display.
707
+ const KNOWN_ACRONYMS = {
708
+ gpt: 'GPT', llm: 'LLM', cli: 'CLI', ai: 'AI', cpu: 'CPU', ram: 'RAM',
709
+ api: 'API', url: 'URL', id: 'ID', ok: 'OK',
710
+ };
711
+ function titleCaseWord(w) {
712
+ const lw = String(w).toLowerCase();
713
+ if (KNOWN_ACRONYMS[lw]) return KNOWN_ACRONYMS[lw];
714
+ return lw.charAt(0).toUpperCase() + lw.slice(1);
715
+ }
716
+
717
+ /**
718
+ * Turn an identifier-ish token into a plain, sentence-cased phrase: split
719
+ * camelCase + separators, drop empties, cap only the first word.
720
+ * 'zombieCleanup' → 'Zombie cleanup'; 'session_watchdog' → 'Session watchdog'.
721
+ */
722
+ export function humanizeToken(token) {
723
+ const words = String(token == null ? '' : token)
724
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
725
+ .split(/[\s\-_/.]+/)
726
+ .filter(Boolean);
727
+ if (words.length === 0) return '';
728
+ return words
729
+ .map((w, i) => (i === 0 ? titleCaseWord(w) : (KNOWN_ACRONYMS[w.toLowerCase()] || w.toLowerCase())))
730
+ .join(' ');
731
+ }
732
+
733
+ /**
734
+ * A plain, jargon-safe display name for a model id — drops every version/date
735
+ * segment (any part containing a digit) and title-cases the words that remain.
736
+ * 'claude-opus-4-8-20260115' → 'Claude Opus'; 'gpt-5.5' → 'GPT'. Returns '' when
737
+ * nothing plain survives OR the result would still trip the F10 jargon check — the
738
+ * caller substitutes a plain phrase, so a raw model id can NEVER leak to Layer 1.
739
+ */
740
+ export function friendlyModel(modelId) {
741
+ // Drop any version/date segment (a part with a digit) AND single-letter fragments
742
+ // (a lone 'm' from 'm_<hex>' is meaningless as a name) — so a hex/opaque id yields
743
+ // '' and the caller substitutes a plain phrase.
744
+ const words = String(modelId == null ? '' : modelId)
745
+ .split(/[\s\-_./]+/)
746
+ .filter((w) => w && w.length >= 2 && !/\d/.test(w));
747
+ const name = words.map(titleCaseWord).join(' ').trim();
748
+ if (!name) return '';
749
+ return findInsiderVocab(name).length === 0 ? name : '';
750
+ }
751
+
752
+ /** The plain model phrase for a routing/spend position, with a door-class fallback. */
753
+ function modelPhrase(pos) {
754
+ if (!pos) return '';
755
+ return friendlyModel(pos.modelId) || (pos.doorClass === 'metered' ? 'a paid model' : 'the built-in model');
756
+ }
757
+
758
+ /** Plain words for a routing/spend door class. */
759
+ function doorClassWord(doorClass) {
760
+ return doorClass === 'metered' ? 'pay-per-use' : 'built-in (no per-use charge)';
761
+ }
762
+
763
+ function fmtUsd(n, dp = 2) {
764
+ const v = Number(n);
765
+ return '$' + (Number.isFinite(v) ? v : 0).toFixed(dp);
766
+ }
767
+ function fmtCount(n) {
768
+ const v = Number(n);
769
+ return (Number.isFinite(v) ? v : 0).toLocaleString('en-US');
770
+ }
771
+
772
+ /** Build a Layer-3 record node from an ordered [key, value] list (all XSS-safe). */
773
+ function recordFields(doc, rows) {
774
+ const wrap = el(doc, 'div', 'glance-record-fields');
775
+ for (const [k, v] of rows) {
776
+ if (v == null || v === '') continue;
777
+ const row = el(doc, 'div', 'glance-record-row');
778
+ row.appendChild(el(doc, 'span', 'glance-record-key', String(k)));
779
+ row.appendChild(el(doc, 'span', 'glance-record-val', String(v)));
780
+ wrap.appendChild(row);
781
+ }
782
+ return wrap;
783
+ }
784
+
785
+ /**
786
+ * Shared tile→list→record wiring: map buildX's { rows, ... } tiles into glance
787
+ * tiles whose onActivate renders each row as a plain Layer-2 line that opens a
788
+ * Layer-3 record. `rowText(item)` → the plain summary; `recordNode(doc, item)` →
789
+ * the full record. Mirrors commitmentsGlanceSpec/blockersGlanceSpec exactly.
790
+ */
791
+ function wireTiles(doc, baseTiles, rowText, recordNode) {
792
+ return baseTiles.map((t) => ({
793
+ key: t.key,
794
+ label: t.label,
795
+ value: t.value,
796
+ tone: t.tone,
797
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
798
+ const rows = t.rows || [];
799
+ if (rows.length === 0) return; // component renders the honest F6 empty-state
800
+ const list = el(d, 'div', 'glance-list');
801
+ for (const item of rows) {
802
+ const row = d.createElement('button');
803
+ row.type = 'button';
804
+ row.className = 'glance-list-row';
805
+ row.setAttribute('aria-label', 'Open the full record');
806
+ row.appendChild(el(d, 'span', 'glance-list-summary', rowText(item)));
807
+ row.addEventListener('click', () => openRecord(recordNode(d, item)));
808
+ list.appendChild(row);
809
+ }
810
+ drilldown.appendChild(list);
811
+ },
812
+ }));
813
+ }
814
+
815
+ // ── Machines glance (F10/F11) + issue #1429 nickname edit ────────────────────
816
+ // The old front page showed a per-machine insider "guards" line ("67 on (16
817
+ // confirmed) · ⚠ 6 off that should be on… as of 3m ago"). On the floor: a plain
818
+ // headline over Online / Attention needed / Dispatcher / Safety-checks tiles. The
819
+ // named safety checks (from GET /guards) drill down with a one-line plain-English
820
+ // explanation each (Layer 2), full posture at Layer 3. Machine NICKNAMES are
821
+ // user-authored, so they NEVER appear at Layer 1 — they live at Layer 2/3, shown
822
+ // through the sanitizer. #1429: the editable nickname lives in the Layer-2 machine
823
+ // row and commits only on Enter/blur; the drill holds it across the 15s poll (F9).
824
+
825
+ function guardPostureProblems(p) {
826
+ if (!p) return 0;
827
+ return (p.offDeviant || 0) + (p.offRuntimeDivergent || 0) + (p.onStale || 0)
828
+ + (p.missing || 0) + (p.errored || 0) + (p.divergedPendingRestart || 0);
829
+ }
830
+ function machineNeedsAttention(m) {
831
+ if (!m) return false;
832
+ return !m.online
833
+ || m.clockSkewStatus === 'suspect-clock-removed'
834
+ || guardPostureProblems(m.guardPosture) > 0;
835
+ }
836
+
837
+ /** The single population: the machines GET /pool returns. */
838
+ export function machinesPopulation(pool) {
839
+ const machines = pool && Array.isArray(pool.machines) ? pool.machines : [];
840
+ return machines.filter((m) => m && typeof m.machineId === 'string');
841
+ }
842
+
843
+ export function buildMachinesGlance(pool, guards = null) {
844
+ const machines = machinesPopulation(pool);
845
+ const online = machines.filter((m) => m.online);
846
+ const attention = machines.filter(machineNeedsAttention);
847
+ const holder = pool && pool.router && pool.router.holder;
848
+ const dispatcher = holder ? machines.find((m) => m.machineId === holder) : null;
849
+
850
+ let headline;
851
+ if (machines.length === 0) {
852
+ headline = 'No machines are paired yet.';
853
+ } else if (online.length === machines.length && attention.length === 0) {
854
+ headline = machines.length === 1 ? 'This machine is online and healthy.'
855
+ : machines.length === 2 ? 'Both machines are online and healthy.'
856
+ : `All ${machines.length} machines are online and healthy.`;
857
+ } else {
858
+ const lookClause = attention.length === 1 ? '1 needs a look' : `${attention.length} need a look`;
859
+ headline = `${online.length} of ${machines.length} machines online; ${lookClause}.`;
860
+ }
861
+
862
+ const tiles = [
863
+ { key: 'online', label: 'Online', value: String(online.length), tone: online.length === machines.length ? 'neutral' : 'warn', rows: online },
864
+ { key: 'attention', label: 'Attention needed', value: String(attention.length), tone: attention.length ? 'warn' : 'neutral', rows: attention },
865
+ { key: 'dispatcher', label: 'Dispatcher', value: dispatcher ? 'Assigned' : 'None', tone: 'muted', rows: dispatcher ? [dispatcher] : [] },
866
+ ];
867
+
868
+ // The named safety checks come from the LOCAL /guards view (the only place guard
869
+ // NAMES exist without the rate-limited pool-scoped call). Omit the tile when the
870
+ // guards view is unavailable rather than invent an empty one.
871
+ const guardRows = guards && Array.isArray(guards.guards) ? guards.guards : null;
872
+ if (guardRows) {
873
+ const problems = guards.summary ? (
874
+ (guards.summary.offDeviant || 0) + (guards.summary.offRuntimeDivergent || 0)
875
+ + (guards.summary.onStale || 0) + (guards.summary.missing || 0)
876
+ + (guards.summary.errored || 0) + (guards.summary.divergedPendingRestart || 0)
877
+ ) : 0;
878
+ tiles.push({ key: 'guards', label: 'Safety checks', value: String(guardRows.length), tone: problems ? 'warn' : 'neutral', rows: guardRows });
879
+ }
880
+
881
+ return { headline, tiles, population: machines };
882
+ }
883
+
884
+ const MACHINE_STATUS_WORD = {
885
+ offline: 'Offline',
886
+ 'suspect-clock-removed': 'Clock out of sync — paused for new conversations',
887
+ };
888
+ function machineStatusWord(m) {
889
+ if (!m.online) return 'Offline';
890
+ if (m.clockSkewStatus === 'suspect-clock-removed') return MACHINE_STATUS_WORD['suspect-clock-removed'];
891
+ if (machineNeedsAttention(m)) return 'Online — needs a look';
892
+ return 'Online and healthy';
893
+ }
894
+ function machineSpecLine(hw) {
895
+ if (!hw) return 'specs not reported yet';
896
+ const chip = (hw.cpuModel && hw.cpuModel !== 'unknown') ? hw.cpuModel
897
+ : ((hw.platform || '') + (hw.arch ? ' ' + hw.arch : '')).trim();
898
+ const cores = hw.cpuCores ? hw.cpuCores + ' cores' : '';
899
+ const ram = hw.totalMemBytes ? Math.round(hw.totalMemBytes / 1073741824) + ' GB' : '';
900
+ return [chip, cores, ram].filter(Boolean).join(' · ') || 'specs not reported yet';
901
+ }
902
+
903
+ /** Plain one-line explanation of a guard's effective state — no per-key dictionary. */
904
+ const GUARD_EFFECTIVE_WORD = {
905
+ 'on-confirmed': 'On and verified working',
906
+ 'on-unverified': 'On (not yet verified this run)',
907
+ 'on-stale': 'On, but its last check is stale — worth a look',
908
+ 'on-dry-run': 'On in practice-only mode (not acting for real yet)',
909
+ 'off-runtime-divergent': 'Off even though the settings say on — needs a look',
910
+ 'diverged-pending-restart': 'Changed; waiting for a restart to take effect',
911
+ 'errored': 'Hit an error — needs a look',
912
+ 'missing': 'Expected but not found — needs a look',
913
+ };
914
+ function guardExplanation(g) {
915
+ if (g.effective === 'off') {
916
+ return g.offClass === 'diverged-from-default'
917
+ ? 'Off, though the default is on — worth a look'
918
+ : 'Off by default — intentionally quiet';
919
+ }
920
+ return GUARD_EFFECTIVE_WORD[g.effective] || String(g.effective || 'unknown');
921
+ }
922
+ function guardNeedsLook(g) {
923
+ return ['on-stale', 'off-runtime-divergent', 'diverged-pending-restart', 'errored', 'missing'].includes(g.effective)
924
+ || (g.effective === 'off' && g.offClass === 'diverged-from-default');
925
+ }
926
+
927
+ export function machineRowText(m) {
928
+ const nick = sanitizeForDisplay(m.nickname || m.machineId || 'A machine', 'label');
929
+ return `${nick} — ${machineStatusWord(m)}`;
930
+ }
931
+
932
+ /** Layer-3 machine record — specs, sessions, and this machine's guard posture in
933
+ * plain words. The raw counts that used to sit on the front page live here. */
934
+ export function machineRecordNode(doc, m, opts = {}) {
935
+ const online = !!m.online;
936
+ const sessions = (m.activeSessionCount != null)
937
+ ? `${m.activeSessionCount}${m.maxSessions ? ' / ' + m.maxSessions : ''} conversation${m.activeSessionCount === 1 ? '' : 's'}`
938
+ : '—';
939
+ const rows = [
940
+ ['Name', m.nickname || m.machineId || '—'],
941
+ ['Status', machineStatusWord(m)],
942
+ ['Specs', machineSpecLine(m.hardware)],
943
+ ['Conversations', online ? sessions : '—'],
944
+ ];
945
+ const p = m.guardPosture;
946
+ if (p) {
947
+ const on = (p.onConfirmed || 0) + (p.onUnverified || 0) + (p.onDryRun || 0);
948
+ rows.push(['Safety checks on', String(on)]);
949
+ const problems = guardPostureProblems(p);
950
+ if (problems > 0) rows.push(['Safety checks needing a look', String(problems)]);
951
+ } else {
952
+ rows.push(['Safety checks', 'not reported yet']);
953
+ }
954
+ const wrap = recordFields(doc, rows);
955
+ // #1429: the editable nickname commits ONLY on Enter/blur, with optimistic local
956
+ // echo; the poll stays authority for external renames. The drill holds it (F9).
957
+ if (typeof opts.onRename === 'function' && m.machineId) {
958
+ const editRow = el(doc, 'div', 'glance-record-row');
959
+ editRow.appendChild(el(doc, 'span', 'glance-record-key', 'Rename'));
960
+ const input = doc.createElement('input');
961
+ input.type = 'text';
962
+ input.className = 'glance-record-input machine-nick';
963
+ input.value = m.nickname || m.machineId;
964
+ input.setAttribute('data-mid', m.machineId);
965
+ input.setAttribute('aria-label', 'Machine nickname — press Enter or click away to save');
966
+ let last = input.value;
967
+ const commit = () => {
968
+ const next = input.value.trim();
969
+ if (next === '' || next === last) { input.value = last; return; }
970
+ const prev = last;
971
+ last = next; // optimistic local echo — keep the typed value
972
+ // prev lets the caller revert this optimistic echo if the save fails.
973
+ opts.onRename(m.machineId, next, input, prev);
974
+ };
975
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); input.blur(); } });
976
+ input.addEventListener('blur', commit);
977
+ editRow.appendChild(input);
978
+ wrap.appendChild(editRow);
979
+ }
980
+ return wrap;
981
+ }
982
+
983
+ export function guardRowText(g) {
984
+ return `${humanizeToken(g.key)} — ${guardExplanation(g)}`;
985
+ }
986
+ export function guardRecordNode(doc, g) {
987
+ const rt = g.runtime;
988
+ const rows = [
989
+ ['Check', humanizeToken(g.key)],
990
+ ['In plain words', guardExplanation(g)],
991
+ ['Turned on in settings', g.configEnabled == null ? 'not set' : (g.configEnabled ? 'yes' : 'no')],
992
+ ['On by default', g.defaultEnabled == null ? '—' : (g.defaultEnabled ? 'yes' : 'no')],
993
+ ['Runs in', g.process === 'lifeline' ? 'the always-on lifeline' : 'the main server'],
994
+ ];
995
+ if (g.loadBearing) rows.push(['Load-bearing', 'yes — a safety-critical check']);
996
+ if (rt && typeof rt === 'object') {
997
+ if (rt.dryRun) rows.push(['Mode', 'practice-only (not acting for real yet)']);
998
+ if (typeof rt.jobCount === 'number') rows.push(['Watched items', String(rt.jobCount)]);
999
+ }
1000
+ if (g.error) rows.push(['Error', g.error]);
1001
+ return recordFields(doc, rows);
1002
+ }
1003
+
1004
+ /**
1005
+ * Build the Machines glance spec with drill wiring. `guards` is the LOCAL /guards
1006
+ * view ({ guards, summary }) or null. `opts.onRename(machineId, nickname, input)`
1007
+ * wires the #1429 nickname edit onto each machine's Layer-3 record.
1008
+ */
1009
+ export function machinesGlanceSpec(doc, pool, guards = null, opts = {}) {
1010
+ const base = buildMachinesGlance(pool, guards);
1011
+ const tiles = base.tiles.map((t) => {
1012
+ if (t.key === 'guards') {
1013
+ return {
1014
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1015
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1016
+ const rows = (t.rows || []).slice().sort((a, b) => (guardNeedsLook(b) ? 1 : 0) - (guardNeedsLook(a) ? 1 : 0));
1017
+ if (rows.length === 0) return;
1018
+ const list = el(d, 'div', 'glance-list');
1019
+ for (const g of rows) {
1020
+ const row = d.createElement('button');
1021
+ row.type = 'button';
1022
+ row.className = 'glance-list-row';
1023
+ if (guardNeedsLook(g)) row.setAttribute('data-tone', 'warn');
1024
+ row.setAttribute('aria-label', 'Open the full record');
1025
+ row.appendChild(el(d, 'span', 'glance-list-summary', guardRowText(g)));
1026
+ row.addEventListener('click', () => openRecord(guardRecordNode(d, g)));
1027
+ list.appendChild(row);
1028
+ }
1029
+ drilldown.appendChild(list);
1030
+ },
1031
+ };
1032
+ }
1033
+ return {
1034
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1035
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1036
+ const rows = t.rows || [];
1037
+ if (rows.length === 0) return;
1038
+ const list = el(d, 'div', 'glance-list');
1039
+ for (const m of rows) {
1040
+ const row = d.createElement('button');
1041
+ row.type = 'button';
1042
+ row.className = 'glance-list-row';
1043
+ if (machineNeedsAttention(m)) row.setAttribute('data-tone', 'warn');
1044
+ row.setAttribute('aria-label', 'Open the full record');
1045
+ row.appendChild(el(d, 'span', 'glance-list-summary', machineRowText(m)));
1046
+ row.addEventListener('click', () => openRecord(machineRecordNode(d, m, { onRename: opts.onRename })));
1047
+ list.appendChild(row);
1048
+ }
1049
+ drilldown.appendChild(list);
1050
+ },
1051
+ };
1052
+ });
1053
+ return { headline: base.headline, tiles, population: base.population };
1054
+ }
1055
+
1056
+ // ── Health glance (F10/F11) ──────────────────────────────────────────────────
1057
+ // The old front page dumped ~390 words of subsystem prose. On the floor: a plain
1058
+ // headline ("All systems are operational." / "N subsystems need attention.") over
1059
+ // Subsystems / Need attention / Recent events tiles. Each subsystem's full prose
1060
+ // description, processes, and metrics move to its Layer-3 record — nothing lost.
1061
+
1062
+ /** The single population: the subsystems GET /systems/status reports as active. */
1063
+ export function healthPopulation(systems) {
1064
+ const caps = systems && Array.isArray(systems.activeCapabilities) ? systems.activeCapabilities : [];
1065
+ return caps.filter((c) => c && typeof c.id === 'string');
1066
+ }
1067
+
1068
+ export function buildHealthGlance(systems) {
1069
+ const caps = healthPopulation(systems);
1070
+ const errored = caps.filter((c) => c.status === 'error');
1071
+ const events = systems && Array.isArray(systems.recentEvents) ? systems.recentEvents : [];
1072
+ const healthy = (systems && systems.health === 'healthy') && errored.length === 0;
1073
+
1074
+ let headline;
1075
+ if (caps.length === 0) {
1076
+ headline = 'No subsystems are reporting yet.';
1077
+ } else if (healthy) {
1078
+ headline = 'All systems are operational.';
1079
+ } else {
1080
+ headline = errored.length === 1
1081
+ ? '1 subsystem needs attention.'
1082
+ : `${errored.length} subsystems need attention.`;
1083
+ }
1084
+
1085
+ const tiles = [
1086
+ { key: 'subsystems', label: 'Subsystems', value: String(caps.length), tone: 'neutral', rows: caps },
1087
+ { key: 'attention', label: 'Need attention', value: String(errored.length), tone: errored.length ? 'warn' : 'neutral', rows: errored },
1088
+ { key: 'events', label: 'Recent events', value: String(events.length), tone: 'muted', rows: events },
1089
+ ];
1090
+
1091
+ return { headline, tiles, population: caps };
1092
+ }
1093
+
1094
+ const CAP_STAT_LABEL = {
1095
+ recoveries: 'Recovered', interventions: 'Auto-fixed', activeCases: 'Active',
1096
+ coherencePassed: 'Checks OK', coherenceFailed: 'Issues', memoryPercent: 'Memory %',
1097
+ enabledJobs: 'Jobs', activeJobSessions: 'Running', queueLength: 'Queued',
1098
+ weeklyUsage: 'Weekly %', fiveHourRate: '5h rate %', topicMappings: 'Topics',
1099
+ totalMessages: 'Messages', proposals: 'Proposals', gaps: 'Gaps', learningsApplied: 'Applied',
1100
+ };
1101
+
1102
+ export function healthCapRowText(c) {
1103
+ const word = c.status === 'error' ? 'needs attention' : 'working';
1104
+ return `${sanitizeForDisplay(c.label || c.id || 'A subsystem', 'label')} — ${word}`;
1105
+ }
1106
+ export function healthCapRecordNode(doc, c) {
1107
+ const rows = [
1108
+ ['Subsystem', c.label || c.id || '—'],
1109
+ ['Status', c.status === 'error' ? 'Needs attention' : 'Working'],
1110
+ ['What it does', c.description || '—'],
1111
+ ['Right now', c.metric || '—'],
1112
+ ];
1113
+ for (const p of (c.processes || [])) {
1114
+ rows.push([p.name, p.status === 'running' ? 'Running' : 'Error']);
1115
+ }
1116
+ for (const [key, label] of Object.entries(CAP_STAT_LABEL)) {
1117
+ const v = c.stats ? c.stats[key] : undefined;
1118
+ if (v != null && v !== 0 && v !== false) rows.push([label, String(v)]);
1119
+ }
1120
+ return recordFields(doc, rows);
1121
+ }
1122
+ export function healthEventRowText(e) {
1123
+ return sanitizeForDisplay(e.narrative || e.subsystem || 'An event', 'summary');
1124
+ }
1125
+ export function healthEventRecordNode(doc, e) {
1126
+ const when = e.timestamp ? defaultFmtTs(e.timestamp) : '—';
1127
+ return recordFields(doc, [
1128
+ ['What happened', e.narrative || '—'],
1129
+ ['Part', e.subsystem || '—'],
1130
+ ['When', when],
1131
+ ]);
1132
+ }
1133
+
1134
+ export function healthGlanceSpec(doc, systems) {
1135
+ const base = buildHealthGlance(systems);
1136
+ const tiles = base.tiles.map((t) => {
1137
+ const rowText = t.key === 'events' ? healthEventRowText : healthCapRowText;
1138
+ const recordNode = t.key === 'events' ? healthEventRecordNode : healthCapRecordNode;
1139
+ return {
1140
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1141
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1142
+ const rows = t.rows || [];
1143
+ if (rows.length === 0) return;
1144
+ const list = el(d, 'div', 'glance-list');
1145
+ for (const item of rows) {
1146
+ const row = d.createElement('button');
1147
+ row.type = 'button';
1148
+ row.className = 'glance-list-row';
1149
+ if (t.key !== 'events' && item.status === 'error') row.setAttribute('data-tone', 'warn');
1150
+ row.setAttribute('aria-label', 'Open the full record');
1151
+ row.appendChild(el(d, 'span', 'glance-list-summary', rowText(item)));
1152
+ row.addEventListener('click', () => openRecord(recordNode(d, item)));
1153
+ list.appendChild(row);
1154
+ }
1155
+ drilldown.appendChild(list);
1156
+ },
1157
+ };
1158
+ });
1159
+ return { headline: base.headline, tiles, population: base.population };
1160
+ }
1161
+
1162
+ // ── Spend glance (F10/F11) ───────────────────────────────────────────────────
1163
+ // The old front page leaned on "paid door / metered / reflows". On the floor: a
1164
+ // plain headline ("Nothing is being billed per-call right now.") over Estimated
1165
+ // cost / Text processed / Pay-per-use access tiles. "Metered / paid door" reads as
1166
+ // "pay-per-use"; the price-reflow detail and per-model math move to Layer 3.
1167
+
1168
+ export function buildSpendGlance(summary, caps = null) {
1169
+ const totals = (summary && summary.totals) || {};
1170
+ const rows = (summary && Array.isArray(summary.rows)) ? summary.rows : [];
1171
+ const keys = (caps && Array.isArray(caps.keys)) ? caps.keys : [];
1172
+ const meteredLiveYet = !!(summary && summary.meteredLiveYet) || !!(caps && caps.meteredLiveYet);
1173
+
1174
+ const netUsd = Number(totals.netUsd) || 0;
1175
+ const headline = meteredLiveYet
1176
+ ? `About ${fmtUsd(netUsd)} of pay-per-use AI so far.`
1177
+ : 'Nothing is being billed per-call right now.';
1178
+
1179
+ const tokensTotal = (Number(totals.tokensIn) || 0) + (Number(totals.tokensOut) || 0);
1180
+ const tiles = [
1181
+ { key: 'cost', label: 'Estimated cost', value: fmtUsd(netUsd), tone: 'neutral', rows, mode: 'cost' },
1182
+ { key: 'usage', label: 'Text processed', value: fmtCount(tokensTotal), tone: 'muted', rows, mode: 'usage' },
1183
+ { key: 'access', label: 'Pay-per-use access', value: String(keys.length), tone: 'muted', rows: keys, mode: 'access' },
1184
+ ];
1185
+
1186
+ return { headline, tiles, population: rows };
1187
+ }
1188
+
1189
+ function plainPriceBasis(r) {
1190
+ if (r.priceBasis === 'subscription-zero') return 'Subscription — not billed per use';
1191
+ if (r.priceBasis === 'no-matching-point') return 'No price on file yet';
1192
+ return String(r.priceBasis || '—') + (r.priceStale ? ' (out of date)' : '');
1193
+ }
1194
+ function plainGoLive(state) {
1195
+ if (state === 'live') return 'Live';
1196
+ if (state === 'disarmed') return 'Turned off';
1197
+ return 'Not switched on yet';
1198
+ }
1199
+
1200
+ export function spendRowCostText(r) {
1201
+ return `${modelPhrase(r)} — ${fmtUsd(Number(r.netUsd) || 0)}`;
1202
+ }
1203
+ export function spendRowUsageText(r) {
1204
+ const total = (Number(r.tokensIn) || 0) + (Number(r.tokensOut) || 0);
1205
+ return `${modelPhrase(r)} — ${fmtCount(total)} pieces of text`;
1206
+ }
1207
+ export function spendKeyRowText(k) {
1208
+ return `${humanizeToken(k.provider || k.door || 'A provider')} — pay-per-use · ${plainGoLive(k.goLiveState)}`;
1209
+ }
1210
+ export function spendRowRecordNode(doc, r) {
1211
+ return recordFields(doc, [
1212
+ ['Model', friendlyModel(r.modelId) || r.modelId || '—'],
1213
+ ['How it is reached', humanizeToken(r.door)],
1214
+ ['Access type', doorClassWord(r.doorClass)],
1215
+ ['Text in', fmtCount(r.tokensIn)],
1216
+ ['Text out', fmtCount(r.tokensOut)],
1217
+ ['Gross cost', fmtUsd(r.grossUsd, 4)],
1218
+ ['Net cost', fmtUsd(r.netUsd, 4)],
1219
+ ['How it is priced', plainPriceBasis(r)],
1220
+ ]);
1221
+ }
1222
+ export function spendKeyRecordNode(doc, k) {
1223
+ return recordFields(doc, [
1224
+ ['Provider', k.provider || '—'],
1225
+ ['How it is reached', humanizeToken(k.door)],
1226
+ ['Daily limit', fmtUsd(k.dailyCapUsd)],
1227
+ ['Lifetime limit', fmtUsd(k.lifetimeCapUsd)],
1228
+ ['Used today', fmtUsd(k.committedDayUsd)],
1229
+ ['Used in total', fmtUsd(k.committedLifetimeUsd)],
1230
+ ['Status', plainGoLive(k.goLiveState) + (k.frozen ? ' · paused' : '')],
1231
+ ]);
1232
+ }
1233
+
1234
+ export function spendGlanceSpec(doc, summary, caps = null) {
1235
+ const base = buildSpendGlance(summary, caps);
1236
+ const tiles = base.tiles.map((t) => ({
1237
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1238
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1239
+ const rows = t.rows || [];
1240
+ if (rows.length === 0) return;
1241
+ const list = el(d, 'div', 'glance-list');
1242
+ for (const item of rows) {
1243
+ const row = d.createElement('button');
1244
+ row.type = 'button';
1245
+ row.className = 'glance-list-row';
1246
+ row.setAttribute('aria-label', 'Open the full record');
1247
+ const text = t.mode === 'access' ? spendKeyRowText(item)
1248
+ : t.mode === 'usage' ? spendRowUsageText(item)
1249
+ : spendRowCostText(item);
1250
+ row.appendChild(el(d, 'span', 'glance-list-summary', text));
1251
+ const recNode = t.mode === 'access' ? spendKeyRecordNode(d, item) : spendRowRecordNode(d, item);
1252
+ row.addEventListener('click', () => openRecord(recNode));
1253
+ list.appendChild(row);
1254
+ }
1255
+ drilldown.appendChild(list);
1256
+ },
1257
+ }));
1258
+ return { headline: base.headline, tiles, population: base.population };
1259
+ }
1260
+
1261
+ // ── Routing Map glance (F10/F11) ─────────────────────────────────────────────
1262
+ // The old front page used "lane / nature / door". On the floor: a plain headline
1263
+ // ("Background AI work runs on X, with Y as backup.") over one tile per lane
1264
+ // (plain-named) + a Job-types tile. Each lane drills into its ordered fallback
1265
+ // list of plain model names (Layer 2); each opens the full door/model config
1266
+ // (Layer 3). The insider door flags live at Layer 3, not the front page.
1267
+
1268
+ const CHAIN_ORDER = ['FAST', 'SORT', 'JUDGE', 'WRITE'];
1269
+ const CHAIN_LABEL = { FAST: 'Quick tasks', SORT: 'Sorting', JUDGE: 'Judging', WRITE: 'Writing' };
1270
+
1271
+ /** The primary lane's first two positions drive the headline (X + backup Y). */
1272
+ function primaryLane(map) {
1273
+ const chains = map && Array.isArray(map.chains) ? map.chains : [];
1274
+ return chains.find((c) => c.chain === 'FAST') || chains[0] || null;
1275
+ }
1276
+
1277
+ export function buildRoutingMapGlance(map) {
1278
+ const chains = map && Array.isArray(map.chains) ? map.chains : [];
1279
+ const components = map && Array.isArray(map.components) ? map.components : [];
1280
+
1281
+ const lead = primaryLane(map);
1282
+ const positions = (lead && Array.isArray(lead.positions)) ? lead.positions : [];
1283
+ let headline;
1284
+ if (chains.length === 0) {
1285
+ headline = 'No background AI routing is set up yet.';
1286
+ } else {
1287
+ const x = modelPhrase(positions[0]) || 'the built-in model';
1288
+ const y = modelPhrase(positions[1]);
1289
+ headline = y
1290
+ ? `Background AI work runs on ${x}, with ${y} as backup.`
1291
+ : `Background AI work runs on ${x}.`;
1292
+ }
1293
+
1294
+ const tiles = [];
1295
+ const ordered = chains.slice().sort((a, b) => CHAIN_ORDER.indexOf(a.chain) - CHAIN_ORDER.indexOf(b.chain));
1296
+ for (const c of ordered) {
1297
+ if (tiles.length >= GLANCE_MAX_TILES - 1) break; // leave room for the Job-types tile
1298
+ const pos = Array.isArray(c.positions) ? c.positions : [];
1299
+ tiles.push({
1300
+ key: 'lane-' + String(c.chain || '').toLowerCase(),
1301
+ label: CHAIN_LABEL[c.chain] || humanizeToken(c.chain),
1302
+ value: String(pos.length),
1303
+ tone: 'neutral',
1304
+ rows: pos,
1305
+ mode: 'lane',
1306
+ });
1307
+ }
1308
+ tiles.push({ key: 'jobs', label: 'Job types', value: String(components.length), tone: 'muted', rows: components, mode: 'jobs' });
1309
+
1310
+ return { headline, tiles, population: components };
1311
+ }
1312
+
1313
+ export function laneRowText(p, i) {
1314
+ const rank = typeof i === 'number' ? `${i + 1}. ` : '';
1315
+ return `${rank}${modelPhrase(p)}${p.doorClass === 'metered' ? ' (pay-per-use)' : ''}`;
1316
+ }
1317
+ export function laneRecordNode(doc, p) {
1318
+ return recordFields(doc, [
1319
+ ['Model', friendlyModel(p.modelId) || p.modelId || '—'],
1320
+ ['How it is reached', humanizeToken(p.door)],
1321
+ ['Access type', doorClassWord(p.doorClass)],
1322
+ ['Safe for untrusted input', p.injectionSafe ? 'yes' : 'no'],
1323
+ ['Pay-per-use', p.moneyGated ? 'yes' : 'no'],
1324
+ ['Skipped for now', p.skippedInIncrementA ? 'yes' : 'no'],
1325
+ ]);
1326
+ }
1327
+ export function jobRowText(c) {
1328
+ const lane = CHAIN_LABEL[c.chain] || 'default routing';
1329
+ return `${humanizeToken(c.component)} — ${lane}`;
1330
+ }
1331
+ export function jobRecordNode(doc, c) {
1332
+ const exposure = c.injectionExposure
1333
+ ? (c.injectionExposure.exposed ? 'yes' : 'no')
1334
+ : (c.untrustedInput === true ? 'yes' : c.untrustedInput === false ? 'no' : '—');
1335
+ return recordFields(doc, [
1336
+ ['Job kind', humanizeToken(c.component)],
1337
+ ['Type', humanizeToken(c.category)],
1338
+ ['Lane', CHAIN_LABEL[c.chain] || 'default routing'],
1339
+ ['Safety-critical', c.criticalGate ? 'yes' : 'no'],
1340
+ ['Can see untrusted input', exposure],
1341
+ ]);
1342
+ }
1343
+
1344
+ export function routingMapGlanceSpec(doc, map) {
1345
+ const base = buildRoutingMapGlance(map);
1346
+ const tiles = base.tiles.map((t) => ({
1347
+ key: t.key, label: t.label, value: t.value, tone: t.tone,
1348
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
1349
+ const rows = t.rows || [];
1350
+ if (rows.length === 0) return;
1351
+ const list = el(d, 'div', 'glance-list');
1352
+ rows.forEach((item, i) => {
1353
+ const row = d.createElement('button');
1354
+ row.type = 'button';
1355
+ row.className = 'glance-list-row';
1356
+ row.setAttribute('aria-label', 'Open the full record');
1357
+ const text = t.mode === 'jobs' ? jobRowText(item) : laneRowText(item, i);
1358
+ row.appendChild(el(d, 'span', 'glance-list-summary', text));
1359
+ const recNode = t.mode === 'jobs' ? jobRecordNode(d, item) : laneRecordNode(d, item);
1360
+ row.addEventListener('click', () => openRecord(recNode));
1361
+ list.appendChild(row);
1362
+ });
1363
+ drilldown.appendChild(list);
1364
+ },
1365
+ }));
1366
+ return { headline: base.headline, tiles, population: base.population };
1367
+ }