create-pmos 0.46.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 (83) 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 +13 -3
  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/lib/handover-run.mjs +33 -0
  11. package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
  12. package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
  13. package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
  14. package/payload/okf/core/concepts/frontmatter-format.md +6 -1
  15. package/payload/planning/evals/eval-calibration.md +1 -1
  16. package/payload/planning/okrs/health-budgets.md +1 -1
  17. package/payload/scripts/pmos-log.sh +18 -8
  18. package/payload/scripts/pmos-model.js +233 -2
  19. package/payload/scripts/status-facts.js +204 -0
  20. package/payload/scripts/workflow-state.js +52 -0
  21. package/payload/skills/SKILL-FORMAT.md +1 -0
  22. package/payload/state/discoveries.md +1 -1
  23. package/workspace/README.md +3 -3
  24. package/workspace/bin/live-notes.js +133 -0
  25. package/workspace/design-parity.js +328 -21
  26. package/workspace/doc-category-bench.js +165 -0
  27. package/workspace/fixtures/doc-category-labelled.json +167 -0
  28. package/workspace/guide/initiatives-and-next-steps.md +16 -2
  29. package/workspace/guide/knowledge-graph.md +89 -2
  30. package/workspace/guide/layout.md +30 -2
  31. package/workspace/guide/proactive-engine.md +26 -4
  32. package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
  33. package/workspace/guide/statuses.md +1 -0
  34. package/workspace/guide/validate-with-typesafe.md +17 -1
  35. package/workspace/lib/claims.js +51 -2
  36. package/workspace/lib/doc-category-corrections.js +113 -0
  37. package/workspace/lib/doc-category-writeback.js +110 -0
  38. package/workspace/lib/doc-category.js +225 -0
  39. package/workspace/lib/engine/pmos-model.js +233 -2
  40. package/workspace/lib/engine/status-facts.js +204 -0
  41. package/workspace/lib/engine/workflow-state.js +52 -0
  42. package/workspace/lib/jev-categorize.js +399 -0
  43. package/workspace/lib/jev-claims.js +59 -15
  44. package/workspace/lib/jev-route.js +129 -0
  45. package/workspace/lib/kg-cli.js +5 -2
  46. package/workspace/lib/kg-dangling-baseline.json +1 -0
  47. package/workspace/lib/kg-dangling-rules.json +1 -0
  48. package/workspace/lib/kg.js +26 -9
  49. package/workspace/lib/learn.js +282 -0
  50. package/workspace/lib/live-notes.js +890 -0
  51. package/workspace/lib/proactive-cli.js +49 -1
  52. package/workspace/lib/proactive.js +8 -7
  53. package/workspace/lib/typesafe-settings.js +10 -2
  54. package/workspace/package-lock.json +1 -1
  55. package/workspace/package.json +5 -4
  56. package/workspace/public/pm/app.js +148 -79
  57. package/workspace/public/pm/data.js +58 -0
  58. package/workspace/public/pm/fixture.js +40 -2
  59. package/workspace/public/pm/modes-live.js +267 -21
  60. package/workspace/public/pm/panel-cockpit.js +636 -0
  61. package/workspace/public/pm/panel-detail.js +210 -8
  62. package/workspace/public/pm/panel-initiatives.js +9 -9
  63. package/workspace/public/pm/panel-newsess.js +145 -86
  64. package/workspace/public/pm/panel-repos.js +34 -17
  65. package/workspace/public/pm/panel-tabs.js +145 -27
  66. package/workspace/public/pm/state.js +45 -4
  67. package/workspace/public/pm/tab-doc.js +244 -16
  68. package/workspace/public/pm/tab-term.js +11 -1
  69. package/workspace/public/pm/tokens.css +4 -3
  70. package/workspace/public/pm/view-addrepo.js +207 -0
  71. package/workspace/public/pm/view-files.js +197 -0
  72. package/workspace/public/pm/view-guide.js +141 -0
  73. package/workspace/public/pm/view-knowledge.js +225 -0
  74. package/workspace/public/pm/view-memory.js +122 -0
  75. package/workspace/public/pm/view-portfolio.js +153 -0
  76. package/workspace/public/pm/view-reports.js +190 -0
  77. package/workspace/public/pm/view-scheduled.js +161 -0
  78. package/workspace/public/pm/view-skills.js +216 -0
  79. package/workspace/public/pm/view-today.js +168 -0
  80. package/workspace/public/pm/view-validate.js +171 -0
  81. package/workspace/server.js +394 -10
  82. package/workspace/skills/live-notes.skill +58 -0
  83. package/workspace/skills/morning-brief.skill +1 -0
