create-pmos 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -10,12 +10,12 @@
10
10
  "scripts": {
11
11
  "start": "node server.js",
12
12
  "test": "node test.js && npm run test:modules",
13
- "test:modules": "node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js",
13
+ "test:modules": "node proactive.test.js && node focus.test.js && node handoff.test.js && node repo-scan.test.js && node profiles.test.js && node external-linking.test.js && node story-artifacts.test.js && node no-network.test.js && node typesafe-validation.test.js && node claims.test.js && node repo-watch.test.js && node fixture-parity.test.mjs && node markdown.test.mjs && node start-policy.test.js && node state-isolation.test.js && node sync.test.js && node schedule-cli.test.js && node initiative-surface.test.js && node guide.test.js && node next-step-actions.test.js",
14
14
  "test:ui": "node ui-test.js",
15
15
  "test:parity": "node design-parity.js --states",
16
16
  "test:all": "npm test && npm run test:panels && npm run test:ui && npm run test:parity && npm run test:offline",
17
17
  "doctor": "node doctor.js",
18
- "test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js",
18
+ "test:panels": "node pm-shell-test.js && node pm-repos-test.js && node pm-initiatives-test.js && node pm-detail-test.js && node pm-tabs-test.js && node pm-term-test.js && node pm-modes-test.js && node pm-r1-test.js && node pm-r2-test.js && node pm-r3-test.js && node pm-r4-test.js && node pm-r5-test.js && node pm-r6-test.js && node pm-r7-test.js && node pm-r8-test.js && node pm-r9-test.js && node pm-r10-test.js && node pm-r11-test.js && node pm-r12-test.js && node pm-r13-test.js && node pm-r14-test.js && node pm-r15-test.js && node pm-r16-test.js && node pm-r17-test.js && node pm-r16-test.js && node pm-r18-test.js && node pm-r19-test.js && node pm-r20-test.js && node pm-r21-test.js && node pm-r22-test.js && node pm-r23-test.js",
19
19
  "test:offline": "node offline-tarball.test.js"
20
20
  },
