create-pmos 0.38.8 → 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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/README.md +37 -1
  7. package/workspace/bin/pmos.js +8 -1
  8. package/workspace/guide/index.md +40 -0
  9. package/workspace/guide/initiatives-and-next-steps.md +72 -0
  10. package/workspace/guide/layout.md +73 -0
  11. package/workspace/guide/proactive-engine.md +75 -0
  12. package/workspace/guide/scheduled-tasks-and-cli.md +41 -0
  13. package/workspace/guide/statuses.md +53 -0
  14. package/workspace/guide/validate-with-typesafe.md +80 -0
  15. package/workspace/lib/brief.js +113 -0
  16. package/workspace/lib/claims.js +605 -0
  17. package/workspace/lib/doc-profiles.js +74 -0
  18. package/workspace/lib/initiative-files.js +37 -6
  19. package/workspace/lib/judgment.js +65 -0
  20. package/workspace/lib/mobile-nav.js +1 -1
  21. package/workspace/lib/next-step-actions.js +139 -0
  22. package/workspace/lib/next-steps.js +27 -7
  23. package/workspace/lib/preview-kind.js +17 -2
  24. package/workspace/lib/proactive-cli.js +137 -0
  25. package/workspace/lib/proactive.js +208 -0
  26. package/workspace/lib/repo-watch.js +120 -0
  27. package/workspace/lib/schedule-cli.js +5 -2
  28. package/workspace/lib/secret-scan.js +28 -0
  29. package/workspace/lib/session-budget.js +35 -0
  30. package/workspace/lib/status-groups.js +53 -10
  31. package/workspace/lib/typesafe-settings.js +122 -0
  32. package/workspace/lib/typesafe.js +124 -0
  33. package/workspace/lib/validate.js +304 -0
  34. package/workspace/package-lock.json +1 -1
  35. package/workspace/package.json +3 -3
  36. package/workspace/public/pm/app.js +53 -18
  37. package/workspace/public/pm/data.js +18 -3
  38. package/workspace/public/pm/mobile.js +1 -1
  39. package/workspace/public/pm/modes-live.js +373 -0
  40. package/workspace/public/pm/modes.js +26 -0
  41. package/workspace/public/pm/next-step-ui.js +185 -0
  42. package/workspace/public/pm/panel-detail.js +47 -11
  43. package/workspace/public/pm/panel-initiatives.js +26 -4
  44. package/workspace/public/pm/panel-repos.js +2 -2
  45. package/workspace/public/pm/state.js +3 -3
  46. package/workspace/public/pm/status-banner.js +87 -0
  47. package/workspace/public/pm/tab-doc.js +66 -8
  48. package/workspace/server.js +562 -18
  49. package/workspace/skills/morning-brief.skill +43 -0
