create-pmos 0.37.0 → 0.38.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/bin/create-pmos.js +22 -3
  2. package/package.json +1 -1
  3. package/payload/AGENTS.md +4 -0
  4. package/payload/KIT-VERSION +2 -2
  5. package/payload/okf/core/concepts/frontmatter-format.md +27 -61
  6. package/payload/okf/core/templates/decision-record-template.md +34 -0
  7. package/payload/okf/core/templates/index.md +7 -2
  8. package/payload/okf/core/templates/pm-brief-template.md +34 -0
  9. package/payload/okf/core/templates/project-plan-template.md +43 -0
  10. package/payload/okf/core/templates/report-weekly-template.md +35 -0
  11. package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
  12. package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
  13. package/payload/planning/evals/eval-calibration.md +8 -0
  14. package/payload/planning/okrs/health-budgets.md +8 -0
  15. package/payload/scripts/anchor-resolve.js +56 -6
  16. package/payload/scripts/gate-initiative.sh +32 -0
  17. package/payload/scripts/okf-upgrade.js +232 -0
  18. package/payload/scripts/pmos-memory.js +375 -0
  19. package/payload/scripts/pmos-model.js +239 -0
  20. package/payload/scripts/pmos-skills.js +274 -0
  21. package/payload/scripts/workflow-state.js +178 -10
  22. package/payload/scripts/workflow-sweep.js +2 -1
  23. package/payload/state/discoveries.md +8 -0
  24. package/payload/state/index.md +7 -0
  25. package/payload/state/log.md +7 -0
  26. package/workspace/README.md +61 -204
  27. package/workspace/lib/board-cache.js +18 -8
  28. package/workspace/lib/console-state.js +2 -0
  29. package/workspace/lib/engine/anchor-resolve.js +56 -6
  30. package/workspace/lib/engine/pmos-memory.js +375 -0
  31. package/workspace/lib/engine/pmos-model.js +239 -0
  32. package/workspace/lib/engine/pmos-skills.js +274 -0
  33. package/workspace/lib/engine/workflow-state.js +178 -10
  34. package/workspace/lib/focus.js +5 -6
  35. package/workspace/lib/memory.js +54 -0
  36. package/workspace/lib/models.js +50 -0
  37. package/workspace/lib/preview-kind.js +12 -1
  38. package/workspace/lib/schedule-logic.js +31 -2
  39. package/workspace/lib/skill-store.js +5 -1
  40. package/workspace/lib/skills-registry.js +74 -0
  41. package/workspace/lib/sync.js +132 -0
  42. package/workspace/lib/tree-share.js +2 -0
  43. package/workspace/package-lock.json +1 -1
  44. package/workspace/package.json +3 -3
  45. package/workspace/public/pm/app.js +32 -2
  46. package/workspace/public/pm/data.js +160 -10
  47. package/workspace/public/pm/lifecycle.js +105 -0
  48. package/workspace/public/pm/markdown.js +10 -1
  49. package/workspace/public/pm/modes-live.js +371 -0
  50. package/workspace/public/pm/modes.js +7 -4
  51. package/workspace/public/pm/panel-detail.js +22 -2
  52. package/workspace/public/pm/panel-initiatives.js +57 -12
  53. package/workspace/public/pm/panel-newsess.js +25 -0
  54. package/workspace/public/pm/panel-repos.js +130 -4
  55. package/workspace/public/pm/panel-tabs.js +49 -2
  56. package/workspace/public/pm/state.js +11 -1
  57. package/workspace/public/pm/tab-doc.js +160 -26
  58. package/workspace/public/pm/tab-term.js +2 -0
  59. package/workspace/public/pm/tab-web.js +89 -0
  60. package/workspace/public/pm/tokens.css +4 -0
  61. package/workspace/public/pm/xlsx-read.js +117 -0
  62. package/workspace/server.js +690 -40
  63. package/workspace/skills/gather-requirements.skill +53 -0
  64. package/workspace/skills/memory-groom.skill +46 -0
  65. package/workspace/skills/report-biweekly.skill +53 -0
  66. package/workspace/skills/report-daily.skill +53 -0
  67. package/workspace/skills/report-monthly.skill +53 -0
  68. package/workspace/skills/report-special.skill +49 -0
  69. package/workspace/skills/report-weekly.skill +53 -0
  70. package/workspace/skills/skill-discover.skill +49 -0
  71. package/workspace/skills/skill-generate.skill +33 -0
