create-pmos 0.47.0 → 0.48.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 (71) hide show
  1. package/package.json +1 -1
  2. package/payload/AGENTS-HUMAN.md +1 -1
  3. package/payload/AGENTS.md +1 -1
  4. package/payload/KIT-VERSION +1 -1
  5. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  6. package/payload/mods/pmos-cockpit/README.md +4 -2
  7. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
  8. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
  9. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
  10. package/payload/mods/pmos-cockpit/hooks/register.tsx +37 -18
  11. package/payload/planning/evals/eval-calibration.md +1 -1
  12. package/payload/planning/okrs/health-budgets.md +1 -1
  13. package/payload/scripts/pmos-log.sh +18 -8
  14. package/payload/scripts/pmos-model.js +233 -2
  15. package/payload/scripts/status-facts.js +204 -0
  16. package/payload/scripts/workflow-state.js +52 -0
  17. package/payload/state/discoveries.md +1 -1
  18. package/workspace/README.md +2 -2
  19. package/workspace/bin/live-notes.js +133 -0
  20. package/workspace/design-parity.js +328 -21
  21. package/workspace/doc-category-bench.js +165 -0
  22. package/workspace/fixtures/doc-category-labelled.json +167 -0
  23. package/workspace/guide/initiatives-and-next-steps.md +15 -2
  24. package/workspace/guide/knowledge-graph.md +83 -1
  25. package/workspace/guide/layout.md +29 -2
  26. package/workspace/guide/proactive-engine.md +6 -2
  27. package/workspace/guide/validate-with-typesafe.md +16 -1
  28. package/workspace/lib/claims.js +51 -2
  29. package/workspace/lib/doc-category-corrections.js +113 -0
  30. package/workspace/lib/doc-category-writeback.js +110 -0
  31. package/workspace/lib/doc-category.js +225 -0
  32. package/workspace/lib/engine/pmos-model.js +233 -2
  33. package/workspace/lib/engine/status-facts.js +204 -0
  34. package/workspace/lib/engine/workflow-state.js +52 -0
  35. package/workspace/lib/jev-categorize.js +399 -0
  36. package/workspace/lib/jev-claims.js +59 -15
  37. package/workspace/lib/jev-route.js +129 -0
  38. package/workspace/lib/kg-cli.js +3 -1
  39. package/workspace/lib/kg.js +6 -1
  40. package/workspace/lib/learn.js +5 -3
  41. package/workspace/lib/live-notes.js +890 -0
  42. package/workspace/lib/typesafe-settings.js +10 -2
  43. package/workspace/package-lock.json +1 -1
  44. package/workspace/package.json +5 -4
  45. package/workspace/public/pm/app.js +148 -79
  46. package/workspace/public/pm/data.js +58 -0
  47. package/workspace/public/pm/fixture.js +40 -2
  48. package/workspace/public/pm/modes-live.js +216 -18
  49. package/workspace/public/pm/panel-cockpit.js +636 -0
  50. package/workspace/public/pm/panel-detail.js +210 -8
  51. package/workspace/public/pm/panel-initiatives.js +9 -9
  52. package/workspace/public/pm/panel-newsess.js +145 -86
  53. package/workspace/public/pm/panel-repos.js +34 -17
  54. package/workspace/public/pm/panel-tabs.js +145 -27
  55. package/workspace/public/pm/state.js +45 -4
  56. package/workspace/public/pm/tab-doc.js +244 -16
  57. package/workspace/public/pm/tab-term.js +11 -1
  58. package/workspace/public/pm/tokens.css +4 -3
  59. package/workspace/public/pm/view-addrepo.js +207 -0
  60. package/workspace/public/pm/view-files.js +197 -0
  61. package/workspace/public/pm/view-guide.js +141 -0
  62. package/workspace/public/pm/view-knowledge.js +225 -0
  63. package/workspace/public/pm/view-memory.js +122 -0
  64. package/workspace/public/pm/view-portfolio.js +153 -0
  65. package/workspace/public/pm/view-reports.js +190 -0
  66. package/workspace/public/pm/view-scheduled.js +161 -0
  67. package/workspace/public/pm/view-skills.js +216 -0
  68. package/workspace/public/pm/view-today.js +168 -0
  69. package/workspace/public/pm/view-validate.js +171 -0
  70. package/workspace/server.js +290 -7
  71. package/workspace/skills/live-notes.skill +58 -0