@@ -27,7 +27,8 @@
27
27
  */
28
28
 
29
29
  import { el, setChildren } from './dom.js';
30
- import { state, setState, FIXTURE } from './state.js';
30
+ import { state, setState, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
31
+ import { isMobile } from './mobile.js';
31
32
  import * as FX from './fixture.js';
32
33
  import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel-initiatives.js';
33
34
  import { mountDoc, disposeDoc, docText } from './tab-doc.js';
@@ -41,7 +42,9 @@ import { boardOf, currentProduct } from './board-read.js';
41
42
  * far smaller failure than a dead panel 4. When it lands, the panel repaints itself. */
42
43
  let termMod = null;
43
44
  let liveMod = null;
45
+ let cockpitMod = null;
44
46
  /* Loaded lazily; a repaint is asked for only when a skill tab is already open (restored on reload) and waiting for it. */
47
+ import('./panel-cockpit.js').then((m) => { cockpitMod = m; if ((state.tabs || []).some((t) => t.kind === 'view')) setState({}); }).catch(() => { cockpitMod = null; });
45
48
  import('./modes-live.js').then((m) => { liveMod = m; if ((state.tabs || []).some((t) => t.kind === 'skill' || t.kind === 'transcript')) setState({}); }).catch(() => { liveMod = null; });
46
49
  import('./tab-term.js')
47
50
  .then((m) => { termMod = m; setState({}); })
@@ -332,7 +335,17 @@ const CHIP_STYLE = 'flex:none;align-self:center;font-family:var(--mono);font-siz
332
335
  const SPLIT_STYLE = 'flex:none;margin-left:auto;align-self:center;font-family:var(--mono);font-size:11px;color:oklch(0.85 0.01 214);padding:5px 12px;min-height:30px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%);white-space:nowrap';
333
336
  const CHIP_HOVER = 'border-color:oklch(0.56 0.021 213.5)';
334
337
 
335
- const tabBar = el('div', { style: BAR_STYLE });
338
+ const tabBar = el('div', { 'data-tabs': 'bar', style: BAR_STYLE });
339
+
340
+ /* ── the cockpit's tab strip (workspace-cockpit-nav Run 4, planning/design/cockpit-mockup/WsA.dc.html) ─────────────────────────
341
+ * On a desktop the strip is the mockup's: a 44px band whose tabs sit on its floor, the tab in front a card with rounded top corners,
342
+ * the others plain, each a dot and a title in 12px mono, and `+ session` at the right end. A phone and `?layout=legacy` keep the
343
+ * prototype's strip above. What the mockup does not draw is kept (R12): a tab's second line (its initiative and session) sits under
344
+ * its title in the tab's own lower padding, so the title stays where the mockup puts it; a tab that is not in front keeps its close
345
+ * control, in its right padding; the web, options and split controls sit to the left of `+ session`. */
346
+ const cockpitLook = () => !LEGACY_LAYOUT_FLAG && !isMobile();
347
+ const CK_BAR = 'flex:none;display:flex;align-items:flex-end;gap:4px;padding:0 10px;height:45px;border-bottom:1px solid var(--border);min-width:0;overflow-x:auto';
348
+ const CK_BTN = 'flex:none;margin:0 0 4px;height:30px;display:grid;place-items:center;font:500 12.5px var(--sans);line-height:normal;background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;white-space:nowrap';
336
349
 
337
350
  const newSessionBtn = el('button', {
338
351
  onClick: () => newSession(), style: CHIP_STYLE, hover: CHIP_HOVER,
@@ -368,7 +381,7 @@ const emptyState = el('div', {
368
381
  style: 'flex:1;display:grid;place-items:center;color:oklch(0.55 0.02 213);font-size:12.5px;padding:20px;text-align:center',
369
382
  }, "nothing open — click a task's output or an artifact, or + session above");
370
383
 
371
- const panesRow = el('div', { style: 'flex:1;display:flex;min-height:0;min-width:0;overflow:hidden' });
384
+ const panesRow = el('div', { 'data-tabs': 'panes', style: 'flex:1;display:flex;min-height:0;min-width:0;overflow:hidden' });
372
385
 
373
386
  /* ── the scratchpad (lines 983–991) ──────────────────────────────────────────────────────────
374
387
  * One textarea, created once. Its text is written back as a CHILD TEXT NODE as well as a value,
@@ -395,9 +408,16 @@ const SCRATCH_PLACEHOLDER = FIXTURE
395
408
  ? 'scratchpad — survives tab switches, dies with the page'
396
409
  : 'scratchpad — one pad per session, saved on this machine';
397
410
 
411
+ /* workspace-cockpit-nav Run 3 (PRD R10) — the drawer takes the form of the cockpit board
412
+ * `planning/design/cockpit-mockup/ViewFilesPad.dc.html`: a labelled header with a fold control, the pad
413
+ * as a bordered field, and one row of buttons with a short note. The textarea stays a DIRECT child of
414
+ * `scratchBlock` (the height ceiling and pm-tabs-test measure through `parentElement`), and every
415
+ * behaviour below — the per-session pad, the autosave, the height handle — is unchanged. */
398
416
  const scratchTa = el('textarea', {
399
417
  placeholder: SCRATCH_PLACEHOLDER,
400
- style: 'height:96px;background:oklch(0.11 0.004 228.8);color:#c4ccce;padding:10px 14px;font-family:var(--mono);font-size:12px',
418
+ 'data-scratch': 'text',
419
+ style: 'height:96px;flex:none;width:100%;border:1px solid var(--input);border-radius:8px;background:var(--background);'
420
+ + 'color:var(--foreground);padding:10px 12px;font:12.5px/1.55 var(--mono)',
401
421
  onInput: (e) => onScratchInput(e.target.value),
402
422
  });
403
423
 
@@ -457,9 +477,10 @@ const scratchGrip = el('div', {
457
477
  'aria-orientation': 'horizontal',
458
478
  'aria-label': 'resize the scratchpad — drag, or ↑ / ↓',
459
479
  title: 'drag to resize · ↑ / ↓ to nudge · double-click to reset',
460
- style: 'flex:none;height:7px;cursor:ns-resize;background:oklch(0.11 0.004 228.8);'
461
- + 'border-top:1px solid oklch(1 0 0 / 10%);touch-action:none',
462
- hover: 'background:oklch(1 0 0 / 10%)',
480
+ 'data-scratch': 'grip',
481
+ // full width across the drawer's padding, on its top edge
482
+ style: 'flex:none;height:7px;margin:-12px -18px 0;cursor:ns-resize;background:var(--sidebar);touch-action:none',
483
+ hover: 'background:var(--border)',
463
484
  onPointerdown: (e) => {
464
485
  dragFrom = { y: e.clientY, h: state.scratchH || SCRATCH_DEFAULT };
465
486
  try { e.target.setPointerCapture(e.pointerId); } catch { /* not captured — the move still works */ }
@@ -564,6 +585,13 @@ if (typeof window !== 'undefined' && !FIXTURE) {
564
585
  document.addEventListener('visibilitychange', () => { if (document.hidden) flushPad(); });
565
586
  }
566
587
 
588
+ /* The board's two button forms, on the token system: `.btnp` (the one primary action) and `.btn`. */
589
+ const PAD_BTN = 'flex:none;font:500 12.5px var(--sans);border-radius:8px;padding:0 12px;height:28px;'
590
+ + 'display:inline-grid;place-items:center;white-space:nowrap;';
591
+ const PAD_BTN_PRIMARY = PAD_BTN + 'background:var(--foreground);color:var(--background);border:0';
592
+ const PAD_BTN_OUTLINE = PAD_BTN + 'background:transparent;color:var(--foreground);border:1px solid var(--input)';
593
+ const PAD_BTN_OFF = PAD_BTN + 'background:transparent;color:var(--muted-foreground);border:1px solid var(--input)';
594
+
567
595
  const scratchCopyLabel = iv('⧉ copy');
568
596
  const scratchCopyBtn = el('button', {
569
597
  onClick: () => {
@@ -571,24 +599,50 @@ const scratchCopyBtn = el('button', {
571
599
  setState({ scratchCopied: true });
572
600
  setTimeout(() => setState({ scratchCopied: false }), 1200);
573
601
  },
574
- style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.85 0.01 214);padding:4px 10px;min-height:26px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%)',
575
- hover: CHIP_HOVER,
602
+ 'data-scratch': 'copy',
603
+ style: PAD_BTN_OUTLINE,
604
+ hover: 'background:var(--accent)',
576
605
  }, scratchCopyLabel);
577
606
 
578
607
  const scratchSendLabel = iv('open a terminal to send');
579
608
  const scratchSendBtn = el('button', {
580
609
  onClick: () => sendScratchToTerminal(),
581
- style: 'font-family:var(--mono);font-size:10.5px;color:' + FX.SCRATCH_SEND_COLOR.off + ';padding:4px 10px;min-height:26px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%)',
582
- hover: CHIP_HOVER,
610
+ 'data-scratch': 'send',
611
+ style: PAD_BTN_OFF,
583
612
  }, scratchSendLabel);
584
613
 
614
+ /* Clear — the board's second button. It empties the pad through the same path as typing, so the
615
+ * empty pad is saved for this session like any other edit. Disabled when there is nothing to clear. */
616
+ const scratchClearBtn = el('button', {
617
+ onClick: () => { if (state.scratchText) onScratchInput(''); },
618
+ 'data-scratch': 'clear',
619
+ style: PAD_BTN_OUTLINE,
620
+ hover: 'background:var(--accent)',
621
+ }, 'clear');
622
+
623
+ const scratchHeadLabel = el('span', {
624
+ 'data-scratch': 'label',
625
+ style: 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)',
626
+ }, 'scratchpad');
627
+ const scratchFoldBtn = el('button', {
628
+ onClick: () => setState({ scratchOpen: false }),
629
+ 'data-scratch': 'fold', 'aria-label': 'close the scratchpad', title: 'Close the scratchpad. The text is kept.',
630
+ style: 'flex:none;color:var(--muted-foreground);padding:0 4px', hover: 'color:var(--foreground)',
631
+ }, '▾');
632
+ const scratchNote = el('span', {
633
+ 'data-scratch': 'note',
634
+ style: 'min-width:0;color:var(--muted-foreground);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
635
+ });
636
+
585
637
  const scratchBlock = el('div', {
586
- style: 'display:flex;flex-direction:column;border-top:1px solid oklch(1 0 0 / 10%)',
638
+ 'data-scratch': 'pad',
639
+ style: 'display:flex;flex-direction:column;gap:8px;padding:12px 18px;border-top:1px solid var(--border);background:var(--sidebar)',
587
640
  },
588
641
  FIXTURE ? null : scratchGrip,
642
+ el('div', { style: 'display:flex;align-items:center;justify-content:space-between' }, scratchHeadLabel, scratchFoldBtn),
589
643
  scratchTa,
590
- el('div', { style: 'display:flex;gap:6px;padding:5px 12px 7px;background:oklch(0.11 0.004 228.8)' },
591
- scratchCopyBtn, scratchSendBtn),
644
+ el('div', { style: 'display:flex;align-items:center;gap:8px;min-width:0' },
645
+ scratchSendBtn, scratchCopyBtn, scratchClearBtn, scratchNote),
592
646
  );
593
647
 
594
648
  /* ── the bottom bar (lines 992–996) ─────────────────────────────────────────────────────────── */
@@ -598,6 +652,7 @@ const colTitleLabel = iv('scratchpad');
598
652
  const copyLabel = iv('copy');
599
653
 
600
654
  const bottomBar = el('div', {
655
+ 'data-tabs': 'bottom',
601
656
  style: 'display:flex;gap:10px;align-items:center;padding:5px 12px;border-top:1px solid oklch(1 0 0 / 10%);font-size:11px;color:oklch(0.6 0.02 213)',
602
657
  },
603
658
  el('button', {
@@ -833,6 +888,8 @@ function paneFor(tab) {
833
888
  if (liveMod && typeof liveMod.mountSkillTab === 'function') liveMod.mountSkillTab(host, tab);
834
889
  } else if (tab.kind === 'transcript') {
835
890
  if (liveMod && typeof liveMod.mountTranscriptTab === 'function') liveMod.mountTranscriptTab(host, tab);
891
+ } else if (tab.kind === 'view') {
892
+ if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
836
893
  } else if (tab.kind === 'web') {
837
894
  mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
838
895
  } else {
@@ -937,6 +994,42 @@ export function tabBranch(t, includeMain) {
937
994
  return trees.length > 1 && main ? main.branch : null;
938
995
  }
939
996
 
997
+ function cockpitTabNode(t, act, splitTab) {
998
+ const front = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
999
+ const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
1000
+ const meta = t.kind === 'term'
1001
+ ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
1002
+ : (t.kind === 'task' ? 'task' : t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
1003
+ const br = tabBranch(t);
1004
+ return el('div', {
1005
+ 'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
1006
+ style: 'position:relative;flex:none;display:flex;align-items:center;gap:8px;padding:0 14px;font:12px var(--mono);'
1007
+ + (front ? 'height:37px;border:1px solid var(--border);border-bottom:0;border-radius:8px 8px 0 0;background:var(--card);color:var(--foreground)'
1008
+ : 'height:36px;color:var(--muted-foreground)'),
1009
+ },
1010
+ el('button', {
1011
+ onClick: () => setState({ active: t.id }),
1012
+ title: t.title + (t.slug ? '\n' + t.slug + ' · ' + meta : ''),
1013
+ style: 'display:flex;gap:8px;align-items:center;align-self:stretch;color:inherit',
1014
+ },
1015
+ el('span', { 'data-tab-dot': t.kind, style: 'width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:' + dotColor + ';color:' + dotColor }),
1016
+ el('span', { style: 'display:flex;flex-direction:column;min-width:0;align-items:flex-start' },
1017
+ el('span', { style: 'max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(t.title)),
1018
+ el('span', {
1019
+ 'data-tab-sub': '',
1020
+ // under the title, inside the tab's own width (the tab is its containing block), clipped to it
1021
+ style: 'position:absolute;left:30px;right:14px;bottom:1px;font:9px var(--mono);line-height:10px;color:oklch(0.52 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left',
1022
+ }, iv(t.slug), ' · ', iv(meta), br ? el('span', { 'data-r1': 'tab-branch' }, iv(' · ⑂ ' + br)) : null),
1023
+ ),
1024
+ ),
1025
+ el('button', {
1026
+ onClick: () => closeTab(t.id), 'data-tab-close': '', 'aria-label': 'close ' + t.title, title: 'close',
1027
+ style: 'color:var(--muted-foreground)' + (front ? '' : ';position:absolute;right:3px;top:50%;transform:translateY(-50%)'),
1028
+ hover: 'color:var(--foreground)',
1029
+ }, '×'),
1030
+ );
1031
+ }
1032
+
940
1033
  function tabNode(t, act, splitTab) {
941
1034
  const selected = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
942
1035
  const bg = selected ? FX.TAB_COLORS.activeBg : 'transparent';
@@ -949,7 +1042,7 @@ function tabNode(t, act, splitTab) {
949
1042
  * fixture render keeps the design's literal — the comparator reads this text node. */
950
1043
  const meta = t.kind === 'term'
951
1044
  ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
952
- : (t.kind === 'task' ? 'task' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
1045
+ : (t.kind === 'task' ? 'task' : t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
953
1046
 
954
1047
  return el('div', {
955
1048
  style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
@@ -997,14 +1090,34 @@ export function renderTabs(host) {
997
1090
  // ── tab bar ──
998
1091
  const canSplit = visTabs.length >= 2 || !!state.splitId;
999
1092
  splitLabel.textContent = state.splitId ? '⫽ unsplit' : '⫽ split';
1000
- setChildren(tabBar, [
1001
- ...visTabs.map((t) => tabNode(t, act, splitTab)),
1002
- newSessionBtn,
1003
- FIXTURE ? null : newWebBtn,
1004
- !FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
1005
- newSessionOptsBtn,
1006
- canSplit ? splitBtn : null,
1007
- ]);
1093
+ const ck = cockpitLook();
1094
+ newSessionBtn.textContent = ck ? '+ session' : '+ session';
1095
+ emptyState.textContent = "nothing open — click a task's output or an artifact, or " + (ck ? '+' : '+') + ' session above';
1096
+ if (ck) {
1097
+ tabBar.setAttribute('style', CK_BAR);
1098
+ newSessionBtn.setAttribute('style', CK_BTN + ';margin-left:0');
1099
+ for (const b of [newWebBtn, newSessionOptsBtn, splitBtn]) b.setAttribute('style', CK_BTN);
1100
+ const extras = [
1101
+ FIXTURE ? null : newWebBtn,
1102
+ !FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
1103
+ newSessionOptsBtn,
1104
+ canSplit ? splitBtn : null,
1105
+ ].filter(Boolean);
1106
+ extras[0].style.marginLeft = 'auto';
1107
+ setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, newSessionBtn]);
1108
+ } else {
1109
+ tabBar.setAttribute('style', BAR_STYLE);
1110
+ newSessionBtn.setAttribute('style', CHIP_STYLE);
1111
+ newWebBtn.setAttribute('style', CHIP_STYLE); newSessionOptsBtn.setAttribute('style', CHIP_STYLE); splitBtn.setAttribute('style', SPLIT_STYLE);
1112
+ setChildren(tabBar, [
1113
+ ...visTabs.map((t) => tabNode(t, act, splitTab)),
1114
+ newSessionBtn,
1115
+ FIXTURE ? null : newWebBtn,
1116
+ !FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
1117
+ newSessionOptsBtn,
1118
+ canSplit ? splitBtn : null,
1119
+ ]);
1120
+ }
1008
1121
 
1009
1122
  // ── panes ──
1010
1123
  reapClosedPanes();
@@ -1031,10 +1144,15 @@ export function renderTabs(host) {
1031
1144
  scratchCopyLabel.textContent = state.scratchCopied ? 'copied ✓' : '⧉ copy';
1032
1145
  const canSend = !!(act && act.kind === 'term');
1033
1146
  scratchSendLabel.textContent = canSend ? '⇥ send to terminal' : 'open a terminal to send';
1034
- scratchSendBtn.setAttribute('style',
1035
- 'font-family:var(--mono);font-size:10.5px;color:'
1036
- + (canSend ? FX.SCRATCH_SEND_COLOR.on : FX.SCRATCH_SEND_COLOR.off)
1037
- + ';padding:4px 10px;min-height:26px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%)');
1147
+ scratchSendBtn.setAttribute('style', canSend ? PAD_BTN_PRIMARY : PAD_BTN_OFF);
1148
+ scratchClearBtn.disabled = !scratchText;
1149
+ scratchClearBtn.style.opacity = scratchText ? '' : '0.5';
1150
+ /* The header names whose pad this is, and the note says what keeps it and what Send does. Both are
1151
+ * read from the same facts `padKey` uses, so they cannot claim a pad the drawer is not showing. */
1152
+ scratchHeadLabel.textContent = FIXTURE ? 'scratchpad'
1153
+ : (canSend ? 'scratchpad · this session' : 'scratchpad · this repository');
1154
+ scratchNote.textContent = FIXTURE ? 'Kept until the page closes.'
1155
+ : (canSend ? 'Kept for this session. Send also presses Enter.' : 'Kept for this repository. Open a terminal to send.');
1038
1156
 
1039
1157
  // ── bottom bar ──
1040
1158
  scratchToggleLabel.textContent = state.scratchOpen ? '▼ scratchpad' : '▲ scratchpad';
@@ -14,15 +14,47 @@
14
14
  /* Fixture mode is decided from the URL, not from another module: the persistence bypass below is
15
15
  * the whole reason the comparator can trust a run. Reading it here keeps state.js loadable on its
16
16
  * own and stops a data-layer import from deciding whether the PM's layout is written to disk. */
17
- export const FIXTURE = (() => {
18
- try { return new URLSearchParams(location.search).get('fixture') === 'proto'; }
19
- catch { return false; }
17
+ /* `?fixture=proto` is the prototype's own seed data; `?fixture=cockpit` is the cockpit mockup's (planning/design/cockpit-mockup/): the
18
+ * same fixture paths, a different seed. Both are the offline demo and the parity script's like-for-like content. */
19
+ const FIXTURE_KIND = (() => {
20
+ try { const f = new URLSearchParams(location.search).get('fixture'); return f === 'proto' || f === 'cockpit' ? f : null; }
21
+ catch { return null; }
20
22
  })();
23
+ export const FIXTURE = FIXTURE_KIND !== null;
24
+ /** `?layout=legacy` draws the three-column shell on a desktop, so the suites of the panels that shell uses keep guarding them until
25
+ * Run 4 moves the phone onto the cockpit. Nothing offers it. */
26
+ export const LEGACY_LAYOUT_FLAG = (() => { try { return new URLSearchParams(location.search).get('layout') === 'legacy'; } catch { return false; } })();
27
+ export const FIXTURE_COCKPIT = FIXTURE_KIND === 'cockpit';
28
+
29
+ /** What the cockpit fixture starts with beyond its seed data: the derived statuses (so the list can group), the open tabs. */
30
+ const COCKPIT_START = {
31
+ statuses: {
32
+ microsites: {
33
+ 'ooh-weather-expansion': { status: 'blocked' }, 'psle-paywall-revamp': { status: 'delivered' }, 'smrt-news-feed': { status: 'building' },
34
+ 'lightbox-ga-tagging': { status: 'accepted' }, 'login-metric-tagging': { status: 'accepted' }, 'shortlink-redirects': { status: 'accepted' },
35
+ },
36
+ // five things wait on the person, as the mockup's "Today 5" says: two here, three in pmos (the tile with the dot), none in scarlet
37
+ pmos: { 'cockpit-terminal-mod': { status: 'blocked' }, 'workspace-cockpit-nav': { status: 'blocked' }, 'kit-publish': { status: 'delivered' } },
38
+ // the mockup's third tile carries no dot: nothing in that repository is waiting on the person
39
+ scarlet: { 'scarlet-onboarding': { status: 'building' } },
40
+ },
41
+ product: 'microsites', sel: 'ooh-weather-expansion',
42
+ tabs: [
43
+ { id: 'term:ooh-weather-expansion:claude · merge PR 41', kind: 'term', title: 'claude · merge PR 41', slug: 'ooh-weather-expansion', repo: 'microsites', sess: 'claude · merge PR 41', state: 'working', role: 'TERM' },
44
+ { id: 'doc:planning/prd/ooh-weather-expansion-prd.md', kind: 'doc', title: '…-prd.md', slug: 'ooh-weather-expansion', path: 'planning/prd/ooh-weather-expansion-prd.md', role: 'DOC' },
45
+ { id: 'view:memory:microsites', kind: 'view', view: 'memory', title: 'tool · memory', slug: 'microsites', repo: 'microsites' },
46
+ ],
47
+ active: 'term:ooh-weather-expansion:claude · merge PR 41',
48
+ };
21
49
 
22
50
  const STORE_KEY = 'pmos.workspace.pm.v1';
23
51
 
24
52
  /** The initial state — the prototype's `state = {…}` block, key for key. */
25
53
  function initial() {
54
+ const base = initialBase();
55
+ return FIXTURE_COCKPIT ? { ...base, ...JSON.parse(JSON.stringify(COCKPIT_START)) } : base;
56
+ }
57
+ function initialBase() {
26
58
  return {
27
59
  theme: 'dark', sel: null, q: '', mode: 'product', collapsedGroups: {}, groupBy: 'objective', activeOnly: false, legendOpen: false, settingsOpen: false,
28
60
  product: null, intgSynced: {},
@@ -35,6 +67,13 @@ function initial() {
35
67
  newType: 'init', newName: '', newSlug: '', newAnchor: null, newFlow: 'adlc-feature',
36
68
  userGroups: {}, userKrs: {}, addedInits: {}, userInits: {}, selTask: null, collapsedPhases: {},
37
69
  detail: { kind: 'init' },
70
+ // the rail's menu (workspace-cockpit-nav): open or shut, never persisted
71
+ menuOpen: false,
72
+ paletteOpen: false, // Run 2: the ⌘K palette — never persisted
73
+ layoutFocus: false, // Run 2: the Focus layout — never persisted (distinct from `focus`, the focus-mode data)
74
+ todayScope: 'repo',
75
+ filesOpen: false, // Run 3: the Work list shows the repository's files — never persisted
76
+ todayDismissed: {},
38
77
  tabs: [], active: null, splitId: null,
39
78
  // overhaul-r1: reader measure per tab (R5), which repositories show their lines of work (R2.1),
40
79
  // and the last remove/rename answer, shown on the row it is about (R3.3)
@@ -155,7 +194,9 @@ const TAB_FIELDS = ['id', 'kind', 'title', 'slug', 'repo', 'sess', 'sessionId',
155
194
  // a skill tab's skill id — a reload reopens the same skill
156
195
  'skillId',
157
196
  // a conversation tab's session id — a reload reopens the same transcript
158
- 'convId'];
197
+ 'convId',
198
+ // a view tab's view key (workspace-cockpit-nav) — a reload reopens the same tool
199
+ 'view'];
159
200
  const MAX_TABS = 40;
160
201
 
161
202
  function packTab(t) {