@@ -295,6 +295,28 @@ function isolateNow() {
295
295
  return live.length > 0;
296
296
  }
297
297
 
298
+ /* ── the model chips (D75) ──────────────────────────────────────────────────────────────────
299
+ * · only a Claude session has a model control PMOS has checked, so only it is offered a choice;
300
+ * · `auto` sends nothing: the server routes by the policy, or asks for no model at all;
301
+ * · the chips say what the PM is choosing (a tier), never a model name that will age. */
302
+ function modelSection() {
303
+ if (state.newSessAgent !== 'claude') {
304
+ return el('div', { 'data-r12': 'model-note', style: SUB }, iv('model: only a Claude session can be given a model by PMOS — a shell, or any other CLI, runs its own default'));
305
+ }
306
+ const cur = state.newSessTier || 'auto';
307
+ return el('div', { 'data-r12': 'model' },
308
+ el('div', { style: LABEL }, 'model'),
309
+ el('div', { style: ROW },
310
+ ['auto', 'fast', 'balanced', 'deep'].map((t) => el('button', {
311
+ 'data-r12': 'tier-' + t,
312
+ onClick: () => setState({ newSessTier: t, newSessError: null }),
313
+ style: cur === t ? CHIP_ON : CHIP, hover: HOVER,
314
+ }, iv(t))),
315
+ el('span', { style: SUB }, cur === 'auto'
316
+ ? 'auto: PMOS asks for no model unless your policy routes this work to one'
317
+ : 'a ' + cur + ' tier — the model and effort it maps to are in your models policy')));
318
+ }
319
+
298
320
  /* ── the profile chips ─────────────────────────────────────────────────────────────────────── */
299
321
 
300
322
  /**
@@ -419,6 +441,8 @@ async function onStart() {
419
441
  // value from liveSessionCount, and its own comment asks the client to state what the PM saw.
420
442
  isolate: branch ? isolateNow() : null,
421
443
  profile: state.preflightProfile === undefined ? null : state.preflightProfile,
444
+ // D75 — only for a Claude session; 'auto' sends nothing and lets the policy route.
445
+ tier: (state.newSessAgent === 'claude' && state.newSessTier && state.newSessTier !== 'auto') ? state.newSessTier : undefined,
422
446
  });
423
447
 
424
448
  if (!res || !res.ok) {
@@ -495,6 +519,7 @@ export function newSessionBlock() {
495
519
  el('span', { style: SUB }, 'a CLI that is not on PATH is refused by the server, and says so'),
496
520
  ),
497
521
 
522
+ modelSection(),
498
523
  branchSection(),
499
524
  profileSection(),
500
525
 
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { el, setChildren } from './dom.js';
27
+ import { syncButton } from './lifecycle.js';
27
28
  import { state, setState, FIXTURE } from './state.js';
28
29
  import {
29
30
  productsList, repoRow, firstSlugOf, MODE_META, SETTINGS_GROUPS, ADDABLE_PRODUCTS,
@@ -166,13 +167,25 @@ export function selectRepo(id) {
166
167
  * belt and braces — it makes the absence a property of THIS module, so it cannot regress by someone
167
168
  * adding an id to the seed list.
168
169
  */