21
21
  "engines": {
@@ -33,7 +33,7 @@ import { isMobile, mountMobile, applyMobile } from './mobile.js';
33
33
  const MODE_TITLE = {
34
34
  product: 'initiatives', scheduled: 'scheduled tasks', workflow: 'workflows', skills: 'skills',
35
35
  knowledge: 'knowledge', agent: 'agents', memory: 'memory', connect: 'connections', focus: 'focus',
36
- portfolio: 'portfolio', guide: 'guide', validate: 'validate',
36
+ portfolio: 'portfolio', guide: 'guide', validate: 'validate', today: 'today',
37
37
  };
38
38
  const DETAIL_TITLE = {
39
39
  init: 'initiative', flow: 'workflow', sched: 'scheduled task', skill: 'skill', know: 'decision',
@@ -1433,9 +1433,9 @@ export async function readDoc(path, ws, ref) {
1433
1433
  const url = '/api/doc?' + q({ ws: ws || state.wsId || (p ? p.ws : ''), path, ...(ref ? { ref } : {}) });
1434
1434
  const res = await fetch(url);
1435
1435
  if (!res.ok) {
1436
- // The server states why a branch read was refused; the plain 404 text is the generic one.
1436
+ // The server states why a read was refused (binary, too large, not text, outside the workspace); show that reason.
1437
1437
  const why = res.status === 404 ? (await res.text().catch(() => '')) : '';
1438
- throw new Error(res.status === 404 ? (why && ref ? why : 'not a readable text document in this workspace') : 'HTTP ' + res.status);
1438
+ throw new Error(res.status === 404 ? (why || 'not a readable text document in this workspace') : 'HTTP ' + res.status);
1439
1439
  }
1440
1440
  return res.text();
1441
1441
  }
@@ -581,12 +581,90 @@ async function vRun() {
581
581
  vSay(out.aborted ? 'bad' : 'ok', 'asked ' + out.asked + ' of ' + out.requests + ' · ' + out.raised + ' raised · ' + out.local_findings + ' local · ' + out.withheld + ' withheld · ' + out.failed + ' failed' + (out.aborted ? ' — ' + out.aborted : ''));
582
582
  }
583
583
 
584
+
585
+ /* ── claims-ledger — is this repository consistent? Local checks first; nothing here sends anything ───────────────
586
+ * The verdict says what was checked and against what, in plain words. A finding names the documents and puts the
587
+ * truth value beside the claim. "Consistent" means no recorded statement contradicts its source: prose is not read,
588
+ * and the page says so every time. */
589
+ let cOpen = new Set(); // 'undecided' | 'fine' | 'unclassified' — which collapsed groups are open
590
+ let cNote = null;
591
+ const cSay = (text) => { cNote = text; setState({}); };
592
+ async function cCheck() {
593
+ const ws = vWs(); if (!ws) return cSay('select a repository first');
594
+ cSay('checking …');
595
+ const out = await post('/api/claims/check', { ws });
596
+ reload('claims');
597
+ cSay(out && out.ok ? 'checked just now — ' + out.run.reads + ' file(s) read, ' + out.run.affected + ' re-verified, ' + out.run.appended + ' recorded' : ((out && out.error) || 'the check failed'));
598
+ }
599
+ async function cWatch(on) {
600
+ const ws = vWs(); if (!ws) return;
601
+ const out = await post('/api/claims/watch', { ws, watch: on });
602
+ reload('claims');
603
+ cSay(out && out.ok ? (on ? 'now checked automatically whenever its files change' : 'automatic checking is off') : ((out && out.error) || 'not saved'));
604
+ }
605
+ const cToggle = (k) => { if (cOpen.has(k)) cOpen.delete(k); else cOpen.add(k); setState({}); };
606
+ const TYPE_CHIP = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground)';
607
+
608
+ function claimsBlock(ws) {
609
+ const kids = [];
610
+ kids.push(el('div', { style: LABEL }, iv('consistency of this repository')));
611
+ if (!ws) return [...kids, el('div', { style: NOTE }, iv('Select a repository first.'))];
612
+ const r = read('claims', '/api/claims?ws=' + encodeURIComponent(ws));
613
+ const st = status(r, 'the consistency checks');
614
+ if (st) return [...kids, st];
615
+ const d = r.data; const c = d.counts;
616
+ const word = d.checking ? 'Checking…' : d.verdict === 'not-checked' ? 'Not checked yet' : d.verdict === 'consistent' ? 'Consistent' : c.problems + (c.problems === 1 ? ' problem' : ' problems');
617
+ const tone = d.verdict === 'problems' ? 'var(--destructive)' : 'var(--foreground)';
618
+ const why = d.verdict === 'not-checked'
619
+ ? 'Press “check now” to read this repository’s documents and config files and compare each with what it must agree with.'
620
+ : 'Checked ' + c.documents + ' documents and config files · ' + c.verified + ' statements verified · ' + c.stale + ' stale · ' + c.undecided + ' worth a look. '
621
+ + c.internal_only + ' are checked by their own structure only; ' + c.unclassified + ' are not classified and are never compared with other files.';
622
+ kids.push(el('div', { 'data-r22': 'verdict', 'data-verdict': d.checking ? 'checking' : d.verdict, style: 'padding:6px 14px;border-left:2px solid ' + tone + ';margin:2px 14px 6px' },
623
+ el('span', { 'data-r22': 'verdict-word', style: 'font-size:15px;font-weight:600;color:' + tone }, iv(word)),
624
+ el('span', { 'data-r22': 'verdict-why', style: 'display:block;font-size:11.5px;color:var(--muted-foreground);line-height:1.5;margin-top:2px' }, iv(why))));
625
+ kids.push(el('div', { 'data-r22': 'limits', style: NOTE }, iv('Checked by structure and the claims record. Prose is not read, so “Consistent” means no recorded statement contradicts its source — not that every sentence was checked. Nothing here is sent anywhere.')));
626
+ kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
627
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'check', disabled: d.checking ? 'true' : null, onClick: cCheck }, 'check now'),
628
+ el('button', { style: BTN + (d.watching ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r22': 'watch', onClick: () => cWatch(!d.watching),
629
+ title: 'When on, the workspace notices changes to files in this repository and re-checks the documents they affect. It records problems in the repository\'s claims.jsonl.' },
630
+ iv(d.watching ? 'automatic checking: ON — turn off' : 'automatic checking: OFF — turn on'))));
631
+ if (d.watching && d.last_run) kids.push(el('div', { 'data-r22': 'last', style: NOTE }, iv('Last automatic check ' + String(d.last_run.at).replace('T', ' ').slice(0, 16) + (d.last_run.ok === false ? ' — failed: ' + (d.last_run.error || 'error') : d.last_run.run ? ' — ' + d.last_run.run.affected + ' document(s) re-verified' : ''))));
632
+ if (cNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE }, iv(cNote)));
633
+ if (d.ledger && d.verdict !== 'not-checked') kids.push(el('div', { style: NOTE }, iv('Record of problems: '), el('button', { style: 'text-decoration:underline;color:var(--foreground)', 'data-r22': 'ledger', onClick: () => openDocIn(ws, d.ledger, state.product) }, iv(d.ledger))));
634
+
635
+ const docs = d.docs || [];
636
+ const finding = (f, rel) => el('div', { 'data-r22': 'finding', 'data-status': f.status, style: 'padding:2px 14px 2px 26px;font-size:11.5px;line-height:1.5;color:' + (f.status === 'contradicted' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(f.sentence));
637
+ const docRow = (x, showFindings) => el('div', { 'data-r22': 'doc', 'data-rel': x.rel, style: 'padding:4px 0;border-bottom:1px solid oklch(1 0 0 / 5%)' },
638
+ el('button', { style: 'display:flex;gap:8px;align-items:baseline;width:100%;padding:2px 14px;text-align:left', hover: 'background:var(--accent)', onClick: () => openDocIn(ws, x.rel, state.product) },
639
+ el('span', { style: 'font-family:var(--mono);font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0' }, iv(x.rel)),
640
+ el('span', { style: TYPE_CHIP + ';margin-left:auto;flex:none' }, iv(x.classified ? x.type : 'not classified'))),
641
+ x.compared_with.length ? el('div', { 'data-r22': 'compared', style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv('compared with: ' + x.compared_with.join(', '))) : (x.classified ? el('div', { style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv(x.references.length ? 'no counterpart found to compare with' : 'checked on its own structure only')) : el('div', { style: 'padding:0 14px 0 26px;font-size:10.5px;color:var(--muted-foreground)' }, iv('nothing declares what this is, so it is checked on its own links and dates only'))),
642
+ showFindings ? x.findings.map((f) => finding(f, x.rel)) : null);
643
+ const group = (key, title, list, open, showFindings) => {
644
+ if (!list.length) return null;
645
+ return el('div', { 'data-r22': 'group', 'data-group': key },
646
+ el('button', { style: LABEL + ';display:block;width:100%;text-align:left', 'data-r22': 'group-toggle', onClick: open === 'always' ? null : () => cToggle(key) }, iv((open === 'always' || cOpen.has(key) ? '▾ ' : '▸ ') + title + ' · ' + list.length)),
647
+ open === 'always' || cOpen.has(key) ? list.slice(0, 150).map((x) => docRow(x, showFindings)) : null,
648
+ list.length > 150 && (open === 'always' || cOpen.has(key)) ? el('div', { style: NOTE }, iv('… and ' + (list.length - 150) + ' more')) : null);
649
+ };
650
+ const withStatus = (st) => docs.filter((x) => x.findings.some((f) => f.status === st));
651
+ const only = (st) => (x) => ({ ...x, findings: x.findings.filter((f) => f.status === st) });
652
+ kids.push(group('problems', 'problems — a statement is contradicted by its source', withStatus('contradicted').map(only('contradicted')), 'always', true));
653
+ kids.push(group('stale', 'stale — a source changed after the document did, or its date passed', withStatus('stale').map(only('stale')), 'always', true));
654
+ kids.push(group('undecided', 'worth a look — structure cannot decide', withStatus('undecided').map(only('undecided')), 'toggle', true));
655
+ kids.push(group('fine', 'checked and fine', docs.filter((x) => x.classified && !x.findings.length), 'toggle', false));
656
+ kids.push(group('unclassified', 'not classified — nothing declares what these are', docs.filter((x) => !x.classified), 'toggle', true));
657
+ return kids.filter(Boolean);
658
+ }
659
+
584
660
  export function validateList() {
585
661
  const kids = [];
586
662
  const k = read('typesafe', '/api/typesafe');
587
663
  const ws = vWs();
588
664
  const rp = ws ? read('vrepo', '/api/validate/repo?ws=' + encodeURIComponent(ws)) : null;
589
665
  const fd = ws ? read('vfind', '/api/validate/findings?ws=' + encodeURIComponent(ws)) : null;
666
+ kids.push(...claimsBlock(ws));
667
+ kids.push(el('div', { style: LABEL + ';margin-top:10px' }, iv('typesafe (jev) — optional, sends text only after you turn it on')));
590
668
  kids.push(el('div', { style: NOTE, 'data-r19': 'intro' }, iv('Checks this repository\'s documents for reliability, coherence and freshness. Local checks run first and send nothing. Text that needs judgement goes to TypeSafe (Jev), only after you switch it on for this repository and confirm what will be sent. Findings are advice; nothing is edited.')));
591
669
 
592
670
  // 1 · the key
@@ -660,3 +738,146 @@ export function validateList() {
660
738
  }
661
739
  return [el('div', { style: 'padding:2px 0 8px', 'data-r19': 'validate' }, kids)];
662
740
  }
741
+
742
+ /* ── Today (proactive-engine D1): what is waiting, computed from the ledgers, with feedback ─────────────────────── */
743
+ let tNote = null;
744
+ let tMissed = false;
745
+ let tRetry = null;
746
+ const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
747
+ const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
748
+ async function tFeedback(body, say) {
749
+ const out = await post('/api/proactive/feedback', body);
750
+ if (!out || out.ok === false) return tSay('bad', (out && out.error) || 'feedback was not saved');
751
+ tSay('ok', say);
752
+ }
753
+ async function tOpen(c) {
754
+ tFeedback({ candidate_id: c.id, event: 'opened' }, 'opened — recorded');
755
+ const o = c.open || {};
756
+ if (o.type === 'schedule') { setState({ mode: 'scheduled', col0: true }); return; }
757
+ if (c.repo && c.initiative_id) await land({ repo: c.repo, id: c.initiative_id });
758
+ }
759
+ async function tScan() {
760
+ const out = await post('/api/proactive/scan', {});
761
+ reload('today'); reload('brief');
762
+ tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'the scan failed') : 'scanned — ' + out.found + ' waiting, ' + out.appended + ' new');
763
+ }
764
+ async function tSettings(patch) {
765
+ const out = await post('/api/proactive/settings', patch);
766
+ reload('today'); reload('brief');
767
+ tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
768
+ }
769
+ function tCard(c) {
770
+ const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
771
+ return el('div', { 'data-r22': 'card', 'data-id': c.id, 'data-kind': c.kind, 'data-repo': c.repo || '', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
772
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
773
+ el('span', { style: 'font-size:12.5px' }, iv(c.title)),
774
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.kind + ' · ' + (OWNER_WORD[c.owner] || c.owner)))),
775
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
776
+ el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
777
+ c.open ? b('open', 'open', () => tOpen(c)) : null,
778
+ b('useful', 'useful', () => tFeedback({ candidate_id: c.id, verdict: 'useful' }, 'thanks — recorded as useful')),
779
+ b('not useful', 'not-useful', () => tFeedback({ candidate_id: c.id, verdict: 'not-useful' }, 'recorded as not useful')),
780
+ b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: c.id, verdict: 'wrong-priority' }, 'recorded as wrong priority')),
781
+ b('dismiss 7 days', 'dismiss', async () => { await tFeedback({ candidate_id: c.id, event: 'dismissed' }, 'dismissed for 7 days'); reload('today'); setState({}); })));
782
+ }
783
+
784
+ /* the morning brief (proactive-engine D2): submitted by a scheduled skill in a session, only checked and stored by the server */
785
+ let bNote = null;
786
+ async function bSchedule() {
787
+ const data = await import('./data.js');
788
+ const ws = state.product ? data.wsOf(state.product) : null;
789
+ if (!ws) { bNote = 'select a repository first'; setState({}); return; }
790
+ const out = await post('/api/schedules/seed', { ws, set: 'brief' });
791
+ bNote = !out || out.ok === false ? ((out && out.error) || 'not scheduled')
792
+ : out.created.length ? 'scheduled: ' + out.created[0].name + ' (' + out.created[0].cadence + ' ' + out.created[0].time + ') — it runs only while the workspace server runs. Made after that time today, it also runs once within a minute.'
793
+ : 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ');
794
+ reload('brief'); setState({});
795
+ }
796
+ function bCard(x, ask) {
797
+ const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
798
+ return el('div', { 'data-r22': ask ? 'ask-card' : 'brief-card', 'data-id': x.id, 'data-source': x.source, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
799
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
800
+ el('span', { style: 'font-size:12.5px' }, iv(x.title)),
801
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(x.source + ' · ' + (OWNER_WORD[x.owner] || x.owner) + ' · ' + x.effort + ' · ' + x.confidence))),
802
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(x.why_now + (x.evidence ? ' — ' + x.evidence : ''))),
803
+ el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
804
+ b('useful', 'useful', () => tFeedback({ candidate_id: x.id, verdict: 'useful' }, 'thanks — recorded as useful')),
805
+ b('not useful', 'not-useful', () => tFeedback({ candidate_id: x.id, verdict: 'not-useful' }, 'recorded as not useful')),
806
+ b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: x.id, verdict: 'wrong-priority' }, 'recorded as wrong priority'))));
807
+ }
808
+ function briefSection() {
809
+ const r = read('brief', '/api/proactive/brief');
810
+ const st = status(r, 'the brief');
811
+ if (st) return [st];
812
+ const d = r.data; const br = d.brief;
813
+ const kids = [el('div', { 'data-r22': 'greeting', style: 'padding:10px 14px 2px;font-size:15px;color:var(--foreground)' }, iv(d.greeting))];
814
+ if (!br) {
815
+ kids.push(el('div', { style: NOTE, 'data-r22': 'no-brief' }, iv('There is no brief yet. A scheduled skill in an agent session reads your calendar and other sources and submits it. This server only checks and stores it.' + (d.scheduled.length ? ' It is scheduled for ' + d.scheduled.map((x) => x.time).join(', ') + ' and runs only while the workspace server runs.' : ''))));
816
+ if (!d.scheduled.length) kids.push(el('div', { style: 'padding:0 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'schedule-brief', onClick: bSchedule }, 'schedule the morning brief (06:45 daily)')));
817
+ if (bNote) kids.push(el('div', { style: NOTE, 'data-r22': 'brief-note' }, iv(bNote)));
818
+ return kids;
819
+ }
820
+ kids.push(el('div', { style: NOTE, 'data-r22': 'brief-date' }, iv('Brief for ' + br.date + ' · made ' + String(br.generated_at).replace('T', ' ').slice(0, 16) + ' UTC · typed scoring ' + (br.judgment.used ? 'used (the brief says so; the server cannot check it)' : 'not used'))));
821
+ if (d.stale) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'stale' }, iv('This brief is for ' + br.date + ', not today (' + d.today + '). It is out of date.')));
822
+ kids.push(el('div', { style: NOTE, 'data-r22': 'sources' }, iv('Sources: ' + Object.entries(br.sources).map(([k, v]) => k + ' ' + v).join(' · '))));
823
+ kids.push(el('div', { style: LABEL }, iv('calendar')));
824
+ if (!br.calendar.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-events' }, iv(br.sources.calendar === 'read' ? 'No events today.' : 'The calendar was not read, so this says nothing about your day.')));
825
+ for (const e of br.calendar) kids.push(el('div', { 'data-r22': 'event', style: 'padding:3px 14px;font-size:12px' }, iv(new Date(e.start).toTimeString().slice(0, 5) + ' ' + e.title + (e.with ? ' — ' + e.with : ''))));
826
+ kids.push(el('div', { style: LABEL }, iv('priorities')));
827
+ if (!br.priorities.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-priorities' }, iv('No priorities in this brief.')));
828
+ for (const x of br.priorities) kids.push(bCard(x, false));
829
+ if (br.worth_asking.length) {
830
+ kids.push(el('div', { style: LABEL }, iv('worth asking · the brief is unsure')));
831
+ for (const x of br.worth_asking) kids.push(bCard(x, true));
832
+ }
833
+ return kids;
834
+ }
835
+ export function todayList() {
836
+ const r = read('today', '/api/proactive/today');
837
+ const kids = [el('div', { style: NOTE, 'data-r22': 'intro' }, iv('What is waiting for you, worked out from the ledgers and git on this computer. It runs only while the workspace server runs, reads no mail or calendar, and changes nothing: your feedback only changes how this list is ordered later.'))];
838
+ kids.push(...briefSection());
839
+ kids.push(el('div', { style: LABEL }, iv('waiting on records')));
840
+ kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:4px 14px' },
841
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'scan', onClick: tScan }, 'scan now'),
842
+ el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); setState({}); } }, '↻ refresh'),
843
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
844
+ if (tMissed) {
845
+ const ta = el('input', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' });
846
+ kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
847
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') tMissed = false; setState({}); } }, 'save')));
848
+ }
849
+ if (tNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE + ';color:' + (tNote.tone === 'bad' ? 'var(--destructive)' : tNote.tone === 'ok' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(tNote.text)));
850
+ const st = status(r, 'Today');
851
+ if (st) return [...kids, st];
852
+ const d = r.data;
853
+ const s = d.settings;
854
+ if (d.off) kids.push(el('div', { style: NOTE, 'data-r22': 'off' }, iv('The engine is switched off. Nothing is scanned automatically; "scan now" still works.')));
855
+ if (d.quiet) kids.push(el('div', { style: NOTE, 'data-r22': 'quiet' }, iv('Quiet hours (' + s.quiet.start + '–' + s.quiet.end + '): no automatic scans now.')));
856
+ // a repository still being read is asked again in a few seconds, so the list fills in without a press
857
+ if ((d.unreadable || []).length && !tRetry && typeof setTimeout === 'function') tRetry = setTimeout(() => { tRetry = null; reload('today'); setState({}); }, 4000);
858
+ for (const u of d.unreadable || []) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'unreadable' }, iv('Not read — ' + (u.name || u.repo || 'a repository') + ': ' + (u.error || 'unknown reason') + '. Nothing is shown for it, which is not the same as nothing waiting.')));
859
+ if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
860
+ for (const c of d.candidates || []) kids.push(tCard(c));
861
+ if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
862
+ // settings
863
+ kids.push(el('div', { style: LABEL }, iv('settings')));
864
+ const chk = (label, on, attr, patch) => el('label', { style: 'display:flex;gap:8px;align-items:center;padding:2px 14px;font-size:12px' },
865
+ el('input', { type: 'checkbox', checked: on ? 'checked' : null, 'data-r22': attr, onChange: () => tSettings(patch(!on)) }), iv(label));
866
+ kids.push(chk('engine on', s.enabled, 'set-enabled', (v) => ({ enabled: v })));
867
+ for (const t of Object.keys(s.triggers)) kids.push(chk('watch: ' + t, s.triggers[t], 'set-' + t, (v) => ({ triggers: { [t]: v } })));
868
+ kids.push(el('div', { style: LABEL }, iv('morning brief')));
869
+ for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
870
+ kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
871
+ const nm = el('input', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', value: s.name || '', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
872
+ kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSettings({ name: nm.value }) }, 'save')));
873
+ kids.push(el('div', { style: LABEL }, iv('learning')));
874
+ kids.push(chk('pause learning (nothing learns yet)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
875
+ const days = el('input', { type: 'number', min: '1', max: '90', value: String(s.stalledDays), 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
876
+ const qs = el('input', { type: 'text', placeholder: '22:00', value: s.quiet ? s.quiet.start : '', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
877
+ const qe = el('input', { type: 'text', placeholder: '07:00', value: s.quiet ? s.quiet.end : '', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' });
878
+ kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px;font-size:12px' }, iv('stalled after'), days, iv('days'),
879
+ el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSettings({ stalledDays: Number(days.value) }) }, 'save')));
880
+ kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px 8px;font-size:12px' }, iv('quiet hours'), qs, iv('to'), qe,
881
+ el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSettings({ quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
882
+ return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
883
+ }
@@ -1046,6 +1046,7 @@ const LISTS = {
1046
1046
  portfolio: () => (FIXTURE ? [] : LIVE.portfolioList()),
1047
1047
  guide: () => (FIXTURE ? [] : LIVE.guideList()),
1048
1048
  validate: () => (FIXTURE ? [] : LIVE.validateList()),
1049
+ today: () => (FIXTURE ? [] : LIVE.todayList()),
1049
1050
  agent: agentsList,
1050
1051
  connect: connectList,
1051
1052
  memory: () => (FIXTURE ? memoryList() : LIVE.memoryLive()),
@@ -1068,7 +1069,7 @@ const BODIES = {
1068
1069
  * a mode or a detail kind that is not in it is a failing test, so a new one has to say which column it opens in. */
1069
1070
  export const MODE_PLACE = {
1070
1071
  lists: { product: 'navigation', focus: 'navigation', scheduled: 'navigation', workflow: 'navigation', skills: 'navigation',
1071
- knowledge: 'navigation', portfolio: 'navigation', guide: 'navigation', validate: 'navigation', agent: 'navigation', connect: 'navigation', memory: 'navigation' },
1072
+ knowledge: 'navigation', portfolio: 'navigation', guide: 'navigation', validate: 'navigation', today: 'navigation', agent: 'navigation', connect: 'navigation', memory: 'navigation' },
1072
1073
  details: { init: 'detail', flow: 'detail', sched: 'detail', skill: 'detail', know: 'detail', agent: 'detail', mem: 'detail',
1073
1074
  prof: 'detail', intg: 'detail', addprod: 'detail', bind: 'detail', new: 'detail', none: 'detail' },
1074
1075
  };
@@ -1078,7 +1079,7 @@ export const MODE_PLACE = {
1078
1079
  export const ROW_OPENS = {
1079
1080
  product: 'detail', portfolio: 'detail', focus: 'detail',
1080
1081
  skills: 'tab', knowledge: 'tab', guide: 'tab', validate: 'tab',
1081
- scheduled: 'list', memory: 'list',
1082
+ scheduled: 'list', memory: 'list', today: 'list',
1082
1083
  };
1083
1084
  /** The column a list mode opens in: always `navigation`. `null` for a mode this table does not know. */
1084
1085
  export const placeOfMode = (mode) => MODE_PLACE.lists[mode] || null;
@@ -177,8 +177,8 @@ export const UNBACKED_MODES = ['agent', 'connect']; // mode ids (labels: agent
177
177
  /* overhaul-r4 — the PORTFOLIO mode joins "all repositories" in live mode only (the fixture drawer is the
178
178
  * prototype's, and the design comparison reads it). Its meta lives here, not in fixture.js, for the
179
179
  * same reason. */
180
- const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'], guide: ['?', 'guide'], validate: ['◉', 'validate'] });
181
- const LIVE_EXTRA = { 'all repositories': ['portfolio', 'guide', 'validate'] };
180
+ const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'], guide: ['?', 'guide'], validate: ['◉', 'validate'], today: ['☀', 'today'] });
181
+ const LIVE_EXTRA = { 'all repositories': ['today', 'portfolio', 'guide', 'validate'] };
182
182
  const DRAWER_GROUPS = SETTINGS_GROUPS.map((g) => ({
183
183
  label: g.label,
184
184
  items: (FIXTURE ? [] : LIVE_EXTRA[g.label] || []).concat(
@@ -476,17 +476,70 @@ function sheetBody(tab) {
476
476
  ]);
477
477
  }
478
478
 
479
+ /* ── file-viewer-any-text — a document that is not markdown, a page or a grid is SOURCE ──────────────────
480
+ * `previewKind` decides what counts as markdown or HTML, in the one place the server also trusts. Anything else
481
+ * the reader serves is shown as numbered monospace lines, never through the markdown renderer: JSON pretty-printed,
482
+ * JSONL as one record per line. These tabs are read-only, because /api/doc/save refuses everything but markdown and
483
+ * an edit or save button that cannot work is worse than none. If the classifier did not load, nothing here applies
484
+ * and the pane behaves as before. */
485
+ function isSourcePath(p) {
486
+ if (isReference(p)) return false; // an external reference has no file to show
487
+ const PK = typeof window !== 'undefined' ? window.PreviewKind : null;
488
+ if (!PK || typeof PK.previewKind !== 'function') return false;
489
+ const kind = PK.previewKind(String(p || ''));
490
+ if (kind === 'markdown' || kind === 'html') return false;
491
+ const e = extOfPath(p);
492
+ return !(e === 'csv' || e === 'tsv' || e === 'pdf' || e === 'xlsx');
493
+ }
494
+ const SRC_CAP = 5000; // lines drawn; the cap is STATED when it applies
495
+ const JSONL_EXT = new Set(['jsonl', 'ndjson', 'jsonlines', 'ldjson']);
496
+ function sourceBody(tab, text) {
497
+ const ext = extOfPath(tab.path);
498
+ return scrollPane(tab, 'view', 'src\u0000' + text, RAW_ST, () => {
499
+ const head = (t, bad) => el('div', {
500
+ 'data-r21': 'source-note',
501
+ style: 'padding:2px 0 8px;font-family:var(--mono);font-size:10.5px;color:' + (bad ? 'var(--destructive)' : 'var(--muted-foreground)'),
502
+ }, iv(t));
503
+ const row = (n, t, bad) => el('div', { style: 'display:flex;gap:10px;min-height:1.2em' },
504
+ el('span', { style: 'flex:none;min-width:4ch;text-align:right;color:var(--muted-foreground);user-select:none' }, iv(String(n))),
505
+ el('div', { style: 'flex:1;min-width:0;white-space:pre-wrap;overflow-wrap:anywhere' + (bad ? ';color:var(--destructive)' : '') }, iv(t)));
506
+ const lines = (arr) => arr.slice(0, SRC_CAP).map((t, i) => row(i + 1, t, false));
507
+ const capNote = (n) => (n > SRC_CAP ? [head('Showing the first ' + SRC_CAP + ' of ' + n + ' lines. The rest is not drawn. Use raw to read it all.', false)] : []);
508
+ const all = text.split('\n');
509
+ if (ext === 'json') {
510
+ let v; let err = null;
511
+ try { v = JSON.parse(text); } catch (e) { err = (e && e.message) || 'parse error'; }
512
+ if (err) return [head('JSON · read-only · NOT valid JSON: ' + err + ' · shown as written', true), ...lines(all), ...capNote(all.length)];
513
+ const pretty = JSON.stringify(v, null, 2).split('\n');
514
+ return [head('JSON · read-only · pretty-printed · ' + pretty.length + ' lines', false), ...lines(pretty), ...capNote(pretty.length)];
515
+ }
516
+ if (JSONL_EXT.has(ext)) {
517
+ const recs = []; let bad = 0; let n = 0;
518
+ all.forEach((t, i) => {
519
+ if (!t.trim()) return;
520
+ n++;
521
+ if (recs.length >= SRC_CAP) return;
522
+ try { recs.push(row(i + 1, JSON.stringify(JSON.parse(t), null, 2), false)); }
523
+ catch (e) { bad++; recs.push(row(i + 1, t + '\n⚠ line ' + (i + 1) + ' is not valid JSON: ' + ((e && e.message) || 'parse error'), true)); }
524
+ });
525
+ return [head('JSONL · read-only · ' + n + ' record' + (n === 1 ? '' : 's') + (bad ? ' · ' + bad + ' not valid JSON (marked)' : '') + (n > SRC_CAP ? ' · first ' + SRC_CAP + ' shown' : ''), bad > 0), ...recs];
526
+ }
527
+ return [head((ext || 'text') + ' · read-only · source', false), ...lines(all), ...capNote(all.length)];
528
+ });
529
+ }
530
+
479
531
  export function mountDoc(host, tab) {
480
532
  if (!FIXTURE && extOfPath(tab.path) === 'pdf') { setChildren(host, [pdfBody(tab)]); return; }
481
533
  if (!FIXTURE && extOfPath(tab.path) === 'xlsx') { setChildren(host, [sheetBody(tab)]); return; }
482
534
  const raw = docText(tab);
483
535
  const text = state.docEdits[docKey(tab)] ?? raw;
484
- const dm = (tab.readonly && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
536
+ const nonMd = !FIXTURE && isSourcePath(tab.path); // file-viewer-any-text: read-only, shown as source
537
+ const dm = ((tab.readonly || nonMd) && (state.docModes[tab.id] === 'edit' || state.docModes[tab.id] === 'diff')) ? 'view' : (state.docModes[tab.id] ?? 'view');
485
538
  const dirty = text !== raw;
486
539
  const err = saveErr.get(docKey(tab));
487
540
 
488
541
  // A file opened from the initiative's git files is evidence of what changed: it is read, never edited or saved here.
489
- const readonly = !!tab.readonly;
542
+ const readonly = !!tab.readonly || nonMd;
490
543
  const modeBtns = ['view', 'raw', 'edit', 'diff'].filter((mm) => !(readonly && (mm === 'edit' || mm === 'diff'))).map((mm) => el('button', {
491
544
  onClick: () => setState((s) => ({ docModes: { ...s.docModes, [tab.id]: mm } })),
492
545
  style: 'font-family:var(--mono);font-size:10.5px;padding:4px 10px;min-height:26px;background:'
@@ -517,7 +570,7 @@ export function mountDoc(host, tab) {
517
570
  * clean file dirty for nothing is how a tool teaches you to ignore its dirty flag. */
518
571
  const norm = typeof window !== 'undefined' ? window.MdNormalize : null;
519
572
  const canNormalize = !!(norm && typeof norm.normalize === 'function')
520
- && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab));
573
+ && !isHtmlDoc(tab.path) && !isReference(tab.path) && !unread.has(docKey(tab)) && !nonMd;
521
574
  const normBtn = canNormalize ? el('button', {
522
575
  onClick: () => {
523
576
  const r = norm.normalize(text);
@@ -566,7 +619,7 @@ export function mountDoc(host, tab) {
566
619
  if (body.defaultValue !== text) body.defaultValue = text;
567
620
  if (body.value !== text) body.value = text;
568
621
  } else if (!FIXTURE && (extOfPath(tab.path) === 'csv' || extOfPath(tab.path) === 'tsv')) body = gridBody(tab, text);
569
- else body = rendersAsPage(tab) ? htmlFrame(tab, text) : viewBody(tab, text);
622
+ else body = rendersAsPage(tab) ? htmlFrame(tab, text) : (nonMd && !unread.has(docKey(tab)) ? sourceBody(tab, text) : viewBody(tab, text));
570
623
 
571
624
  setChildren(host, [modeBar, body]);
572
625
  return host;