@@ -62,7 +62,7 @@ const DEST_MODE = { board: 'product', scheduled: 'scheduled', agents: 'agent', m
62
62
  /* Two modes postdate the reducer's map (`connect` and `focus` arrived with the four-panel design).
63
63
  * They are named here rather than added to `DEST_FOR_MODE`, because MOBNAV-P7 pins that map to the
64
64
  * retired app.js's MODES exactly and a mode invented for a different surface would read as drift. */
65
- const EXTRA_DEST = { connect: 'more', focus: 'board' };
65
+ const EXTRA_DEST = { connect: 'more', focus: 'board', portfolio: 'more', guide: 'more' };
66
66
 
67
67
  const destForMode = (m) => (NAV && NAV.destForMode(m)) || EXTRA_DEST[m] || null;
68
68
 
@@ -156,6 +156,23 @@ export function portfolioList() {
156
156
  return [el('div', { style: 'padding:2px 0 8px' }, kids)];
157
157
  }
158
158
 
159
+ /* ── the user guide (status-clarity R6): pages that ship with the app, opened read-only in a doc tab ───────────── */
160
+ async function openGuide(pg) {
161
+ const ini = await import('./panel-initiatives.js');
162
+ ini.openTab({ id: 'guide:' + pg.page, kind: 'doc', title: pg.title || pg.page, guide: true, readonly: true, path: pg.page, slug: null, repo: state.product || null, role: 'GUIDE' });
163
+ }
164
+ export function guideList() {
165
+ const r = read('guide', '/api/guide');
166
+ const st = status(r, 'the guide');
167
+ if (st) return [st];
168
+ const pages = (r.data && r.data.pages) || [];
169
+ if (!pages.length) return [el('div', { style: NOTE, 'data-r18': 'guide-empty' }, iv('The guide has no pages in this install.'))];
170
+ return [el('div', { style: 'padding:2px 0 8px', 'data-r18': 'guide-list' },
171
+ el('div', { style: NOTE }, iv('How this workspace works. These pages ship with the app and are updated with it.')),
172
+ pages.map((pg) => el('button', { onClick: () => openGuide(pg), style: ROW, hover: 'background:var(--accent)', 'data-r18': 'guide-page', 'data-page': pg.page },
173
+ el('span', { style: 'font-size:12.5px' }, iv(pg.title)))))];
174
+ }
175
+
159
176
  /* ── D71 — set up the recommended schedules for the selected repository, on a press ───────────── */
160
177
  let seedNote = null;
161
178
  function seedRow(set, label, hint) {
@@ -508,3 +525,359 @@ export function knowledgeLive() {
508
525
  if (!trees.length && !notes.length) kids.push(el('div', { style: NOTE }, iv('No knowledge bundle in this repository — a successful read of nothing.')));
509
526
  return [el('div', { style: 'padding:2px 0 8px' }, kids)];
510
527
  }
528
+
529
+ /* ── D79 — validate the selected repository with TypeSafe's Jev, on a press ───────────────────────────────────
530
+ * Four things on one page, in the order the PM meets them: the key (never shown again once saved), this
531
+ * repository's consent switch (off until the PM turns it on), the plan (what would be sent, before anything
532
+ * is) and the findings. A finding is advice with a confidence; nothing here edits a file, and a verdict is not
533
+ * an acceptance. The key is typed into a password field and sent once; no route returns it. */
534
+ let vNote = null; // { tone: 'ok'|'bad'|'mut', text }
535
+ let vPlan = null; // the last plan, for the repository it was made for
536
+ let vBusy = false;
537
+ const vSay = (tone, text) => { vNote = { tone, text }; setState({}); };
538
+ const vWs = () => (dataMod && state.product ? dataMod.wsOf(state.product) : null);
539
+ const TONE = { bad: 'var(--destructive)', ok: 'var(--foreground)', mut: 'var(--muted-foreground)' };
540
+
541
+ async function vSaveKey(input) {
542
+ const key = input && input.value ? input.value : '';
543
+ if (!key.trim()) { vSay('bad', 'type or paste the key first'); return; }
544
+ const out = await fetch('/api/typesafe/key', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) }).then(readJson).catch((e) => ({ ok: false, error: e.message }));
545
+ if (input) input.value = '';
546
+ reload('typesafe'); reload('vrepo');
547
+ vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? 'the key is saved on this machine and is not shown again' : ((out && out.error) || 'the key was not saved'));
548
+ }
549
+ async function vClearKey() {
550
+ const out = await fetch('/api/typesafe/key', { method: 'DELETE' }).then(readJson).catch((e) => ({ ok: false, error: e.message }));
551
+ reload('typesafe'); reload('vrepo'); vPlan = null;
552
+ vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? 'the key is removed; nothing will be sent' : ((out && out.error) || 'the key was not removed'));
553
+ }
554
+ async function vConsent(on) {
555
+ const ws = vWs(); if (!ws) return vSay('bad', 'select a repository first');
556
+ const out = await post('/api/validate/repo', { ws, consent: on });
557
+ reload('vrepo'); vPlan = null;
558
+ vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? (on ? 'validation is on for this repository' : 'validation is off; nothing from this repository will be sent') : ((out && out.error) || 'not saved'));
559
+ }
560
+ async function vExclude(text) {
561
+ const ws = vWs(); if (!ws) return;
562
+ const exclude = String(text || '').split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
563
+ const out = await post('/api/validate/repo', { ws, exclude });
564
+ reload('vrepo'); vPlan = null;
565
+ vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? 'excluded paths saved (' + exclude.length + ')' : ((out && out.error) || 'not saved'));
566
+ }
567
+ async function vMakePlan() {
568
+ const ws = vWs(); if (!ws) return vSay('bad', 'select a repository first');
569
+ vBusy = true; vSay('mut', 'working out what would be sent …');
570
+ const out = await post('/api/validate/plan', { ws });
571
+ vBusy = false;
572
+ if (!out || out.ok === false) { vPlan = null; return vSay('bad', (out && out.error) || 'no plan'); }
573
+ vPlan = { ws, plan: out.plan }; vSay('mut', 'review the plan, then confirm to send');
574
+ }
575
+ async function vRun() {
576
+ if (!vPlan) return;
577
+ vBusy = true; vSay('mut', 'validating … this sends text to ' + vPlan.plan.host);
578
+ const out = await post('/api/validate/run', { ws: vPlan.ws, expect_requests: vPlan.plan.requests });
579
+ vBusy = false; vPlan = null; reload('vfind');
580
+ if (!out || out.ok === false) return vSay('bad', (out && out.error) || 'the run failed');
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
+ }
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
+
660
+ export function validateList() {
661
+ const kids = [];
662
+ const k = read('typesafe', '/api/typesafe');
663
+ const ws = vWs();
664
+ const rp = ws ? read('vrepo', '/api/validate/repo?ws=' + encodeURIComponent(ws)) : null;
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')));
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.')));
669
+
670
+ // 1 · the key
671
+ kids.push(el('div', { style: LABEL }, iv('1 · TypeSafe key')));
672
+ const st = status(k, 'the key status');
673
+ if (st) kids.push(st);
674
+ else {
675
+ const present = k.data && k.data.key && k.data.key.present;
676
+ const src = k.data && k.data.key && k.data.key.source;
677
+ kids.push(el('div', { style: NOTE, 'data-r19': 'key-status' }, iv(present
678
+ ? 'A key is set (' + (src === 'environment' ? 'from the environment' : 'saved on this machine') + '). It is never shown again and is never given to a terminal.'
679
+ : 'No key is set. Without a key nothing is sent.')));
680
+ const input = el('input', { type: 'password', autocomplete: 'off', spellcheck: 'false', placeholder: present ? 'replace the key' : 'paste your TypeSafe key', 'data-r19': 'key-input',
681
+ style: 'flex:1;min-width:0;font-family:var(--mono);font-size:11px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' });
682
+ kids.push(el('div', { style: 'display:flex;gap:4px;padding:0 14px 6px' }, input,
683
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r19': 'key-save', onClick: () => vSaveKey(input) }, 'save'),
684
+ present && src === 'settings' ? el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r19': 'key-remove', onClick: vClearKey }, 'remove') : null));
685
+ }
686
+
687
+ // 2 · this repository
688
+ kids.push(el('div', { style: LABEL }, iv('2 · this repository' + (state.product ? ' · ' + state.product : ''))));
689
+ if (!ws) kids.push(el('div', { style: NOTE }, iv('Select a repository first.')));
690
+ else {
691
+ const rs = status(rp, 'this repository\'s validation setting');
692
+ if (rs) kids.push(rs);
693
+ else {
694
+ const on = !!(rp.data && rp.data.consent);
695
+ kids.push(el('div', { style: 'display:flex;gap:8px;align-items:center;padding:4px 14px' },
696
+ el('span', { style: 'font-size:12px', 'data-r19': 'consent-state' }, iv(on ? 'validation is ON for this repository' : 'validation is OFF — nothing from this repository is sent')),
697
+ el('button', { style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', 'data-r19': 'consent-toggle', onClick: () => vConsent(!on) }, on ? 'turn off' : 'turn on')));
698
+ const ta = el('textarea', { rows: 2, placeholder: 'paths never sent, one per line (e.g. private/ or notes/*.md)', 'data-r19': 'exclude-input',
699
+ style: 'width:calc(100% - 28px);margin:4px 14px 0;font-family:var(--mono);font-size:10.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, (rp.data.exclude || []).join('\n'));
700
+ kids.push(ta, el('div', { style: 'padding:4px 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r19': 'exclude-save', onClick: () => vExclude(ta.value) }, 'save excluded paths')));
701
+ }
702
+ }
703
+
704
+ // 3 · plan and run
705
+ kids.push(el('div', { style: LABEL }, iv('3 · validate')));
706
+ kids.push(el('div', { style: 'padding:4px 14px 6px;display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
707
+ el('button', { style: BTN, hover: 'color:var(--foreground)', disabled: vBusy ? 'true' : null, 'data-r19': 'plan', onClick: vMakePlan }, 'show what would be sent'),
708
+ vPlan && vPlan.ws === ws ? el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', disabled: vBusy ? 'true' : null, 'data-r19': 'run', onClick: vRun }, 'confirm and send ' + vPlan.plan.requests + ' request(s)') : null));
709
+ if (vPlan && vPlan.ws === ws) {
710
+ const p = vPlan.plan;
711
+ kids.push(el('div', { style: NOTE, 'data-r19': 'plan-text' },
712
+ iv(p.requests + ' request(s), about ' + p.characters.toLocaleString() + ' characters, to ' + p.host + '. ' + p.eligible + ' of ' + p.documents + ' documents eligible · ' + p.excluded + ' excluded · ' + p.local_findings + ' local finding(s) that send nothing.')));
713
+ if (p.withheld.length) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r19': 'withheld' }, iv('Withheld, matched a credential pattern, will NOT be sent: ' + p.withheld.map((w) => w.file + ' (' + w.rules.join(', ') + ')').join('; '))));
714
+ if (p.too_large.length) kids.push(el('div', { style: NOTE }, iv('Too large to send: ' + p.too_large.join(', '))));
715
+ }
716
+ if (vNote) kids.push(el('div', { 'data-r19': 'note', style: NOTE + ';color:' + (TONE[vNote.tone] || TONE.mut) }, iv(vNote.text)));
717
+
718
+ // 4 · findings
719
+ const fs0 = fd ? status(fd, 'the findings') : null;
720
+ const run = fd && fd.data ? fd.data.run : null;
721
+ kids.push(el('div', { style: LABEL }, iv('4 · findings' + (run ? ' · last run ' + String(run.at).replace('T', ' ').slice(0, 16) + ' · model ' + (run.model || 'none') : ''))));
722
+ if (fs0) kids.push(fs0);
723
+ else if (!run) kids.push(el('div', { style: NOTE, 'data-r19': 'no-run' }, iv('No validation has run for this repository.')));
724
+ else {
725
+ kids.push(el('div', { style: NOTE, 'data-r19': 'coverage' }, iv('Coverage: ' + run.asked + ' of ' + run.requests + ' request(s) answered · ' + run.withheld + ' withheld · ' + run.too_large + ' too large · ' + run.excluded + ' excluded · ' + run.failed + ' failed. A short list is not a clean bill: read this line first.')));
726
+ const list = fd.data.findings || [];
727
+ if (!list.length) kids.push(el('div', { style: NOTE }, iv('The run raised nothing.')));
728
+ for (const f of list) {
729
+ const unsure = f.label === 'for-your-judgement';
730
+ kids.push(el('button', { style: ROW, hover: 'background:var(--accent)', 'data-r19': 'finding', 'data-rule': f.rule, title: f.question || f.detail || '',
731
+ onClick: () => openDocIn(ws, f.file, state.product) },
732
+ el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
733
+ el('span', { style: 'font-family:var(--mono);font-size:10px;color:' + (f.severity === 'high' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(f.severity)),
734
+ el('span', { style: 'font-size:12px' }, iv(f.rule.replace(/-/g, ' '))),
735
+ el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(f.kind === 'judged' ? (unsure ? 'for your judgement · ' : '') + 'confidence ' + (f.confidence == null ? '—' : f.confidence.toFixed(2)) : f.sent ? 'sent' : 'local'))),
736
+ el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(f.files.join(' ↔ ') + (f.detail ? ' — ' + f.detail : '')))));
737
+ }
738
+ }
739
+ return [el('div', { style: 'padding:2px 0 8px', 'data-r19': 'validate' }, kids)];
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
+ }
@@ -1044,6 +1044,9 @@ const LISTS = {
1044
1044
  skills: () => (FIXTURE ? skillsList() : LIVE.skillsLive()),
1045
1045
  knowledge: () => (FIXTURE ? knowledgeList() : LIVE.knowledgeLive()),
1046
1046
  portfolio: () => (FIXTURE ? [] : LIVE.portfolioList()),
1047
+ guide: () => (FIXTURE ? [] : LIVE.guideList()),
1048
+ validate: () => (FIXTURE ? [] : LIVE.validateList()),
1049
+ today: () => (FIXTURE ? [] : LIVE.todayList()),
1047
1050
  agent: agentsList,
1048
1051
  connect: connectList,
1049
1052
  memory: () => (FIXTURE ? memoryList() : LIVE.memoryLive()),
@@ -1060,6 +1063,29 @@ const BODIES = {
1060
1063
  intg: intgBody,
1061
1064
  };
1062
1065
 
1066
+ /* WHERE EACH THING OPENS (column-layout). A list or a directory (memory, skills, knowledge, scheduled tasks, focus,
1067
+ * portfolio, the guide, the initiatives themselves) is drawn in the navigation column, under the repository list. A
1068
+ * detail (an initiative, a skill, a schedule, a memory item) is drawn in the detail column. This table is the rule;
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. */
1070
+ export const MODE_PLACE = {
1071
+ lists: { product: 'navigation', focus: 'navigation', scheduled: 'navigation', workflow: 'navigation', skills: 'navigation',
1072
+ knowledge: 'navigation', portfolio: 'navigation', guide: 'navigation', validate: 'navigation', today: 'navigation', agent: 'navigation', connect: 'navigation', memory: 'navigation' },
1073
+ details: { init: 'detail', flow: 'detail', sched: 'detail', skill: 'detail', know: 'detail', agent: 'detail', mem: 'detail',
1074
+ prof: 'detail', intg: 'detail', addprod: 'detail', bind: 'detail', new: 'detail', none: 'detail' },
1075
+ };
1076
+ /* WHAT A ROW IN EACH LIST OPENS, as the live app does it today. An initiative (from the board, the portfolio or the focus
1077
+ * queue) opens in the detail column. A file-like row (a skill, a knowledge note, a guide page) opens in a TAB, like any
1078
+ * other document, so it can sit beside a terminal. A schedule and a memory entry are acted on inside the list itself. */
1079
+ export const ROW_OPENS = {
1080
+ product: 'detail', portfolio: 'detail', focus: 'detail',
1081
+ skills: 'tab', knowledge: 'tab', guide: 'tab', validate: 'tab',
1082
+ scheduled: 'list', memory: 'list', today: 'list',
1083
+ };
1084
+ /** The column a list mode opens in: always `navigation`. `null` for a mode this table does not know. */
1085
+ export const placeOfMode = (mode) => MODE_PLACE.lists[mode] || null;
1086
+ /** The column a detail kind opens in: always `detail`. `null` for a kind this table does not know. */
1087
+ export const placeOfDetail = (kind) => MODE_PLACE.details[kind] || null;
1088
+
1063
1089
  /** Is `mode` one this module renders? `product` belongs to panel-initiatives.js. */
1064
1090
  export function hasMode(mode) {
1065
1091
  return Object.prototype.hasOwnProperty.call(LISTS, mode);