170
+ /* overhaul-r1 R6.1 — in LIVE mode, a mode with no server behind it is not offered. `agents`,
171
+ * `memory` and `connections` start empty and every edit is a client-only setState that no server
172
+ * receives (modes.js `seedOr`), so they looked broken because they were. They come back in a later
173
+ * release wired or replaced. Fixture mode keeps them: it is the prototype's recorded state, and the
174
+ * design comparison reads it. Exported so the live-mode test can assert the list, not a screenshot. */
175
+ export const UNBACKED_MODES = ['agent', 'connect']; // mode ids (labels: agents · connections). `memory` left this list in R9 (D72): the repository's ledger backs it.
176
+ /* overhaul-r4 — the PORTFOLIO mode joins "all repositories" in live mode only (the fixture drawer is the
177
+ * prototype's, and the design comparison reads it). Its meta lives here, not in fixture.js, for the
178
+ * same reason. */
179
+ const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'] });
180
+ const LIVE_EXTRA = { 'all repositories': ['portfolio'] };
169
181
  const DRAWER_GROUPS = SETTINGS_GROUPS.map((g) => ({
170
182
  label: g.label,
171
- items: g.items.filter((id) => id !== 'workflow' && MODE_META[id]),
172
- }));
183
+ items: (FIXTURE ? [] : LIVE_EXTRA[g.label] || []).concat(
184
+ g.items.filter((id) => id !== 'workflow' && MODE_META[id] && (FIXTURE || !UNBACKED_MODES.includes(id)))),
185
+ })).filter((g) => g.items.length);
173
186
 