@@ -23,23 +23,36 @@ const ROW = 'display:block;width:100%;padding:7px 14px;min-height:40px;text-alig
23
23
  const BTN = 'font-family:var(--mono);font-size:10.5px;padding:3px 9px;border:1px solid var(--border);color:var(--muted-foreground);min-height:26px';
24
24
  const NOTE = 'padding:8px 14px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5';
25
25
 
26
- /* one cached read per endpoint; `reload(key)` drops it so the next render refetches */
26
+ /* one cached read per endpoint; `reload(key)` drops it so the next render refetches.
27
+ *
28
+ * AN ANSWER OLDER THAN A RELOAD IS DROPPED. `reload` follows a write (a saved setting, a dismissed
29
+ * item). A read that was already in flight when the write went out carries the state from BEFORE it,
30
+ * and it used to land after the fresh read and overwrite it — then count as fresh for 15 s. Measured
31
+ * on Today: quiet hours saved, the note shown, and 1.5 s later the note gone while the server held
32
+ * the setting (pm-r22-test.js, about 1 run in 8). `gen` counts reloads per key; a response whose
33
+ * generation is out of date is not written. */
27
34
  const cache = new Map();
35
+ const gen = new Map();
28
36
  function read(key, url) {
29
37
  const hit = cache.get(key);
30
38
  if (hit && (hit.state !== 'ok' || Date.now() - hit.at < 15000)) return hit;
31
39
  if (!hit || hit.state === 'ok') {
32
40
  const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, at: hit ? hit.at : 0 };
33
41
  cache.set(key, next);
42
+ const g = gen.get(key) || 0;
43
+ const current = () => (gen.get(key) || 0) === g;
34
44
  fetch(url).then(readJson).then(
35
- (data) => { cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
36
- (e) => { cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
45
+ (data) => { if (!current()) return; cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
46
+ (e) => { if (!current()) return; cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
37
47
  );
38
48
  return next;
39
49
  }
40
50
  return hit;
41
51
  }
42
- const reload = (key) => cache.delete(key);
52
+ const reload = (key) => { gen.set(key, (gen.get(key) || 0) + 1); cache.delete(key); };
53
+ /* refetch on the next render but keep showing the last answer meanwhile, so a list does not blank while it updates.
54
+ * It follows a write too, so it bumps the generation like `reload`: a read already in flight is older than the write. */
55
+ const refresh = (key) => { gen.set(key, (gen.get(key) || 0) + 1); const h = cache.get(key); if (h && h.state === 'ok') h.at = 0; else cache.delete(key); };
43
56
  /* A route this server does not have answers 404 with the plain text `not found`. Parsing that as JSON
44
57
  * showed the PM "Unexpected token 'o', \"not found\" is not valid JSON" — true, and no help. The usual
45
58
  * cause is a server still running code older than the page, so say that and the one command. */
@@ -275,7 +288,8 @@ async function memDo(body) {
275
288
  const out = await post('/api/memory', { ws, ...body });
276
289
  memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
277
290
  if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
278
- reload('memory'); setState({});
291
+ // the list caches its read per workspace, so the reload names that key; a bare 'memory' left the old list on screen for 15s
292
+ reload('memory:' + ws); reload('memory'); setState({});
279
293
  }
280
294
  const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
281
295
  let dataMod = null;
@@ -494,10 +508,147 @@ export function skillDetailLive(det) {
494
508
 
495
509
  /* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
496
510
  function flatten(node, out, depth) {
497
- for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
511
+ for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth, category: d.category || null, basis: d.category_basis || null, why: d.category_why || null, kind: d.category_kind || null, disagrees: d.category_disagrees || [] });
498
512
  if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
499
513
  return out;
500
514
  }
515
+ /* jev-doc-categorizer Run 1 — each document's category, derived locally beside its own `type`, with the basis that decided it.
516
+ * `undetermined` is its own value: the file has no evidence, and it is shown as that, never as `other`. A press on a category
517
+ * narrows the list; a second press on it shows everything again. Nothing is written. */
518
+ let knCat = '';
519
+ const catTitle = (it) => it.category ? 'category ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis ? ' · decided by ' + it.basis : ' · no evidence') + (it.why ? ' (' + it.why + ')' : '')
520
+ + (it.disagrees && it.disagrees.length ? ' · ' + it.disagrees.map((d) => d.basis + ' says ' + d.category).join(', ') : '') + ' · advisory' : '';
521
+ const catChip = (it) => it.category ? el('span', { 'data-cat': it.category, 'data-cat-basis': it.basis || '', title: catTitle(it), style: 'margin-left:8px;font-size:9.5px;letter-spacing:.06em;color:var(--muted-foreground)' }, iv('· ' + it.category + (it.basis === 'jev' ? ' (jev)' : ''))) : null;
522
+ function categoryBar(trees) {
523
+ const counts = {};
524
+ for (const t of trees) for (const it of flatten(t, [], 0)) if (it.path && it.category) counts[it.category] = (counts[it.category] || 0) + 1;
525
+ const names = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
526
+ if (!names.length) return null;
527
+ return el('div', { 'data-cat-bar': '1', style: 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 14px 6px' },
528
+ names.map((n) => el('button', { 'data-cat-filter': n, 'aria-pressed': knCat === n ? 'true' : 'false', title: 'show only ' + n + ' documents', onClick: () => { knCat = knCat === n ? '' : n; setState({}); },
529
+ style: 'font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);' + (knCat === n ? 'background:var(--accent);' : '') }, iv(n + ' ' + counts[n]))));
530
+ }
531
+ /* D82 — Jev on documents the local rules leave undetermined. The switch is off until the PM turns it on; the first run shows
532
+ * its request count and sends only after the PM confirms that number; afterwards opening this list sends only new or changed
533
+ * files. Jev's categories are marked "(jev)" and are advisory. */
534
+ let dPlan = null; // the plan the PM is looking at: { ws, plan }
535
+ let dNote = null;
536
+ const dSay = (text) => { dNote = text; setState({}); };
537
+ async function dSwitch(ws, on) {
538
+ const out = await post('/api/doccat/jev', { ws, categorize: on });
539
+ reload('library:' + ws); dPlan = null;
540
+ dSay(out && out.ok ? (on ? 'categorizing with Jev is on — review the first run’s count, then confirm it; nothing is sent before that' : 'categorizing with Jev is off') : ((out && out.error) || 'not saved'));
541
+ }
542
+ async function dResume(ws) {
543
+ const out = await post('/api/doccat/jev/resume', { ws });
544
+ reload('library:' + ws);
545
+ dSay(out && out.ok ? 'categorizing with Jev resumed' : ((out && out.error) || 'not resumed'));
546
+ }
547
+ async function dPlanFor(ws) {
548
+ const out = await post('/api/doccat/jev/run', { ws });
549
+ if (!out || out.ok === false) { dPlan = null; return dSay((out && out.error) || 'no plan'); }
550
+ dPlan = { ws, plan: out.plan }; dSay('review the count, then confirm to send');
551
+ }
552
+ async function dRun() {
553
+ if (!dPlan) return;
554
+ const { ws, plan } = dPlan;
555
+ dSay('sending ' + plan.requests + ' request(s) to ' + plan.host + ' …');
556
+ const out = await post('/api/doccat/jev/run', { ws, confirm: true, expect_requests: plan.requests, expect_fingerprint: plan.fingerprint });
557
+ dPlan = null; reload('library:' + ws);
558
+ dSay(!out || out.ok === false ? ((out && out.error) || 'the run failed') : 'asked ' + out.sent + ' · ' + out.decided + ' categorized · ' + out.unsure + ' unsure · ' + (out.unreadable || 0) + ' unreadable · ' + out.cached + ' answered from the cache · ' + out.failed + ' failed');
559
+ }
560
+ function doccatBlock(ws, j) {
561
+ if (!ws || !j) return [];
562
+ const kids = [];
563
+ kids.push(el('div', { 'data-doccat': 'explain', style: NOTE }, iv('Files with no local evidence are “undetermined”. Jev can be asked to categorize them from a short snippet: the title, the headings and the opening, never the whole file. Its answers are marked (jev), are advisory, and an unsure answer stays undetermined.')));
564
+ kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
565
+ el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-doccat': 'switch', onClick: () => dSwitch(ws, !j.switch) },
566
+ iv(j.switch ? 'categorize with Jev: ON — turn off' : 'categorize with Jev: OFF — turn on')),
567
+ el('button', { style: BTN, hover: 'color:var(--foreground)', disabled: j.open ? null : 'true', 'data-doccat': 'plan',
568
+ title: 'Shows how many requests the undetermined files would take, and sends only after you confirm that number.', onClick: () => dPlanFor(ws) }, 'ask Jev about undetermined files'),
569
+ dPlan && dPlan.ws === ws ? el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-doccat': 'run', onClick: dRun }, 'confirm and send ' + dPlan.plan.requests + ' request(s)') : null));
570
+ kids.push(el('div', { 'data-doccat': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
571
+ iv(j.open ? (j.confirmed ? 'Active: new or changed undetermined files are sent when the workspace reads this repository’s documents (when it starts, and when this list opens), at most once a minute.' : 'Ready: nothing is sent until you confirm the first run’s count.') : 'Not active: ' + j.sentence + ' All three must hold before anything is sent.')));
572
+ if (j.paused) kids.push(el('div', { 'data-doccat': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
573
+ el('span', null, iv(j.paused.reason)),
574
+ el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'resume', onClick: () => dResume(ws) }, 'resume')));
575
+ const l = j.last;
576
+ if (l && l.waiting) kids.push(el('div', { 'data-doccat': 'waiting', style: NOTE + ';color:var(--foreground)' }, iv(l.waiting + ' new or changed file(s) are waiting: more than an automatic pass sends. Ask Jev and confirm the count to send them.')));
577
+ else if (l) kids.push(el('div', { 'data-doccat': 'last', style: NOTE }, iv('Last Jev pass ' + String(l.at).replace('T', ' ').slice(0, 16) + ': sent ' + l.sent + ' · categorized ' + l.decided + ' · unsure ' + l.unsure + ' · unreadable ' + (l.unreadable || 0) + ' · withheld ' + (l.withheld_count || 0) + ' · excluded ' + l.excluded + ' · failed ' + l.failed + '.')));
578
+ if (dPlan && dPlan.ws === ws) {
579
+ const p = dPlan.plan;
580
+ kids.push(el('div', { 'data-doccat': 'plan-text', style: NOTE }, iv(p.requests + ' request(s), about ' + p.characters.toLocaleString() + ' characters, to ' + p.host + ' · ' + p.candidates + ' undetermined file(s) · ' + p.cached + ' already answered · ' + p.excluded + ' excluded · ' + p.withheld_count + ' withheld by the credential scan.')));
581
+ }
582
+ if (dNote) kids.push(el('div', { 'data-doccat': 'note', style: NOTE }, iv(dNote)));
583
+ return kids;
584
+ }
585
+ /* jev-doc-categorizer Run 3 (C7) — the PM corrects a category. `fix` on a row opens a small control: a category, a ticket kind for
586
+ * a ticket, `save correction` and `clear correction`. A correction is kept per user, outside the repository, and never edits the
587
+ * file. `write it into the file` is a separate press: it shows the one-line diff first, and `apply` writes it only if the file is
588
+ * still the text the diff was made from (the server compares the sha1 and answers 409 when it is not). The server checks every
589
+ * value again; these two lists only fill the menus. */
590
+ const CATS = ['prd', 'meeting-notes', 'report', 'ticket', 'knowledge', 'spec-artifact', 'other'];
591
+ const KINDS = ['design', 'engineering', 'other'];
592
+ let knFix = null; // { path, category, ticket_kind } — the row whose control is open
593
+ let knPlan = null; // { path, category, ticket_kind, diff, sha, unchanged } — the write-back diff on screen
594
+ let knNote = null; // { path, tone, text }
595
+ let knBusy = false;
596
+ const FIX = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;margin-right:10px;border:1px solid var(--border);color:var(--muted-foreground);flex:none';
597
+ const SEL = 'font-family:var(--mono);font-size:10.5px;padding:2px 4px;border:1px solid var(--border);background:var(--background);color:var(--foreground)';
598
+ const knSay = (path, tone, text) => { knNote = { path, tone, text }; knBusy = false; setState({}); };
599
+ function knOpen(it) {
600
+ if (knFix && knFix.path === it.path) { knFix = null; knPlan = null; knNote = null; setState({}); return; }
601
+ const cat = CATS.includes(it.category) ? it.category : 'other';
602
+ knFix = { path: it.path, category: cat, ticket_kind: cat === 'ticket' ? (it.kind || 'other') : null };
603
+ knPlan = null; knNote = null; setState({});
604
+ }
605
+ async function knSave(ws, clear) {
606
+ if (!knFix || knBusy) return;
607
+ knBusy = true; setState({});
608
+ const body = clear ? { ws, path: knFix.path, clear: true } : { ws, path: knFix.path, category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
609
+ const res = await post('/api/doccat/correct', body);
610
+ refresh('library:' + ws);
611
+ if (res && res.ok) knSay(knFix.path, 'ok', clear ? (res.cleared ? 'correction cleared — the rules decide this file again' : 'this file had no correction') : 'saved — your correction wins over every rule. The file is not changed.');
612
+ else knSay(knFix.path, 'bad', (res && res.error) || 'not saved');
613
+ }
614
+ async function knWriteback(ws, confirm) {
615
+ if (!knFix || knBusy) return;
616
+ knBusy = true; setState({});
617
+ const want = { category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
618
+ if (!confirm) {
619
+ const res = await post('/api/doccat/writeback', { ws, path: knFix.path, ...want });
620
+ if (res && res.ok) { knPlan = { path: knFix.path, ...want, diff: res.diff, sha: res.sha, next_sha: res.next_sha, unchanged: !!res.unchanged }; knSay(knFix.path, 'mut', res.unchanged ? 'the file already says this — nothing to write' : 'this is the change. Press apply to write it into the file.'); }
621
+ else { knPlan = null; knSay(knFix.path, 'bad', (res && res.error) || 'no diff'); }
622
+ return;
623
+ }
624
+ if (!knPlan) { knBusy = false; return; }
625
+ const res = await post('/api/doccat/writeback', { ws, path: knPlan.path, category: knPlan.category, ticket_kind: knPlan.ticket_kind, confirm: true, expect_sha: knPlan.sha, expect_next_sha: knPlan.next_sha });
626
+ knPlan = null;
627
+ refresh('library:' + ws);
628
+ if (res && res.ok) knSay(knFix.path, 'ok', res.written ? 'written into the file\'s frontmatter' : 'the file already says this — nothing was written');
629
+ else knSay(knFix.path, 'bad', (res && res.error) || 'not written');
630
+ }
631
+ function knPanel(ws, it) {
632
+ const f = knFix;
633
+ const pad = 'padding:2px 14px 8px ' + (20 + it.depth * 12) + 'px';
634
+ const btn = (attr, label, fn, disabled) => el('button', { [attr]: '1', disabled: knBusy || disabled, onClick: fn, style: BTN, hover: 'color:var(--foreground)' }, label);
635
+ const kids = [
636
+ el('select', { 'data-cat-select': '1', 'aria-label': 'category', style: SEL, onChange: (e) => { f.category = e.target.value; f.ticket_kind = f.category === 'ticket' ? (f.ticket_kind || 'other') : null; knPlan = null; setState({}); } },
637
+ CATS.map((c) => el('option', { value: c, selected: c === f.category }, c))),
638
+ f.category === 'ticket' ? el('select', { 'data-cat-kind': '1', 'aria-label': 'ticket kind', style: SEL, onChange: (e) => { f.ticket_kind = e.target.value; knPlan = null; setState({}); } },
639
+ KINDS.map((k) => el('option', { value: k, selected: k === f.ticket_kind }, k))) : null,
640
+ btn('data-cat-save', 'save correction', () => knSave(ws, false)),
641
+ it.basis === 'pm correction' ? btn('data-cat-clear', 'clear correction', () => knSave(ws, true)) : null,
642
+ btn('data-cat-writeback', 'write it into the file', () => knWriteback(ws, false)),
643
+ ];
644
+ const out = [el('div', { 'data-cat-panel': it.path, style: pad + ';display:flex;flex-wrap:wrap;gap:6px;align-items:center' }, kids)];
645
+ if (knPlan && knPlan.path === it.path && !knPlan.unchanged) {
646
+ out.push(el('pre', { 'data-cat-diff': '1', style: pad.replace('padding:2px', 'margin:0;padding:6px') + ';font-family:var(--mono);font-size:10.5px;white-space:pre-wrap;color:var(--foreground);background:var(--accent)' }, iv(knPlan.diff)));
647
+ out.push(el('div', { style: pad + ';display:flex;gap:6px' }, btn('data-cat-apply', 'apply', () => knWriteback(ws, true)), btn('data-cat-cancel', 'cancel', () => { knPlan = null; knNote = null; setState({}); })));
648
+ }
649
+ if (knNote && knNote.path === it.path) out.push(el('div', { 'data-cat-note': knNote.tone, style: pad + ';font-size:11px;color:' + TONE[knNote.tone] }, iv(knNote.text)));
650
+ return out;
651
+ }
501
652
  export function knowledgeLive() {
502
653
  const ws = state.wsId || '';
503
654
  const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
@@ -505,11 +656,18 @@ export function knowledgeLive() {
505
656
  if (st) return [st];
506
657
  const trees = (r.data && r.data.knowledge) || [];
507
658
  const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
659
+ const bar = categoryBar(trees); if (bar) kids.push(bar);
660
+ kids.push(...doccatBlock(ws, r.data && r.data.doccat));
508
661
  for (const t of trees) {
509
662
  kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
510
663
  for (const it of flatten(t, [], 0)) {
664
+ if (knCat && (it.dir || it.category !== knCat)) continue;
511
665
  if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
512
- kids.push(el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;width:100%;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name)));
666
+ const open = !!(knFix && knFix.path === it.path);
667
+ kids.push(el('div', { 'data-cat-row': it.path, style: 'display:flex;align-items:center' },
668
+ el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;flex:1 1 auto;min-width:0;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name), catChip(it)),
669
+ el('button', { 'data-cat-fix': it.path, 'aria-expanded': open ? 'true' : 'false', title: 'correct the category of this document', onClick: () => knOpen(it), style: FIX + (open ? ';background:var(--accent);color:var(--foreground)' : ''), hover: 'color:var(--foreground)' }, 'fix')));
670
+ if (open) kids.push(...knPanel(ws, it));
513
671
  }
514
672
  }
515
673
  // R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
@@ -614,6 +772,12 @@ async function jSwitch(on) {
614
772
  reload('claims'); jPlan = null;
615
773
  jSay(out && out.ok ? (on ? 'checking with Jev automatically is on — the first check records what exists and sends nothing; after that only changed pairs are sent' : 'checking with Jev automatically is off') : ((out && out.error) || 'not saved'));
616
774
  }
775
+ async function jStatus(on) {
776
+ const ws = vWs(); if (!ws) return;
777
+ const out = await post('/api/claims/jev/status', { ws, status_read: on });
778
+ reload('claims'); jPlan = null;
779
+ jSay(out && out.ok ? (on ? 'reading documents for status is on — the next check records the documents that exist and sends nothing for them; after that only changed documents are sent' : 'reading documents for status is off') : ((out && out.error) || 'not saved'));
780
+ }
617
781
  async function jResume() {
618
782
  const ws = vWs(); if (!ws) return;
619
783
  const out = await post('/api/claims/jev/resume', { ws });
@@ -648,7 +812,11 @@ function jevBlock(ws, d) {
648
812
  kids.push(el('div', { 'data-r24': 'explain', style: NOTE }, iv('Where the local checks cannot decide, a pair of short texts (a requirement and the rubric’s dimensions, a story section and the requirement it cites, a sentence and the decision it cites, two sections of one PRD) can be put to Jev. Only pairs that changed are sent. Jev’s answers are suspicions: advisory, shown in words, and never counted as problems.')));
649
813
  kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
650
814
  el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'switch', onClick: () => jSwitch(!j.switch) },
651
- iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on'))));
815
+ iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on')),
816
+ el('button', { style: BTN + (j.status_read ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'status-switch',
817
+ title: 'Also send the documents tied to each initiative (its PRD, rubric and brief, and notes that name it), to ask whether a document is a stub and whether a note contradicts a requirement. The answers are suggestions in the initiative detail. Needs the switch on the left.',
818
+ onClick: () => jStatus(!j.status_read) },
819
+ iv(j.status_read ? 'read documents for status: ON — turn off' : 'read documents for status: OFF — turn on'))));
652
820
  kids.push(el('div', { 'data-r24': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
653
821
  iv(j.open ? 'Active: a key is saved, validation is on for this repository, automatic checking is on and this switch is on.' : 'Not active: ' + j.sentence + ' All four must hold before anything is sent.')));
654
822
  if (j.paused) kids.push(el('div', { 'data-r24': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
@@ -810,6 +978,35 @@ export function validateList() {
810
978
  /* ── Today (proactive-engine D1): what is waiting, computed from the ledgers, with feedback ─────────────────────── */
811
979
  let tNote = null;
812
980
  let tMissed = false;
981
+ /* The text inputs of the Today list are kept across redraws. The list is drawn again on every state change (the 4-second retry of
982
+ * an unread repository is one), and a new input on each draw threw away what the person was typing and the caret with it. Each
983
+ * input is made once; a redraw writes the stored value into it only while the person has not edited it since the last save. */
984
+ const tInputs = new Map();
985
+ function tInput(key, attrs, stored) {
986
+ let n = tInputs.get(key);
987
+ if (!n) {
988
+ n = el('input', { ...attrs, onInput: (e) => { e.target.dataset.dirty = '1'; } });
989
+ tInputs.set(key, n);
990
+ }
991
+ if (!n.dataset.dirty && n.value !== stored) n.value = stored;
992
+ // The redraw moves the input into a new row, and moving a node blurs it: give the focus and the caret back once the draw is done.
993
+ if (typeof document !== 'undefined' && document.activeElement === n) {
994
+ let a = null, b = null;
995
+ try { a = n.selectionStart; b = n.selectionEnd; } catch { /* a number input has no selection */ }
996
+ queueMicrotask(() => {
997
+ if (!n.isConnected || document.activeElement === n) return;
998
+ n.focus();
999
+ try { if (a !== null) n.setSelectionRange(a, b); } catch { /* as above */ }
1000
+ });
1001
+ }
1002
+ return n;
1003
+ }
1004
+ /** Save a settings patch from kept inputs; on success their text is the stored value again. */
1005
+ async function tSaveFrom(inputs, patch) {
1006
+ const out = await tSettings(patch);
1007
+ if (out && out.ok !== false) for (const n of inputs) delete n.dataset.dirty;
1008
+ setState({});
1009
+ }
813
1010
  let tRetry = null;
814
1011
  const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
815
1012
  const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
@@ -834,6 +1031,7 @@ async function tSettings(patch) {
834
1031
  const out = await post('/api/proactive/settings', patch);
835
1032
  reload('today'); reload('brief'); reload('prepare'); reload('learn');
836
1033
  tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
1034
+ return out;
837
1035
  }
838
1036
  function tCard(c) {
839
1037
  const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
@@ -1011,7 +1209,7 @@ function learningSection() {
1011
1209
  kids.push(el('div', { style: NOTE, 'data-r26': 'last-run' }, iv(last ? 'Last weekly review: ' + String(last.at).replace('T', ' ').slice(0, 16) + ' UTC. It runs about every 7 days, only while the workspace server runs, and does not catch up.' : 'No weekly review has run yet. It runs about every 7 days once there is feedback, only while the workspace server runs, and does not catch up.')));
1012
1210
  kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:2px 14px 6px' },
1013
1211
  b('run the weekly review now', 'run', () => lPost('/api/proactive/learn/run', {}, (o) => 'review taken — ' + o.proposed.length + ' lesson(s) proposed'), 'refused while learning is paused'),
1014
- b('forget what it learned', 'reset', () => lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'), 'earlier feedback stops counting; nothing is deleted')));
1212
+ b('forget what it learned', 'reset', () => { if (typeof window !== 'undefined' && window.confirm && !window.confirm('Forget what it learned? Earlier feedback stops counting. Nothing is deleted.')) return; lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'); }, 'earlier feedback stops counting; nothing is deleted')));
1015
1213
  return kids;
1016
1214
  }
1017
1215
  export function todayList() {
@@ -1024,9 +1222,9 @@ export function todayList() {
1024
1222
  el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); reload('learn'); setState({}); } }, '↻ refresh'),
1025
1223
  el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
1026
1224
  if (tMissed) {
1027
- 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' });
1225
+ const ta = tInput('missed', { 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' }, '');
1028
1226
  kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
1029
- 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')));
1227
+ 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; delete ta.dataset.dirty; } setState({}); } }, 'save')));
1030
1228
  }
1031
1229
  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)));
1032
1230
  const st = status(r, 'Today');
@@ -1053,16 +1251,16 @@ export function todayList() {
1053
1251
  kids.push(el('div', { style: LABEL }, iv('morning brief')));
1054
1252
  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 } })));
1055
1253
  kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
1056
- 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)' });
1057
- 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')));
1254
+ const nm = tInput('name', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', '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)' }, s.name || '');
1255
+ 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: () => tSaveFrom([nm], { name: nm.value }) }, 'save')));
1058
1256
  kids.push(...learningSection());
1059
1257
  kids.push(chk('pause learning (no weights, no lessons, nothing narrowed)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
1060
- 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)' });
1061
- 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)' });
1062
- 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)' });
1258
+ const days = tInput('stalled', { type: 'number', min: '1', max: '90', '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)' }, String(s.stalledDays));
1259
+ const qs = tInput('quiet-start', { type: 'text', placeholder: '22:00', '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)' }, s.quiet ? s.quiet.start : '');
1260
+ const qe = tInput('quiet-end', { type: 'text', placeholder: '07:00', '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)' }, s.quiet ? s.quiet.end : '');
1063
1261
  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'),
1064
- el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSettings({ stalledDays: Number(days.value) }) }, 'save')));
1262
+ el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSaveFrom([days], { stalledDays: Number(days.value) }) }, 'save')));
1065
1263
  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,
1066
- 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')));
1264
+ el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSaveFrom([qs, qe], { quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
1067
1265
  return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
1068
1266
  }