174
187
  function drawerButton(id) {
175
- const [glyph, label] = MODE_META[id];
188
+ const [glyph, label] = META[id];
176
189
  const on = state.mode === id;
177
190
  return el('button', {
178
191
  onClick: () => setState({ mode: id, settingsOpen: false, col1: true }),
@@ -233,6 +246,119 @@ function repoButton(tuple, product) {
233
246
  );
234
247
  }
235
248
 
249
+ /* ── overhaul-r1: lines of work, rename, remove (LIVE mode only) ─────────────────────────────
250
+ * Fixture mode renders the prototype's row unchanged. In live mode the row gains a `⑂ N` chip
251
+ * when the repository has linked worktrees (R2.1), and the ACTIVE row gains rename and remove
252
+ * (R3.1/R3.2). The server's answer to a remove is shown under the row in its own words (R3.3).
253
+ */
254
+ const chipStyle = 'flex:none;font-family:var(--mono);font-size:10px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground);min-height:22px';
255
+ const actStyle = 'flex:none;font-size:12px;padding:2px 6px;color:var(--muted-foreground);min-height:28px';
256
+
257
+ function treesOf(name) {
258
+ return dataMod && typeof dataMod.productTrees === 'function' ? dataMod.productTrees(name) : [];
259
+ }
260
+
261
+ async function renameRepo(name) {
262
+ const label = typeof window !== 'undefined' && window.prompt ? window.prompt('Rename “' + name + '” to:', name) : null;
263
+ if (label === null || !String(label).trim() || String(label).trim() === name) return;
264
+ const out = await dataMod.renameRepository(name, String(label).trim());
265
+ if (!out || out.ok === false) { setState({ repoNotice: { name, tone: 'bad', text: (out && out.error) || 'rename refused' } }); return; }
266
+ await connectData();
267
+ setState({ product: String(label).trim(), repoNotice: null });
268
+ }
269
+
270
+ async function removeRepo(name) {
271
+ const ok = typeof window === 'undefined' || !window.confirm ? true
272
+ : window.confirm('Remove “' + name + '” from PMOS?\n\nThis forgets the repository and its worktree rows. No folder is deleted.');
273
+ if (!ok) return;
274
+ const out = await dataMod.removeRepository(name);
275
+ if (!out || out.ok === false) { setState({ repoNotice: { name, tone: 'bad', text: (out && out.error) || 'remove refused' } }); return; }
276
+ realRepos = (realRepos || []).filter((t) => t[0] !== name);
277
+ setState({ repoNotice: null });
278
+ }
279
+
280
+ function repoEntry(tuple, product) {
281
+ const row = repoButton(tuple, product);
282
+ if (FIXTURE || !dataMod) return row;
283
+ const name = tuple[0];
284
+ const trees = treesOf(name);
285
+ const linked = trees.filter((t) => !t.main);
286
+ const open = !!(state.treesOpen || {})[name];
287
+ const tools = [];
288
+ if (linked.length) {
289
+ tools.push(el('button', {
290
+ onClick: () => setState((s) => ({ treesOpen: { ...(s.treesOpen || {}), [name]: !open } })),
291
+ style: chipStyle, hover: 'color:var(--foreground)',
292
+ title: linked.length + ' linked worktree' + (linked.length === 1 ? '' : 's') + ' — show the lines of work',
293
+ 'data-r1': 'trees-chip',
294
+ }, ip('⑂ ' + linked.length)));
295
+ }
296
+ if (product === name) {
297
+ { const sb = syncButton(name); if (sb) tools.push(sb); }
298
+ tools.push(el('button', { onClick: () => renameRepo(name), style: actStyle, hover: 'color:var(--foreground)', title: 'rename this repository', 'data-r1': 'rename' }, '✎'));
299
+ tools.push(el('button', { onClick: () => removeRepo(name), style: actStyle, hover: 'color:var(--destructive)', title: 'remove this repository from PMOS (no folder is deleted)', 'data-r1': 'remove' }, '✕'));
300
+ }
301
+ const kids = [el('div', { style: 'display:flex;align-items:center' },
302
+ el('div', { style: 'flex:1;min-width:0' }, row),
303
+ tools.length ? el('div', { style: 'display:flex;align-items:center;gap:2px;padding-right:8px' }, tools) : null)];
304
+ if (open && trees.length) {
305
+ kids.push(el('div', { style: 'padding:2px 14px 8px 39px', 'data-r1': 'trees' },
306
+ trees.map((t) => el('div', {
307
+ style: 'display:flex;gap:8px;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);padding:2px 0',
308
+ title: t.initiative ? 'initiative read from the branch name (initiative/<id>) — not a recorded attribution' : '',
309
+ },
310
+ el('span', { style: 'color:var(--foreground)' }, ip((t.main ? '⎇ ' : '⑂ ') + (t.branch || '(detached)'))),
311
+ el('span', null, ip(t.sessions + ' session' + (t.sessions === 1 ? '' : 's'))),
312
+ t.initiative ? el('span', null, ip('#' + t.initiative)) : null,
313
+ )),
314
+ ));
315
+ }
316
+ const n = state.repoNotice;
317
+ if (n && n.name === name) {
318
+ kids.push(el('div', {
319
+ style: 'padding:4px 14px 8px 39px;font-size:11px;color:' + (n.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)'),
320
+ 'data-r1': 'notice',
321
+ }, ip(n.text)));
322
+ }
323
+ return el('div', null, kids);
324
+ }
325
+
326
+ /**
327
+ * overhaul-r1 R4 — the collapsed repositories rail, one glyph per repository, LIVE mode only (the
328
+ * fixture rail is the prototype's and the comparator reads it). Initials + kind badge; a dot when a
329
+ * session on any of its lines of work needs the PM. Click selects WITHOUT expanding the panel. The
330
+ * active glyph is marked by a left bar and a filled box — shape, not colour alone.
331
+ */
332
+ export function railGlyphs() {
333
+ if (FIXTURE) return [];
334
+ const tuples = repoTuples();
335
+ const product = activeProduct(tuples);
336
+ const sess = Array.isArray(state.sessions) ? state.sessions : [];
337
+ return tuples.map((t) => {
338
+ const rp = repoRow(t);
339
+ const name = rp.name;
340
+ const wss = new Set(treesOf(name).map((x) => x.ws));
341
+ const needs = sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs');
342
+ const active = product === name;
343
+ const ini = (String(name).replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, 2)
344
+ .map((w) => w[0]).join('') || String(name).slice(0, 2)).toUpperCase();
345
+ const said = name + ' — ' + rp.kindLabel + ' repository' + (needs ? ' · a session needs you' : '');
346
+ return el('button', {
347
+ onClick: (e) => { if (e && e.stopPropagation) e.stopPropagation(); selectRepo(name); },
348
+ title: said, 'aria-label': said,
349
+ 'data-r1': 'rail-glyph', 'data-repo': name, 'data-active': active ? '1' : '0',
350
+ 'data-ini': ini, 'data-kind': rp.kindGlyph,
351
+ style: 'position:relative;flex:none;width:28px;height:28px;margin:3px auto;display:flex;align-items:center;justify-content:center;'
352
+ + 'font-family:var(--mono);font-size:10px;font-weight:600;border:1px solid ' + (active ? 'var(--brand)' : 'var(--border)')
353
+ + ';background:' + (active ? 'var(--accent)' : 'transparent') + ';color:var(--foreground);'
354
+ + 'box-shadow:' + (active ? 'inset 3px 0 0 var(--brand)' : 'none'),
355
+ hover: 'background:var(--accent)',
356
+ },
357
+ needs ? el('span', { style: 'position:absolute;right:-3px;top:-3px;width:7px;height:7px;border-radius:50%;background:var(--warning, var(--destructive))', 'data-r1': 'needs-dot' }) : null,
358
+ );
359
+ });
360
+ }
361
+
236
362
  /** setDetail (line 1401) — open panel 3 on a detail kind, un-collapsing it if the PM folded it. */
237
363
  const setDetail = (d) => setState({ detail: d, col2: true });
238
364
 
@@ -293,7 +419,7 @@ export function renderRepos(host) {
293
419
 
294
420
  const scroll = el('div', { style: 'flex:1;overflow-y:auto;min-height:0' },
295
421
  stateHealthBanner(),
296
- tuples.map((t) => repoButton(t, product)),
422
+ tuples.map((t) => repoEntry(t, product)),
297
423
  el('button', {
298
424
  onClick: onAddProduct,
299
425
  style: 'display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;min-height:40px;color:var(--muted-foreground)',
@@ -32,6 +32,7 @@ import * as FX from './fixture.js';
32
32
  import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel-initiatives.js';
33
33
  import { mountDoc, disposeDoc, docText } from './tab-doc.js';
34
34
  import { mountTask } from './tab-task.js';
35
+ import { mountWeb, promptWeb } from './tab-web.js';
35
36
  import { newSessionBlock, openNewSession } from './panel-newsess.js';
36
37
  import { boardOf, currentProduct } from './board-read.js';
37
38
 
@@ -300,6 +301,12 @@ const newSessionBtn = el('button', {
300
301
  onClick: () => newSession(), style: CHIP_STYLE, hover: CHIP_HOVER,
301
302
  }, '+ session');
302
303
 
304
+ /* overhaul-r5 (D69) — open a web page in a tab, by typing its address. Live mode only. */
305
+ const newWebBtn = el('button', {
306
+ onClick: () => promptWeb(), style: CHIP_STYLE, hover: CHIP_HOVER,
307
+ title: 'open a web page in a tab (http/https; it loads only when you open it)', 'data-r5': 'new-web',
308
+ }, '🌐 web');
309
+
303
310
  /* `⋯ options` — the demoted composer, restored (workspace-session-preflight).
304
311
  *
305
312
  * `workspace-repo-first`'s accepted PRD says the six-question composer "remains reachable as *more
@@ -661,6 +668,7 @@ function peerRow(prompt, p) {
661
668
  p.doing ? el('div', {
662
669
  style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.62 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
663
670
  }, iv(p.doing)) : null,
671
+ p.modelLine ? el('div', { 'data-r12': 'model-line', style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.62 0.02 213)' }, iv(p.modelLine)) : null,
664
672
  el('div', { style: 'font-size:11px;color:' + RISK_COLOR[p.risk.level] + ';line-height:1.45' }, iv(p.risk.why)),
665
673
  /* WHY THE END DID NOT HAPPEN, in the row the PM was looking at. A refusal recorded in state
666
674
  * and rendered nowhere would be the same silence in a new place — the whole defect here was a
@@ -679,6 +687,18 @@ function peerRow(prompt, p) {
679
687
  onClick: () => joinSession(prompt.slug, p),
680
688
  style: CH_BTN + ';color:var(--brand)', hover: CHIP_HOVER,
681
689
  }, 'open it'),
690
+ // D75 — switch a RUNNING Claude session's model, on a press: PMOS types the harness's own /model command into the pane.
691
+ (p.agent === 'claude' && p.shown !== 'exited') ? el('button', {
692
+ 'data-r12': 'switch-model', title: 'types /model into this pane; effort is not changed',
693
+ onClick: async () => {
694
+ const t = typeof window !== 'undefined' && window.prompt ? window.prompt('Switch “' + p.name + '” to which tier? fast, balanced or deep', 'balanced') : null;
695
+ if (!t || !t.trim()) return;
696
+ const out = await fetch('/api/session/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: p.id, tier: t.trim() }) })
697
+ .then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
698
+ setState({ endRefusal: out && out.ok ? null : { id: p.id, why: 'model not switched — ' + ((out && out.error) || 'no answer') } });
699
+ },
700
+ style: CH_BTN, hover: CHIP_HOVER,
701
+ }, 'switch model') : null,
682
702
  el('button', {
683
703
  onClick: () => {
684
704
  if (!armed) { setState({ endConfirm: p.id, endRefusal: null }); return; }
@@ -772,6 +792,8 @@ function paneFor(tab) {
772
792
  if (termMod && typeof termMod.mountTerm === 'function') termMod.mountTerm(host, tab);
773
793
  } else if (tab.kind === 'task') {
774
794
  mountTask(host, tab);
795
+ } else if (tab.kind === 'web') {
796
+ mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
775
797
  } else {
776
798
  mountDoc(host, tab);
777
799
  }
@@ -851,6 +873,29 @@ function keepSessionsLive(shown) {
851
873
 
852
874
  /* ── the tab bar ───────────────────────────────────────────────────────────────────────────── */
853
875
 
876
+ /**
877
+ * overhaul-r1 R2.2/R2.3 — the line of work a tab is on, or null when it is the main tree (which
878
+ * shows nothing extra). A doc tab carries its tree from the search hit that opened it; a terminal
879
+ * tab is on whatever tree its session's workspace is. Live mode only; fixture tabs have neither.
880
+ */
881
+ export function tabBranch(t, includeMain) {
882
+ if (FIXTURE || !t || !dataMod) return null;
883
+ let ws = null;
884
+ if (t.kind === 'doc') ws = t.ws || null;
885
+ else if (t.kind === 'term' && t.sessionId) {
886
+ const s = (Array.isArray(state.sessions) ? state.sessions : []).find((x) => x && x.id === t.sessionId);
887
+ ws = s ? s.ws : null;
888
+ }
889
+ if (ws && dataMod.isLinkedTree(ws)) return (t.kind === 'doc' && t.branch) || dataMod.branchOfWs(ws) || 'detached';
890
+ if (!includeMain) return null;
891
+ // the status bar (R2.3) also names the MAIN tree's branch, but only where the repository has other
892
+ // lines of work — in a repository with one tree the branch answers no question anyone asked
893
+ const prod = t.repo || currentProduct();
894
+ const trees = prod && typeof dataMod.productTrees === 'function' ? dataMod.productTrees(prod) : [];
895
+ const main = trees.find((x) => x.main);
896
+ return trees.length > 1 && main ? main.branch : null;
897
+ }
898
+
854
899
  function tabNode(t, act, splitTab) {
855
900
  const selected = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
856
901
  const bg = selected ? FX.TAB_COLORS.activeBg : 'transparent';
@@ -863,7 +908,7 @@ function tabNode(t, act, splitTab) {
863
908
  * fixture render keeps the design's literal — the comparator reads this text node. */
864
909
  const meta = t.kind === 'term'
865
910
  ? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
866
- : (t.kind === 'task' ? 'task' : t.role);
911
+ : (t.kind === 'task' ? 'task' : t.kind === 'web' ? 'web' : t.role);
867
912
 
868
913
  return el('div', {
869
914
  style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
@@ -881,7 +926,7 @@ function tabNode(t, act, splitTab) {
881
926
  }, iv(t.title)),
882
927
  el('span', {
883
928
  style: 'font-family:var(--mono);font-size:9px;color:oklch(0.52 0.02 213);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2',
884
- }, iv(t.slug), ' · ', iv(meta)),
929
+ }, iv(t.slug), ' · ', iv(meta), tabBranch(t) ? el('span', { 'data-r1': 'tab-branch' }, iv(' · ⑂ ' + tabBranch(t))) : null),
885
930
  ),
886
931
  ),
887
932
  el('button', {
@@ -914,6 +959,8 @@ export function renderTabs(host) {
914
959
  setChildren(tabBar, [
915
960
  ...visTabs.map((t) => tabNode(t, act, splitTab)),
916
961
  newSessionBtn,
962
+ FIXTURE ? null : newWebBtn,
963
+ !FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
917
964
  newSessionOptsBtn,
918
965
  canSplit ? splitBtn : null,
919
966
  ]);
@@ -36,6 +36,9 @@ function initial() {
36
36
  userGroups: {}, userKrs: {}, addedInits: {}, userInits: {}, selTask: null, collapsedPhases: {},
37
37
  detail: { kind: 'init' },
38
38
  tabs: [], active: null, splitId: null,
39
+ // overhaul-r1: reader measure per tab (R5), which repositories show their lines of work (R2.1),
40
+ // and the last remove/rename answer, shown on the row it is about (R3.3)
41
+ docNarrow: {}, treesOpen: {}, repoNotice: null,
39
42
  initLinks: {}, artLinks: {}, artSyncOpen: {}, artSyncState: {},
40
43
  /* The sync half of panel 3, all per-window and none of it persisted. `artSyncTask` holds the
41
44
  * task text the server composed for a direction the PM just chose; `linkGone` holds the
@@ -143,7 +146,12 @@ const isFlagMap = (v) =>
143
146
  * id, so a restored terminal re-attaches to exactly the PTY it was showing rather than re-running
144
147
  * the name join — which is what made a reloaded tab land on a session the PM could not identify. */
145
148
  const TAB_FIELDS = ['id', 'kind', 'title', 'slug', 'repo', 'sess', 'sessionId', 'state', 'profile',
146
- 'path', 'role', 'phase', 'status'];
149
+ 'path', 'role', 'phase', 'status',
150
+ // the line of work a doc tab was opened from (overhaul-r1 R1.3/R2.2) — a reload must keep
151
+ // reading the branch-only file from ITS tree, not fall back to the main tree
152
+ 'ws', 'branch',
153
+ // a web tab's address (overhaul-r5, D69) — restored, re-validated, and loaded only on a PM press
154
+ 'url'];
147
155
  const MAX_TABS = 40;
148
156
 
149
157
  function packTab(t) {
@@ -169,6 +177,8 @@ const PERSIST = {
169
177
  sel: isSlug,
170
178
  collapsedGroups: isFlagMap,
171
179
  collapsedPhases: isFlagMap,
180
+ // per doc tab: true = 72ch measure, false = full width; absent = the viewport default (R5.1)
181
+ docNarrow: isFlagMap,
172
182
  tabs: isTabList,
173
183
  active: isSlug,
174
184
  splitId: isSlug,