create-pmos 0.515.0 → 0.517.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 (79) hide show
  1. package/package.json +41 -71
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/payload/state/log.md +1 -1
  7. package/workspace/briefing-premium-calendar-test.js +6 -3
  8. package/workspace/briefing-premium-insights-test.js +2 -2
  9. package/workspace/changelog.json +40 -0
  10. package/workspace/ev-everywhere-test.js +3 -3
  11. package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
  12. package/workspace/guide/agent-capabilities.md +73 -0
  13. package/workspace/guide/board.md +7 -63
  14. package/workspace/guide/breakdown.md +8 -108
  15. package/workspace/guide/briefing-premium.md +5 -526
  16. package/workspace/guide/console-calendar.md +7 -206
  17. package/workspace/guide/docs.md +10 -125
  18. package/workspace/guide/home.md +8 -5
  19. package/workspace/guide/index.md +79 -78
  20. package/workspace/guide/initiatives-and-next-steps.md +6 -19
  21. package/workspace/guide/layout.md +23 -230
  22. package/workspace/guide/live-notes.md +5 -7
  23. package/workspace/guide/my-work.md +8 -172
  24. package/workspace/guide/notes.md +14 -21
  25. package/workspace/guide/people.md +6 -39
  26. package/workspace/guide/report-design.md +2 -2
  27. package/workspace/guide/report-structure.md +1 -1
  28. package/workspace/guide/reports-needs-you.md +34 -0
  29. package/workspace/guide/reports-set-up.md +9 -10
  30. package/workspace/guide/reports.md +17 -1
  31. package/workspace/guide/settings.md +16 -69
  32. package/workspace/guide/today.md +12 -188
  33. package/workspace/lib/report-acts.js +189 -0
  34. package/workspace/lib/report-checks.js +3 -0
  35. package/workspace/lib/report-cli.js +112 -3
  36. package/workspace/lib/report-engine.js +779 -0
  37. package/workspace/lib/report-export.js +36 -1
  38. package/workspace/lib/report-pyramid.js +20 -0
  39. package/workspace/lib/routes-insights-premium.js +17 -3
  40. package/workspace/lib/routes-ready-reports.js +11 -2
  41. package/workspace/lib/routes-report-design.js +7 -2
  42. package/workspace/lib/routes-report-setups.js +38 -3
  43. package/workspace/package-lock.json +2 -2
  44. package/workspace/package.json +1 -1
  45. package/workspace/premium-vw-meet-test.js +9 -5
  46. package/workspace/public/pm/cozy-evui.js +1 -0
  47. package/workspace/public/pm/cozy-insights.css +34 -0
  48. package/workspace/public/pm/cozy-nav.js +4 -1
  49. package/workspace/public/pm/home-app.js +3 -1
  50. package/workspace/public/pm/premium-insights.js +112 -14
  51. package/workspace/public/pm/premium-vw-guide.css +36 -0
  52. package/workspace/public/pm/premium-vw-guide.js +137 -0
  53. package/workspace/public/pm/premium-vw-open.js +4 -1
  54. package/workspace/public/pm/premium-vw-page.js +2 -1
  55. package/workspace/public/premium-intelligence.html +1 -1
  56. package/workspace/run-tests.js +6 -0
  57. package/workspace/skills/report-biweekly.skill +7 -0
  58. package/workspace/skills/report-daily.skill +7 -0
  59. package/workspace/skills/report-monthly.skill +7 -0
  60. package/workspace/skills/report-special.skill +2 -0
  61. package/workspace/skills/report-template.skill +8 -1
  62. package/workspace/skills/report-weekly.skill +7 -0
  63. package/workspace/skills/report-write.skill +44 -0
  64. package/workspace/test-lists/report-engine.json +5 -0
  65. package/workspace/test-lists/reports-needs-you.json +5 -0
  66. package/workspace/guide/ask-the-agent.md +0 -46
  67. package/workspace/guide/briefing-cards.md +0 -52
  68. package/workspace/guide/briefing.md +0 -159
  69. package/workspace/guide/calendar-popup.md +0 -46
  70. package/workspace/guide/command-palette.md +0 -77
  71. package/workspace/guide/console3-sessions.md +0 -147
  72. package/workspace/guide/document-sheet.md +0 -89
  73. package/workspace/guide/initiative-boards.md +0 -123
  74. package/workspace/guide/initiatives-screen.md +0 -179
  75. package/workspace/guide/initiatives-timeline.md +0 -55
  76. package/workspace/guide/phone-companion.md +0 -89
  77. package/workspace/guide/reports-dashboards.md +0 -70
  78. package/workspace/guide/reports-page.md +0 -87
  79. package/workspace/guide/timeline.md +0 -70
@@ -1902,9 +1902,15 @@ async function saveNewDash() {
1902
1902
  let chartDash = null;
1903
1903
  function chartHost(D) { return D && D.saved ? D : savedDash().find((d) => d.id === M.pinned) || savedDash()[0] || null; }
1904
1904
  function openChartEdit(goalId, D0) {
1905
- const D = chartHost(D0 || curDash()); chartDash = D;
1905
+ let D = chartHost(D0 || curDash());
1906
1906
  const g = M.goals.find((x) => x.id === goalId) || M.goals.find((x) => x.key) || M.goals[0];
1907
1907
  if (!g) { toast('There is no goal to chart yet. Add one with New goal.'); return; }
1908
+ // Reports and dashboards redesign R (RgR3Dash): a key result nothing reads opens its connector card, never "no reading yet, make a dashboard first"
1909
+ const kw = !g.readings ? okrKrs().find((k) => k.goal === g.id && k.need && !k.decision) : null;
1910
+ if (kw) { openKr(kw.key); return; }
1911
+ // with no saved dashboard, the chart goes on the one drafted from the OKR file; Save chart keeps that dashboard first
1912
+ if (!D) { const dd = draftedDash(); if (dd && dd.charts.length) D = { id: null, drafted: true, saved: false, name: dd.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: dd.charts, main: true }; }
1913
+ chartDash = D;
1908
1914
  const on = D && D.charts.includes(g.id), cs = (D && (M.chartSet || {})[D.id] || {})[g.id] || {};
1909
1915
  openSide({ kind: 'chart', eye: D ? 'Chart on ' + D.name : 'Chart · no dashboard yet', title: g.title, go: 'Save chart', onGo: saveChart,
1910
1916
  goOff: !D ? 'Make a dashboard first, with New dashboard' : '',
@@ -1962,8 +1968,13 @@ function chartSvg(o) {
1962
1968
  return `<svg class="ins-ch2" viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(o.pts.length + ' readings as a line')}">${tgt}${pv}<polygon class="ar" points="${line[0].split(',')[0]},${H} ${line.join(' ')} ${last[0]},${H}"/>${pj}<polyline class="ln" points="${line.join(' ')}"/>${ship}<circle class="dt" cx="${last[0]}" cy="${last[1]}" r="3.6" style="fill:${TONE_BAR[o.tone] || TONE_BAR.q}"/></svg>`;
1963
1969
  }
1964
1970
  async function saveChart() {
1965
- const D = chartDash, id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
1971
+ let D = chartDash; const id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
1966
1972
  $('#sideGo').disabled = true; m.textContent = 'Saving.';
1973
+ if (D.drafted) {
1974
+ const k = await saveDash({ name: D.name, purpose: D.purpose, charts: D.charts.includes(id) ? D.charts : D.charts.concat(id), main: true });
1975
+ if (!k || k.ok !== true || !k.dashboard) { $('#sideGo').disabled = false; m.textContent = (k && k.error) || 'The dashboard could not be kept. Nothing changed.'; return; }
1976
+ D = Object.assign({}, k.dashboard, { saved: true }); chartDash = D;
1977
+ }
1967
1978
  const res = await pmPost('/api/insights-records/chart', { ws: M.board.ws, board: M.board.id, dash: D.id, goal: id, form: segVal('Form'), range: segVal('Range'), compare: $('#chCmp').value,
1968
1979
  target: $('#chTgt').getAttribute('aria-checked') === 'true', forecast: $('#chFc').getAttribute('aria-checked') === 'true', note: $('#chNote').value.trim(), add: !D.charts.includes(id) });
1969
1980
  $('#sideGo').disabled = false;
@@ -2013,7 +2024,7 @@ function reportGroups() {
2013
2024
  for (const f of R.missing ? [] : R.rows) { if (!by.has(f.cadence)) by.set(f.cadence, []); by.get(f.cadence).push(f); }
2014
2025
  const out = [], shown = (fs) => fs && fs.some((x) => !x.archived);
2015
2026
  for (const s of R.setupsOk === false ? [] : (R.setups || [])) { const fs = by.get(s.id); by.delete(s.id); out.push(shown(fs) ? { kind: 'file', x: { id: s.id, name: s.name, setup: s, files: fs } } : { kind: 'setup', x: s }); }
2016
- for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: id, setup: null, files: fs } });
2027
+ for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: (R.names && R.names[id]) || id, setup: null, files: fs } }); // redesign R: a definition report is named by its title
2017
2028
  return out;
2018
2029
  }
2019
2030
  /** A schedule said short, for the status pill: "Every other Monday at 09:00" is "Every other Monday". */
@@ -2053,31 +2064,118 @@ function drawList() {
2053
2064
  const reps = reportGroups().filter((g) => hit(g.x.name)), dashes = M.dashboards.filter((d) => hit(dashName(d)));
2054
2065
  const nRep = reps.length + asks.length;
2055
2066
  $('#repCap').textContent = String(nRep + dashes.length);
2056
- $('#newRep').href = homeAsk('insights', 'Make a new report: ');
2067
+ $('#newRep').href = homeAsk('insights', 'Make a new report: '); // with no product picked, Home still makes it; with one, + opens the drafted report (below)
2057
2068
  const fbox = found.length ? `<div class="ins-fd" aria-label="Found in your work"><div class="ins-fdh"><b>Found in your work · ${found.length}</b><span class="note">Things you write by hand often</span></div>`
2058
2069
  + found.map((f) => `<div class="ins-fcard" data-f="${esc(f.id)}"><button type="button" class="nm" data-fx="open" aria-label="${esc(f.name)}: what PMOS found"><b>${esc(f.name)}</b><small>${esc(f.why)}</small></button><button type="button" class="ins-btn sm dark" data-fx="setup">Set it up</button><button type="button" class="ins-btn sm ghost" data-fx="dismiss">Not a report</button></div>`).join('') + '</div>' : '';
2059
2070
  const empty = R.missing ? '<div class="ins-empty"><b>The report files could not be read</b>, so none is listed.</div>'
2060
2071
  : R.setupsOk === false ? '<div class="ins-empty"><b>The report setups could not be read</b>, so what you set up is not listed.</div>' : '';
2061
- /* Intelligence redesign A (IgA5Reports): what needs you first (a draft waiting for your ✓, a proposal ready), then the ready-made set folded
2062
- * (its switches stay under the fold), then Yours, then the dashboards. */
2063
- const waits = reps.filter((G) => repState(G).draft), waitAsks = asks.filter((a) => a.state === 'ready');
2072
+ /* Reports and dashboards redesign R (RgR1Panel; A's IgA5Reports): Needs you first, each waiting report with one line of its headline and its one ✓
2073
+ * (Issue · Confirm the definition · Move to <product> · Resume, lib/report-acts.js), then Drafting on their moment and Weekly updates (the
2074
+ * ready-made switches, folded), then Yours, then the dashboards. Needs you is first on a phone too, before the one-product note. */
2075
+ const acts = R.acts || {};
2076
+ const waits = reps.filter((G) => acts[G.x.id]), waitAsks = asks.filter((a) => a.state === 'ready');
2064
2077
  const rest = reps.filter((G) => !waits.includes(G)), restAsks = asks.filter((a) => !waitAsks.includes(a));
2065
2078
  const head3 = '<div class="ins-rl ins-rlh" role="presentation"><span>Report</span><span>Next</span><span>Status</span></div>';
2066
- const needs = waits.length + waitAsks.length ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${waits.length + waitAsks.length}</span></div><div class="ins-rtbl" id="repNeeds">${head3}<div role="list">${waits.map(repRowHtml).join('')}${waitAsks.map(askRowHtml).join('')}</div></div>` : '';
2079
+ const nNeed = waits.length + waitAsks.length;
2080
+ const needs = nNeed ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${nNeed}</span></div><div class="ins-nlist" id="repNeeds" role="list">${waits.map(needRowHtml).join('')}${waitAsks.map(askNeedHtml).join('')}</div>` : '';
2067
2081
  const table = rest.length + restAsks.length ? `<div class="ins-sec"><h3>Yours</h3><span class="q">${rest.length + restAsks.length}</span></div><div class="ins-rtbl" id="repAsks">${head3}<div role="list">${rest.map(repRowHtml).join('')}${restAsks.map(askRowHtml).join('')}</div></div>`
2068
2082
  : !empty && !needs ? '<div class="ins-empty"><b>No report is set up and no report file is on record.</b> Reports are made in Home.</div>' : '';
2069
- const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length ? dashes.length + ' · a dashboard is a saved view of Goals' : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dashes.map(dashCard).join('')}</div>`;
2070
- const lib = RL.libHtml(LIB.L, { open: LIB.open, yours: false });
2071
- $('#repBody').innerHTML = `${oneNote()}${needs}<details class="ins-libfold"${LIB.fold ? ' open' : ''}><summary><b>Drafting on their moment</b> <span class="note">ready-made reports, nothing to set up</span></summary><div id="readyLib">${lib}</div></details><div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
2072
- const fd = $('#repBody .ins-libfold'); if (fd) fd.addEventListener('toggle', () => { LIB.fold = fd.open; });
2083
+ const dd = savedDash().length ? null : draftedDash();
2084
+ const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length && !dd ? dashes.length + ' · a dashboard is a saved view of Goals' : dd ? '1 drafted · ' + esc(dd.name) : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dd ? draftedCard(dd) : ''}${dashes.map(dashCard).join('')}</div>`;
2085
+ $('#repBody').innerHTML = `${needs}${oneNote()}${libFolds(LIB.L) || '<div id="readyLib"></div>'}<div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
2086
+ wireFolds();
2073
2087
  if (!LIB.read && M.board && M.board.ws) { LIB.read = true; RL.readLib(M.board.ws).then((L) => { LIB.L = L; if (!$('#libSide').hidden) drawLib(); }); }
2074
2088
  }
2089
+ const wireFolds = () => { for (const fd of $$('#repBody .ins-libfold')) fd.addEventListener('toggle', () => { LIB.fold[fd.dataset.fold] = fd.open; }); };
2090
+ /** The ready-made switches as two folds (RgR1Panel): the moment reports (up to four peek lines: what each drafts from), then the weekly updates
2091
+ * (one line: how many are on and off). The switches themselves are inside, unchanged (pm/ready-lib.js). Zero is silence: an empty fold is not drawn. */
2092
+ function libFolds(L) {
2093
+ if (!L || !(L.groups || []).length) return '';
2094
+ const onlyKind = (weekly) => Object.assign({}, L, { lead: weekly ? L.lead : null, overlap: weekly ? L.overlap : null,
2095
+ groups: L.groups.map((g) => Object.assign({}, g, { rows: (g.rows || []).filter((r) => (r.kind === 'weekly') === weekly), more: weekly ? g.more : null }))
2096
+ .filter((g) => g.rows.length || (weekly && g.more && g.more.count)) });
2097
+ const all = L.groups.flatMap((g) => (g.rows || []).concat((g.more && g.more.rows) || []).map((r) => Object.assign({ level: g.level, gname: g.name }, r)));
2098
+ const moments = all.filter((r) => r.kind !== 'weekly' && r.on), wk = all.filter((r) => r.kind === 'weekly'), wkOn = wk.filter((r) => r.on).length;
2099
+ const peek = moments.slice(0, 4).map((r) => `<li><b>${esc(r.name)}</b>${r.level === 'initiative' ? '' : ' · ' + esc(r.gname)}${r.sub ? ` <span class="note">${esc(r.sub)}</span>` : ''}</li>`).join('');
2100
+ const mHtml = RL.libHtml(onlyKind(false), { open: LIB.open, yours: false }), wHtml = RL.libHtml(onlyKind(true), { open: LIB.open, yours: false });
2101
+ const foldBox = (k, title, sub, pk, body) => (body ? `<details class="ins-libfold" data-fold="${k}"${LIB.fold[k] ? ' open' : ''}><summary><b>${esc(title)}</b> <span class="note">${esc(sub)}</span></summary>${pk}<div class="ins-libin">${body}</div></details>` : '');
2102
+ return `<div id="readyLib">${foldBox('moment', 'Drafting on their moment', 'nothing to set up', peek ? `<ul class="ins-mlist">${peek}</ul>` : '', mHtml)}`
2103
+ + `${foldBox('weekly', 'Weekly updates', wk.length ? (wkOn ? wkOn + ' on' : 'none on') + (wk.length - wkOn ? ' · ' + (wk.length - wkOn) + ' off' : '') : '', '', wHtml)}</div>`;
2104
+ }
2105
+ /** A waiting report (RgR1Panel): its name, when it was drafted and for whom, one line of its headline, and its one ✓. */
2106
+ function needRowHtml(G) {
2107
+ const st = repState(G), a = (M.reports.acts || {})[G.x.id], s = st.s, d = st.draft;
2108
+ const about = (M.reports.about || {})[G.x.id];
2109
+ const sub = [d ? 'Drafted ' + md(day(d.slot)) : s && s.paused ? 'Paused' : '', s && s.audience ? 'for ' + s.audience : '', !s && a && a.act === 'confirm' ? 'its definition waits for your word' : '',
2110
+ s && s.next && !s.paused ? 'next ' + s.next : '', about ? 'in ' + (M.board.name || 'this product') + ', about ' + about.name : ''].filter(Boolean).join(' · ');
2111
+ const lead = d && d.lead ? d.lead : s && s.latest && s.latest.lead ? s.latest.lead : '';
2112
+ const key = G.kind + ':' + G.x.id;
2113
+ return `<div class="ins-nrow${selRow === key ? ' sel' : ''}" role="listitem"><button type="button" class="nm" ${G.kind === 'setup' ? `data-setup="${esc(G.x.id)}"` : `data-rpt="${esc(G.x.id)}"`}><b>${esc(G.x.name)}</b><small>${esc(sub)}</small>${lead ? `<span class="hl">${esc(lead)}</span>` : ''}</button>`
2114
+ + `<span class="ac"><span class="note">${esc(a.label.charAt(0).toLowerCase() + a.label.slice(1))}</span><button type="button" class="cz-ib sm solid" data-nact="${esc(G.x.id)}" aria-label="${esc(a.label + ': ' + G.x.name)}" title="${esc(a.label)}">${SV(I.chk, 2.4)}</button></span></div>`;
2115
+ }
2116
+ /** A proposal from Home that is ready: what was asked, and its ✓, which opens the proposal (Set it up is one press there). */
2117
+ const askNeedHtml = (a) => `<div class="ins-nrow" role="listitem"><button type="button" class="nm" data-ask="${esc(a.id)}"><b>${esc(a.proposal && a.proposal.name ? a.proposal.name : a.text)}</b><small>${esc('You asked in Home: ' + a.text)}</small></button>`
2118
+ + `<span class="ac"><span class="note">set it up</span><button type="button" class="cz-ib sm solid" data-ask="${esc(a.id)}" aria-label="${esc('Open the proposal: ' + a.text)}" title="Open the proposal">${SV(I.chk, 2.4)}</button></span></div>`;
2119
+ /** The one ✓ of a waiting report: Issue, Confirm the definition, Move to <product> or Resume. One press, then the panel reads again. */
2120
+ async function needPress(b) {
2121
+ const id = b.dataset.nact, a = (M.reports.acts || {})[id]; if (!a) return;
2122
+ const G = reportGroups().find((x) => x.x.id === id), name = G ? G.x.name : id;
2123
+ b.disabled = true;
2124
+ const r = a.act === 'issue' ? (a.sha ? await pmPost('/api/report/issue', { ws: M.board.ws, cadence: id, slot: a.slot, sha: a.sha }) : { ok: false, error: 'The draft could not be read, so it was not issued. Open it and issue it from the reader.' })
2125
+ : a.act === 'confirm' ? await pmPost('/api/report-setups/confirm', { ws: M.board.ws, id })
2126
+ : a.act === 'move' ? await pmPost('/api/report-setups/move', { ws: M.board.ws, id, to: a.to })
2127
+ : await pmPost('/api/report-setups/resume', { ws: M.board.ws, id });
2128
+ if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'Nothing changed. The server did not say why.'); return; }
2129
+ toast(a.act === 'issue' ? 'Issued ' + name + '. Send it from your own mail.' : a.act === 'confirm' ? 'Confirmed the definition of ' + name + '. Its drafts follow it as it is.'
2130
+ : a.act === 'move' ? 'Moved ' + name + ' to ' + a.toName + '. It drafts there from now on.' : 'Resumed ' + name + '.');
2131
+ await load();
2132
+ }
2133
+
2134
+ /* ── the first dashboard, drafted from the OKR file (RgR3Dash) ──────────────────────────────────────────────────────────────────────
2135
+ * With no saved dashboard, the panel offers one tile per key result of the OKR file (A's model.okrFile): a key result with a reader shows its
2136
+ * reading with the reader and its date; one without shows what it needs and the connector ✓. Nothing is a number without a reading. ✓ Keep saves
2137
+ * it as a dashboard of the read key results (POST /api/premium-dashboards/save); the waiting ones join it once a reader reads them. */
2138
+ function draftedDash() {
2139
+ const F = M.okrFile; if (!F || !F.total) return null;
2140
+ const tiles = F.objectives.flatMap((o) => o.krs.map((k) => {
2141
+ const g = k.goal ? M.goals.find((x) => x.id === k.goal) : null, rd = g && g.reading;
2142
+ if (k.read && g) return { k, read: true, big: g.nowTxt || (rd && rd.txt) || '—', dot: rd && rd.dot === 'behind' ? 'risk' : rd && rd.dot === 'broken' ? 'late' : 'ok', meta: [rd && rd.source ? rd.source : 'its reader', rd && rd.at ? md(day(rd.at)) : ''].filter(Boolean).join(' · ') };
2143
+ if (k.decision) return { k, read: false, big: 'Not yet', dot: 'none', meta: k.due ? 'a decision, due ' + md(day(k.due)) : 'a decision' };
2144
+ return { k, read: false, big: '—', dot: 'none', meta: k.need ? 'needs ' + k.need : 'no source named', need: k.need || '' };
2145
+ }));
2146
+ return { name: (F.period ? F.period.label + ' ' : '') + 'key results', tiles, charts: tiles.filter((t) => t.read).map((t) => t.k.goal), nRead: tiles.filter((t) => t.read).length };
2147
+ }
2148
+ const okrTileHtml = (t) => `<div class="ins-dtile${t.read ? '' : ' w'}" role="listitem"><span class="lb"><span class="kr">${esc(t.k.token)}</span>${esc(t.k.title)}</span><b class="big">${esc(t.big)}</b>`
2149
+ + `<span class="ft"><span class="note"><span class="ins-tdot ${t.dot}" aria-hidden="true"></span>${esc(t.meta)}</span>${t.need ? `<button type="button" class="cz-ib sm" data-ddash="con" aria-label="${esc('Connect ' + t.need)}" title="${esc('Connect ' + t.need)}">${SV(I.chk, 2.4)}</button>` : ''}</span></div>`;
2150
+ function draftedCard(D) {
2151
+ return `<div class="ins-dcard ins-ddraft" role="listitem"><span class="ins-dh"><b>${esc(D.name)}</b><span class="cz-pill q">Drafted</span></span>`
2152
+ + `<p class="note">${esc('I drafted it from your OKR file: one tile per key result. ' + (D.nRead ? D.nRead + ' read' + (D.tiles.length - D.nRead ? ', ' + (D.tiles.length - D.nRead) + ' wait on a source, and each tile says which.' : '.') : 'None has a reading yet, and each tile says what it needs.'))}</p>`
2153
+ + `<div class="ins-dtiles" role="list">${D.tiles.slice(0, 8).map(okrTileHtml).join('')}</div>`
2154
+ + `<span class="ins-dact">${D.nRead ? `<span class="note">keep it, and it opens first</span><button type="button" class="cz-ib sm solid" data-ddash="keep" aria-label="Keep this dashboard" title="Keep this dashboard">${SV(I.chk, 2.4)}</button>` : '<span class="note">kept once one key result has a reading</span>'}</span></div>`;
2155
+ }
2156
+ async function draftedPress(b) {
2157
+ if (b.dataset.ddash === 'con') { import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); return; }
2158
+ const D = draftedDash(); if (!D || !D.charts.length) return;
2159
+ b.disabled = true;
2160
+ const r = await saveDash({ name: D.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: D.charts, main: true });
2161
+ if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'The dashboard could not be saved. Nothing changed.'); return; }
2162
+ toast('Kept ' + D.name + '. It opens first.'); await load();
2163
+ }
2075
2164
  /* the ready-made group before Yours (batch 17 item 1, PbA6): read once per open of the panel, again after a switch */
2076
- const LIB = { L: null, read: false, open: new Set(), fold: false };
2077
- const drawLib = () => { const el = $('#readyLib'); if (el) el.innerHTML = RL.libHtml(LIB.L, { open: LIB.open, yours: false }); };
2165
+ const LIB = { L: null, read: false, open: new Set(), fold: {} };
2166
+ const drawLib = () => { const el = $('#readyLib'); if (el) { el.outerHTML = libFolds(LIB.L) || '<div id="readyLib"></div>'; wireFolds(); } };
2167
+ /* + in the panel header (RgR4Make): the drafted report of this view, never a blank chat. With all products on screen there is no one product to
2168
+ * draft for, so it keeps its link to Home. */
2169
+ $('#newRep').addEventListener('click', (e) => {
2170
+ if (PF || !M || !M.board || !M.board.ws) return;
2171
+ e.preventDefault();
2172
+ const D = curDash(); RI.openDraft(D && D.saved ? 'dashboard:' + D.id : 'view');
2173
+ });
2078
2174
  RL.wireLib($('#repBody'), { get ws() { return M && M.board ? M.board.ws : ''; }, open: LIB.open, toast: (t) => toast(t), redraw: drawLib,
2079
2175
  reload: async () => { LIB.L = await RL.readLib(M.board.ws); drawLib(); } });
2080
2176
  $('#repBody').addEventListener('click', async (e) => {
2177
+ const na = e.target.closest('[data-nact]'); if (na) { if (!na.disabled) needPress(na); return; }
2178
+ const dk = e.target.closest('[data-ddash]'); if (dk) { draftedPress(dk); return; }
2081
2179
  const d = e.target.closest('[data-d]'); if (d) { setView(d.dataset.d); return; }
2082
2180
  const fx = e.target.closest('[data-fx]');
2083
2181
  if (fx) {
@@ -0,0 +1,36 @@
1
+ /* premium-vw-guide.css — the guide in the Viewer (premium-vw-guide.js; canvas Exploring › User guide, option B): a page rail on the left,
2
+ * the page on the right with its section chips. Under 720 px the rail hides and the list icon shows it over the page. Quiet: hairlines, one
3
+ * well for the open page, no colour but the chips' hover. */
4
+ .vwdoc.gd{padding:0}
5
+ .gd-wrap{container-type:inline-size;display:flex;align-items:flex-start;gap:20px;position:relative}
6
+ .gd-rail{width:248px;flex:none;position:sticky;top:0;max-height:calc(100vh - 140px);overflow:auto;padding:4px 4px 16px 0;display:flex;flex-direction:column;gap:2px}
7
+ .gd-read{flex:1;min-width:0}
8
+ .gd-tog{display:none}
9
+ .gd-find{display:flex;align-items:center;gap:8px;height:40px;padding:0 10px;margin:0 0 6px;border-radius:10px;background:var(--well);color:var(--ink-3)}
10
+ .gd-find .i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;flex:none}
11
+ .gd-find input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font:var(--fw-regular) var(--fs-body)/var(--lh-body) var(--sans);outline:none;box-shadow:none}
12
+ .gd-find input:focus,.gd-find input:focus-visible{outline:none}
13
+ .gd-find:focus-within{outline:2px solid var(--ember);outline-offset:1px}
14
+ .gd-grp summary{list-style:none;cursor:pointer;padding:8px 8px 4px;color:var(--ink-3);font:var(--fw-strong) var(--fs-meta)/var(--lh-meta) var(--sans);border-radius:6px}
15
+ .gd-grp summary::-webkit-details-marker{display:none}
16
+ .gd-grp summary::after{content:"";display:inline-block;width:6px;height:6px;margin-left:8px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-2px) rotate(45deg)}
17
+ .gd-grp:not([open]) summary::after{transform:translateY(0) rotate(-45deg)}
18
+ .gd-grp summary:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
19
+ .gd-grp ul{list-style:none;margin:0;padding:0}
20
+ .gd-grp a{display:block;padding:5px 8px;border-radius:6px;color:var(--ink-2);text-decoration:none;font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
21
+ .gd-grp a:hover{background:var(--well);color:var(--ink)}
22
+ .gd-grp a[aria-current="page"]{background:var(--well);color:var(--ink);font-weight:var(--fw-strong)}
23
+ .gd-grp a:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
24
+ .gd-rail [hidden]{display:none}
25
+ .gd-none{margin:8px;color:var(--ink-3);font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
26
+ .gd-secs{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}
27
+ .gd-chip{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;border-radius:999px;background:var(--well);color:var(--ink-2);text-decoration:none;font:var(--fw-regular) var(--fs-meta)/var(--lh-meta) var(--sans)}
28
+ .gd-chip:hover{color:var(--ink)}
29
+ .gd-chip[hidden]{display:none}
30
+ .gd-more{border:0;cursor:pointer;color:var(--ink-3)}
31
+ .gd-chip:focus-visible{outline:2px solid var(--ember);outline-offset:1px}
32
+ @container (max-width: 720px){
33
+ .gd-rail{display:none}
34
+ .gd-tog{display:flex}
35
+ .gd-railon .gd-rail{display:flex;position:absolute;z-index:3;top:64px;left:0;right:0;width:auto;max-height:70vh;padding:12px;border:1px solid var(--hair);border-radius:14px;background:var(--card-solid);box-shadow:var(--card-shadow)}
36
+ }
@@ -0,0 +1,137 @@
1
+ /* pm/premium-vw-guide.js — the user guide, read in the Viewer (initiative guide-in-app; canvas Exploring › User guide, option B, Wawan 2026-11-03).
2
+ *
3
+ * The guide ships with the app (workspace/guide/*.md, GET /api/guide). The menu's Guide icon (cozy-nav.js) opens its start page here, and
4
+ * /premium-viewer.html?open=guide&page=<name> opens any page on its own. The guide opens wide (the Viewer's wide mode) unless the PM set a width
5
+ * for it before.
6
+ * Rail every page, grouped by Start here's own headings, with Find a page on top; the open page is marked; a group folds and stays folded.
7
+ * Page the title, then one chip per section (a press scrolls there), then the text. A link to another guide page opens that page in the same
8
+ * Viewer card as a drill-in, so ← comes back. A link to a repository file is drawn as text.
9
+ * Narrow under 720 px (a phone, or a narrow Viewer) the rail hides; the list icon beside the title shows it over the page.
10
+ * Nothing here writes, except the folded groups, kept in this browser.
11
+ * openGuide(page = 'index') -> the pane
12
+ * guideHtml(name, text, groups) -> the page as html (pure)
13
+ * guideLinks(md) -> the markdown with each link to a guide page rewritten to #guide:<name> (pure)
14
+ * contentsOf(indexMd) -> [{ group, pages: [{ page, title }] }] from Start here's headings and links (pure) */
15
+ import { vwOpen, vwStyle } from './premium-viewer.js';
16
+ import { drill, wide, widthOf } from './cozy-viewer.js';
17
+ import { bodyHtml, pageHead, splitDoc, readMinutes } from './premium-vw-doc-model.js';
18
+
19
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
20
+ const NAME = /^[a-z0-9][a-z0-9-]*$/;
21
+ const TOC_ICON = '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01"/></svg>';
22
+ const FIND_ICON = '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4a7 7 0 1 0 0 14a7 7 0 1 0 0-14M20 20l-4-4"/></svg>';
23
+ const FOLD_KEY = 'pmos.guide.folded';
24
+ const CHIPS = 6;
25
+ let TOC = null; // the groups, read once from Start here
26
+
27
+ const store = { get() { try { return JSON.parse(localStorage.getItem(FOLD_KEY) || '[]'); } catch { return []; } }, set(v) { try { localStorage.setItem(FOLD_KEY, JSON.stringify(v)); } catch { /* a private window */ } } };
28
+
29
+ /** Start here's groups: each ## or ### heading from "Read next" on, with the guide pages its list links, in order. A page linked twice keeps its first place. */
30
+ export function contentsOf(md) {
31
+ const out = [], seen = new Set(['index']); let cur = null, on = false;
32
+ for (const l of String(md || '').replace(/\r/g, '').split('\n')) {
33
+ const h = /^#{2,3}\s+(.*)$/.exec(l);
34
+ if (h && /^Read next$/i.test(h[1].trim())) { on = true; cur = null; continue; }
35
+ if (h && on) { cur = { group: h[1].trim(), pages: [] }; out.push(cur); continue; }
36
+ if (!cur) continue;
37
+ for (const m of l.matchAll(/\[([^\]]+)\]\(([a-z0-9][a-z0-9-]*)\.md\)/g)) if (!seen.has(m[2])) { seen.add(m[2]); cur.pages.push({ page: m[2], title: m[1] }); }
38
+ }
39
+ return [{ group: 'Start here', pages: [{ page: 'index', title: 'Start here' }] }, ...out.filter((g) => g.pages.length)];
40
+ }
41
+
42
+ /** The rail: Find a page, then every page by group, the open one marked. folded: the group names kept folded. */
43
+ export function railHtml(name, groups, folded = []) {
44
+ return '<nav class="gd-rail" aria-label="Guide pages">'
45
+ + `<div class="gd-find">${FIND_ICON}<input type="search" placeholder="Find a page" aria-label="Find a page" autocomplete="off" data-gd-find></div>`
46
+ + (groups || []).map((g) => {
47
+ const here = g.pages.some((x) => x.page === name), open = here || !folded.includes(g.group);
48
+ return `<details class="gd-grp" data-gd-grp="${esc(g.group)}"${open ? ' open' : ''}><summary>${esc(g.group)}</summary><ul>`
49
+ + g.pages.map((x) => `<li><a href="#guide:${esc(x.page)}"${x.page === name ? ' aria-current="page"' : ''} data-gd-t="${esc(x.title.toLowerCase())}">${esc(x.title)}</a></li>`).join('')
50
+ + '</ul></details>';
51
+ }).join('')
52
+ + '<p class="gd-none" data-gd-none hidden>No page has that name.</p></nav>';
53
+ }
54
+
55
+ /** Each markdown link to a guide page (`[x](layout.md)`, `[x](layout.md#keys)`) becomes `[x](#guide:layout)`, which the page model keeps as a link. */
56
+ export const guideLinks = (md) => String(md || '').replace(/\]\(([a-z0-9][a-z0-9-]*)\.md(?:#[^)\s]*)?\)/g, '](#guide:$1)');
57
+
58
+ /** One guide page: the rail, then the page with its title, its section chips, the text and a quiet footer. */
59
+ export function guideHtml(name, text, groups = TOC, folded = []) {
60
+ const t = guideLinks(text), h = pageHead(t), sd = splitDoc(t);
61
+ const b = bodyHtml([sd.pre, ...sd.sections.map((s, n) => (n === 0 && h.h1 ? s.body : s.head + '\n' + s.body))].join('\n'));
62
+ const secs = b.heads.filter((x) => x[0] === 2);
63
+ // At most 6 chips show; the rest wait behind "N more", so a long page does not open on a wall of chips.
64
+ const chip = ([, s, id], n) => `<a href="#${esc(id)}" class="gd-chip" data-gd-jump="${esc(id)}"${n >= CHIPS ? ' hidden' : ''}>${esc(s)}</a>`;
65
+ const more = secs.length > CHIPS ? `<button type="button" class="gd-chip gd-more" data-gd-more>${secs.length - CHIPS} more</button>` : '';
66
+ const chips = secs.length > 1 ? `<div class="gd-secs" role="group" aria-label="On this page">${secs.map(chip).join('')}${more}</div>` : '';
67
+ const home = name === 'index' ? '' : '<span class="gap"></span><a href="#guide:index">Start here</a>';
68
+ return `<div class="vwdoc gd"><div class="gd-wrap">${railHtml(name, groups || [], folded)}`
69
+ + `<article class="cf vwfile gd-read"><header class="cfh vwfh"><h1 class="cft">${esc(h.title || name)}</h1>`
70
+ + `<div class="vwacts gd-tog"><button type="button" class="vwib" data-gd-toc aria-label="Guide pages" title="Guide pages" aria-expanded="false">${TOC_ICON}</button></div></header>`
71
+ + chips
72
+ + `<div class="cfb">${b.html || '<p class="vwmut">This page has no text yet.</p>'}</div>`
73
+ + `<footer class="cff"><span>Guide · ${readMinutes(text)} min read</span>${home}</footer></article></div></div>`;
74
+ }
75
+
76
+ const failHtml = (why) => `<div class="vwdoc"><div class="vwfnone" role="status"><b>The guide page could not be shown</b><p class="vwmut">${esc(why)}</p></div></div>`;
77
+
78
+ async function read(name) {
79
+ try {
80
+ const r = await fetch('/api/guide?page=' + encodeURIComponent(name), { credentials: 'same-origin' });
81
+ return r.ok ? { ok: true, text: await r.text() } : { ok: false, why: 'There is no guide page called ' + name + '.' };
82
+ } catch { return { ok: false, why: 'The workspace server did not answer.' }; }
83
+ }
84
+
85
+ function wire(el) {
86
+ const wrap = () => el.querySelector('.gd-wrap'), btn = () => el.querySelector('[data-gd-toc]');
87
+ const show = (on) => { const w = wrap(), b = btn(); if (!w || !b) return; w.classList.toggle('gd-railon', on); b.setAttribute('aria-expanded', String(on)); if (on) { const f = w.querySelector('[data-gd-find]'); if (f) f.focus(); } };
88
+ el.addEventListener('input', (e) => {
89
+ if (!e.target.matches('[data-gd-find]')) return;
90
+ const q = e.target.value.trim().toLowerCase(), r = el.querySelector('.gd-rail'); let any = false;
91
+ r.querySelectorAll('details.gd-grp').forEach((d) => {
92
+ let n = 0; d.querySelectorAll('li').forEach((li) => { const hit = !q || li.firstChild.dataset.gdT.includes(q); li.hidden = !hit; if (hit) n++; });
93
+ d.hidden = !n; if (q && n) d.open = true; if (n) any = true;
94
+ });
95
+ r.querySelector('[data-gd-none]').hidden = any;
96
+ });
97
+ el.addEventListener('toggle', (e) => {
98
+ const d = e.target; if (!d.matches || !d.matches('details.gd-grp') || el.querySelector('[data-gd-find]').value) return;
99
+ const k = d.dataset.gdGrp, f = store.get().filter((x) => x !== k);
100
+ if (!d.open) f.push(k);
101
+ store.set(f);
102
+ }, true);
103
+ el.addEventListener('keydown', (e) => { if (e.key === 'Escape' && wrap() && wrap().classList.contains('gd-railon')) { e.stopPropagation(); show(false); btn().focus(); } });
104
+ el.addEventListener('click', (e) => {
105
+ if (e.target.closest('[data-gd-toc]')) { show(!wrap().classList.contains('gd-railon')); return; }
106
+ const m = e.target.closest('[data-gd-more]');
107
+ if (m) { m.parentElement.querySelectorAll('.gd-chip[hidden]').forEach((c) => { c.hidden = false; }); m.remove(); return; }
108
+ const j = e.target.closest('[data-gd-jump]');
109
+ if (j) { e.preventDefault(); const h = el.querySelector('#' + CSS.escape(j.dataset.gdJump)); if (h) h.scrollIntoView({ block: 'start', behavior: 'smooth' }); return; }
110
+ const a = e.target.closest('a'); if (!a) return;
111
+ const href = a.getAttribute('href') || '';
112
+ const g = /^#guide:([a-z0-9-]+)$/.exec(href);
113
+ if (g) { e.preventDefault(); drill(); openGuide(g[1]); return; }
114
+ if (href === '#') e.preventDefault(); // a link to a repository file: the page model drew it without a target
115
+ });
116
+ }
117
+
118
+ /** Open a guide page in the Viewer. */
119
+ export function openGuide(page = 'index') {
120
+ const name = NAME.test(String(page || '')) ? String(page) : 'index';
121
+ vwStyle('doc'); vwStyle('guide');
122
+ const id = 'guide:' + name;
123
+ let pane = null, done = null;
124
+ pane = vwOpen({ id, kind: 'Guide', title: 'Guide', icon: 'doc', url: '/premium-viewer.html?open=guide&page=' + name,
125
+ render: (el) => { el.innerHTML = done || '<div class="vwdoc"><p class="vwmut" role="status">Opening the guide.</p></div>'; wire(el); } });
126
+ try { if (!widthOf('Guide')) wide(true); } catch { /* a page with no cozy Viewer */ }
127
+ const toc = TOC ? Promise.resolve() : read('index').then((r) => { if (r.ok) TOC = contentsOf(r.text); });
128
+ Promise.all([read(name), toc]).then(([r]) => {
129
+ done = r.ok ? guideHtml(name, r.text, TOC, store.get()) : failHtml(r.why);
130
+ const el = document.querySelector(`.obody.vw[data-vw="${CSS.escape(id)}"]`) || pane;
131
+ if (el && el.isConnected) {
132
+ el.innerHTML = done; el.scrollTop = 0;
133
+ const cur = el.querySelector('.gd-rail [aria-current="page"]'); if (cur) cur.scrollIntoView({ block: 'nearest' });
134
+ }
135
+ });
136
+ return pane;
137
+ }
@@ -8,6 +8,7 @@
8
8
  * openReport(idOrRow, { ws, name, cycles, onIssue, onChange }) a report path reports/<cadence>/<date>.md or a Reports card row
9
9
  * newNote(ws | { ws }) starts an untitled note now and opens it, recording
10
10
  * makeWorkItem(noteId, step) Becomes V.4: a ticket drafted in PMOS from a next step (never pushed to Jira here)
11
+ * openGuide(page) a page of the user guide (workspace/guide/<page>.md); no page: Start here
11
12
  * The modules load on first use (dynamic import), so this file has no import cycle with the view modules that import it.
12
13
  * It also adds the Viewer's style sheet (cozy-vw.css) to the page once. */
13
14
 
@@ -19,6 +20,7 @@ export function vzStyle() {
19
20
  const mtg = () => import('./premium-vw-mtg.js');
20
21
  const cal = () => import('./premium-vw-cal.js');
21
22
  const rep = () => import('./premium-report-read.js');
23
+ const gd = () => import('./premium-vw-guide.js');
22
24
  const plain = (o) => (o && typeof o === 'object' && !o.openDlg && !o.$ ? o : {}); // an old caller passed the shell's deps
23
25
 
24
26
  export async function openMeeting(ref, o = {}) { vzStyle(); return (await mtg()).openMeeting(ref, plain(o)); }
@@ -27,8 +29,9 @@ export async function openNotes(o = {}) { vzStyle(); return (await cal()).openNo
27
29
  export async function openReport(ref, o = {}) { vzStyle(); return (await rep()).openReportView(ref, plain(o)); }
28
30
  export async function newNote(o) { vzStyle(); return (await mtg()).newNote(typeof o === 'string' ? o : (o && o.ws) || ''); }
29
31
  export async function makeWorkItem(noteId, step) { return (await mtg()).workItem(noteId, step); }
32
+ export async function openGuide(page) { vzStyle(); return (await gd()).openGuide(page); }
30
33
 
31
- export const views = { openMeeting, openCalendar, openNotes, openReport, newNote, makeWorkItem };
34
+ export const views = { openMeeting, openCalendar, openNotes, openReport, newNote, makeWorkItem, openGuide };
32
35
  if (typeof window !== 'undefined') {
33
36
  window.czViews = Object.assign(window.czViews || {}, views);
34
37
  window.pmViewer = window.czViews;
@@ -1,6 +1,6 @@
1
1
  /* pm/premium-vw-page.js — /premium-viewer.html: opens the item its address names in the Viewer card (initiative premium-agreed-design).
2
2
  * ?open=meeting&note=<id> | &event=<id>&date=YYYY-MM-DD ?open=calendar[&tab=notes][&date=] ?open=notes[&initiative=]
3
- * ?open=report&path=reports/<cadence>/<date>.md each with an optional &ws=
3
+ * ?open=report&path=reports/<cadence>/<date>.md ?open=guide[&page=<name>] each with an optional &ws=
4
4
  * Closing the card leaves a line that links to Home. An address that names nothing says so. */
5
5
  import { views } from './premium-vw-open.js';
6
6
  import { onChange } from './cozy-viewer.js';
@@ -13,4 +13,5 @@ if (what === 'meeting') views.openMeeting(q.get('note') ? { note: q.get('note')
13
13
  else if (what === 'calendar') views.openCalendar({ ws, tab: q.get('tab') === 'notes' ? 'notes' : 'cal', date: q.get('date') || '' });
14
14
  else if (what === 'notes') views.openNotes({ ws, initiative: q.get('initiative') || '' });
15
15
  else if (what === 'report' && q.get('path')) views.openReport(q.get('path'), { ws });
16
+ else if (what === 'guide') views.openGuide(q.get('page') || 'index');
16
17
  else gone.hidden = false;
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- Reports and dashboards (⋯; the old Library): Found in your work, requests from Home, the reports, then the dashboards -->
70
70
  <aside class="ins-side ins-lib" id="libSide" role="dialog" aria-modal="false" aria-labelledby="libTitle" tabindex="-1" hidden>
71
- <div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report (opens Home)" title="New report (opens Home)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
71
+ <div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report: Claude drafts it from this view" title="New report: Claude drafts it from this view"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
72
72
  <div class="ins-sb" id="repBody"></div>
73
73
  <span class="vh" id="repCap"></span>
74
74
  </aside>
@@ -498,6 +498,12 @@ function traceChanges(changes, ctx) {
498
498
  }
499
499
  if (/^(guide|docs)\/|\.md$/.test(f)) continue; // prose: no suite asserts on it except guide.test.js, a unit suite
500
500
  if (f === 'test-timings.json') continue; // run order only: no suite reads it
501
+ if (/^skills\/[^/]+\.skill$/.test(f) && get(f) != null) {
502
+ // a built-in skill is prose an agent reads: a suite reads it by its name, or loads the whole folder
503
+ nameHits(f).forEach((s) => picked.add(s));
504
+ for (const s of list) if (/\b(loadDir|readdirSync)\(path\.join\(__dirname, 'skills'\)/.test(get(s) || '')) picked.add(s);
505
+ continue;
506
+ }
501
507
  let why = null;
502
508
  const before = picked.size;
503
509
  if (get(f) == null) { // removed: only the names in the suites can say who used it
@@ -23,6 +23,13 @@ Rules that always hold (D71):
23
23
  7. THE SCRIPT WRITES EVERY NUMBER. In a note you type none: no count, no ratio such as "7 of 12", no percentage, no date, no day count. If a sentence needs a number, write it without the number; the block above the note already shows it. A name is not a number: an initiative id, a file path and a note id may be written as they are.
24
24
  8. PROSE ONLY BETWEEN NOTE MARKERS. In the draft file, write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
25
25
 
26
+ THE REPORT ENGINE FIRST (design E). A new biweekly report is written with the report engine:
27
+ - Run `pmos report make biweekly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
28
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read biweekly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
29
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
30
+ - If `pmos report make biweekly` refuses because the definition names no purpose, use the procedure below.
31
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
32
+
26
33
  Procedure:
27
34
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
28
35
  2. Read the definition: `<state>/reports/definitions/biweekly.md`.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new daily report is written with the report engine:
26
+ - Run `pmos report make daily`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read daily --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make daily` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/daily.md`.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new monthly report is written with the report engine:
26
+ - Run `pmos report make monthly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read monthly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make monthly` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/monthly.md`.
@@ -13,6 +13,8 @@ output:
13
13
 
14
14
  /report-special — prepare a one-off report for THIS repository as a draft.
15
15
 
16
+ THE REPORT ENGINE FIRST (design E). A one-off report is an ad hoc report of the engine: once the PM has said what it is for, follow `skills/report-write.skill` with an id you make from the topic (lower case, dashes) and the PM's question as `--ask`. You choose the outline; every figure is a reading. The procedure below is the older path: use it only when the engine refuses the brief.
17
+
16
18
  Rules that always hold (D71):
17
19
  1. DRAFT ONLY. You write a file in this repository. Never send, post, share, email, publish or upload the report anywhere; the PM does that.
18
20
  2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in mail, chat, Jira, Confluence, calendar or any other source.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: report-template
3
3
  label: "Saved report (from its template)"
4
- description: "Prepare a saved report as a draft. The schedule's args name the template; the script writes every number, then this skill writes the notes between the markers. Never sends or posts."
4
+ description: "Prepare a saved report as a draft with the report engine (pmos report make, then the report-write skill). The schedule's args name the template. Never sends or posts."
5
5
  run: agent
6
6
  agent: claude
7
7
  output:
@@ -23,6 +23,13 @@ Rules that always hold (D71):
23
23
  6. THE SCRIPT WRITES EVERY NUMBER. In a note you type no count, ratio, percentage or date.
24
24
  7. PROSE ONLY BETWEEN NOTE MARKERS. Write only between a `<!-- pmos:note <id> -->` line and its `<!-- /pmos:note -->` line. Never edit, move or delete a marker or any line outside one. Never add a section to the file.
25
25
 
26
+ THE REPORT ENGINE FIRST (design E). A new cycle is written with the report engine, not with the note markers:
27
+ - Run `pmos report make <id>` (the id is the schedule's args). It derives the brief from the template's purpose, audience and sources, and from a saved structure's purpose and questions, reads every source PMOS reaches, and finds what changed.
28
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read <id> --from <file>`). If there is still no finding, there is nothing to report this cycle: stop and say so in one line. Do not draft a report about nothing.
29
+ - Otherwise follow `skills/report-write.skill` from its step 2. The template's sections are the agreed outline only when the last issue was written by the engine; you choose the outline. A saved structure's charts are sources to read: file each value as a reading.
30
+ - A change to a draft that has a `<slot>.report.json` is a change to that report.json: edit it and run `pmos report write <id> --slot <slot> --from <file>` again. A figure stays a reading.
31
+ The procedure below is for a change to a draft written before the engine (a draft with note markers and no `<slot>.report.json`), and for a template whose engine brief is refused.
32
+
26
33
  Procedure:
27
34
  1. The template id is the schedule's `args`. It must match `^[a-z][a-z0-9-]{0,39}$`. If it is missing or does not match, stop and say which schedule has no valid template id.
28
35
  2. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
@@ -22,6 +22,13 @@ Rules that always hold (D71):
22
22
  6. THE PM'S DEFINITION RULES. The shape of this report is whatever the definition says. Do not substitute your own format.
23
23
  7. SHOW THE STEPS. Keep a TodoWrite list of the steps of the procedure below, one item each, and mark each one completed as you finish it. The PM's chat shows "3 of 5 done". If a source you need cannot be reached because its sign-in expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
24
 
25
+ THE REPORT ENGINE FIRST (design E). A new weekly report is written with the report engine:
26
+ - Run `pmos report make weekly`. It derives the brief from the definition's purpose, audience and sources, reads every source PMOS reaches, and finds what changed since the last issue.
27
+ - If it prints "No finding, so no draft", read the sources its gaps name and file their readings (`pmos report read weekly --from <file>`). If there is still no finding, there is nothing to report: stop and say so in one line.
28
+ - Otherwise follow `skills/report-write.skill` from its step 2: you choose the outline for the readers; every figure is a reading.
29
+ - If `pmos report make weekly` refuses because the definition names no purpose, use the procedure below.
30
+ The procedure below is the older path: use it only when the engine refuses the brief, or for a change to a draft that has no `<slot>.report.json`.
31
+
25
32
  Procedure:
26
33
  1. Find the state directory: `pmos/state/` if it exists, otherwise `planning/`. Call it <state>.
27
34
  2. Read the definition: `<state>/reports/definitions/weekly.md`.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: report-write
3
+ label: "Write a report (the report engine)"
4
+ description: "Write any report as a draft, ad hoc or recurring, in any domain: derive the brief, read the sources, find what matters, choose the outline for these readers, and write report.json; PMOS renders the document and the deck. Never sends or posts."
5
+ run: agent
6
+ agent: claude
7
+ output:
8
+ - "<state>/reports/<id>/<slot>.report.json (the answer, the sections you chose, the ask, the parts beyond the ask)"
9
+ - "<state>/reports/<id>/<slot>.md and the deck, rendered by PMOS from it"
10
+ ---
11
+
12
+ /report-write — write one report for THIS repository as a draft, with the report engine (design E). The args are the report id, and for an ad hoc report the question, for example `scarlet-assessment "Do we keep Scarlet, or move small RSVP events to a SaaS?"`.
13
+
14
+ There is no template and no fixed frame. You choose the outline of this report from its readers and their question. What never changes is the discipline below.
15
+
16
+ Rules that always hold (D71):
17
+ 1. DRAFT ONLY. You write files in this repository. Never send, post, share, email, publish or upload the report anywhere; the PM issues it.
18
+ 2. READ-ONLY on every source. Never send, reply, forward, post, comment, create, edit, transition, label, archive or delete anything in mail, chat, Jira, Confluence, BigQuery, calendar or any other source.
19
+ 3. EVERY FIGURE IS A READING. A number, a percentage, a count or a duration in your words is a reading token `{{r:<reading id>}}`, never typed by hand. `{{r:id}}` writes the value with its unit, `{{r:id|of}}` writes "597 of 733", `{{r:id|n}}` writes the count. PMOS refuses a report with a bare figure. Dates are written in full (21 October 2026); a date is not a figure.
20
+ 4. NOTHING INVENTED. A reading has a source of the brief, the date it was read or measured, and how it was read (the query, the filter, the field). A source you cannot reach is a gap with its reason. Never guess a value.
21
+ 5. THE ANSWER FIRST. The title is the answer to the readers' question, with its number. If you cannot write that sentence from the readings, there is nothing to report: stop and say so in one line.
22
+ 6. THE VOICE. Follow the brief's `voice` lines and its `rules`: neutral and straightforward sentences, one claim per paragraph, the mood never happier than the data (0% is written as 0%, with its why). A leadership report does not carry the engineering appendix; it links to it.
23
+ 7. SHOW THE STEPS. Keep a TodoWrite list of the steps below and mark each one completed. If a source needs a sign-in that expired, end with a pmos-ask block: {"kind":"reach","what":"<the source>","why":"sign-in expired","need":"signin"}.
24
+
25
+ Procedure:
26
+ 1. Run `pmos report make <id>`. For an ad hoc report add `--ask "<the question>"`, and `--readers "<who>"` when you know them. Name sources the brief must read with `--source`, comma-separated: `connector:<name>=<what it is>` (Jira, BigQuery for GA4, Confluence, …), `document:<path>`, `pack:<state file of readings>`, `pmos` (the PMOS ledgers). It writes `<slot>.brief.json`, `<slot>.readings.json` and `<slot>.findings.json` next to the draft, and prints the gaps.
27
+ - If it refuses because the slot is issued, stop: an issued report is frozen.
28
+ - If it says there is no report with that id, it needs a question: ask the PM once for the question, or take it from the chat line.
29
+ 2. Read `<slot>.brief.json`: the readers, the questions, the window, the sources, the last issue, the agreed outline, the carried asks, the voice and the rules.
30
+ 3. Read each source that the gaps name, through its connector or the file, read-only. Write what you read as readings in one JSON file: `{"readings":[{"id":"web-login-rate","value":81,"unit":"%","n":597,"of":733,"at":"2026-10-19","source":"ga4","how":"events_20261019, sessions with a password start","label":"Web password login, recorded success"}],"gaps":[{"source":"native-app","reason":"read only on the device"}]}`. Give `target` and `better` ("higher" or "lower") when the reading has a target. Give `flag` when the reading is off what the readers expect. Run `pmos report read <id> --from <file>`. A parameter of a method (a 5-second match window) is a reading too.
31
+ 4. Find what answers the readers. Write the findings that answer each question, each citing the readings it rests on, and run `pmos report find <id> --from <file>` with `{"findings":[{"text":"…","readings":["…"],"kind":"answers-question","question":"…"}]}`. The engine adds its own findings: changes since the last issue, misses against a target, initiatives off plan, items due within 14 days, the asks carried over. If `<slot>.findings.json` says `"none": true`, there is nothing to report: stop and say so in one line. Do not write a report about nothing.
32
+ 5. Choose the outline for these readers and this question. Any sections, any titles, any order.
33
+ - A recurring report keeps `agreed_outline`, the sections of its last issued issue. Add a section only when a finding needs one, drop one only when it would be empty, and say so in `outline_note` (one line).
34
+ - `earlier_outline` is the shape of an issue written before the engine (the old fixed frame). It is an example, not an agreement: keep what served the readers.
35
+ - Each section title is a finding, not a label: "Sign-up stalls before submit", not "Sign-up". Reading only the titles tells the story.
36
+ - Shapes other reports took, as examples and never a menu: a decision (the lean · the options and what each costs · what could reverse it · your call); a watch (today against yesterday and the week · the drop that moved most · why); a stakeholder update (are the dates on track · what each team needs and by when · open decisions · what could stop it); a review (the summary for decision · the outcomes by segment · the challenges and answers · method); did-it-work (what was promised · what we got · the verdict).
37
+ 6. Write `<slot>.report.json` (in a scratch file, then `pmos report write <id> --from <file>`):
38
+ `{"title":"…{{r:…}}…","answer":{"text":"…","supports":["Label: …{{r:…}}…"]},"sections":[{"id":"…","title":"…","blocks":[{"kind":"p","text":"…"},{"kind":"list","items":["…"]},{"kind":"table","columns":["…"],"rows":[["…"]]},{"kind":"bars","title":"…","rows":[["label","{{r:id}}"]]},{"kind":"options","options":[{"name":"…","text":"…"}]},{"kind":"quote","text":"…"}]}],"ask":{"text":"…","who":"…","by":"YYYY-MM-DD","options":["…"]},"beyond":{"why":"…","so_what":"…","now_what":"…","since":"…","would_change":"…","unknown":["…"],"else":["…"],"next_question":"…"},"questions":[{"question":"<a question of the brief>","answered_in":"<section id or answer>","gap":"<why it cannot be answered>"}],"outline_note":"…"}`.
39
+ - `ask` is null when the readers have nothing to decide. When it is a call with no better option on the evidence, give the options and say "your call".
40
+ - BEYOND THE ASK. Answer the readers' next three questions before they ask: why (the cause, from the evidence), so what (the effect on the target and its date), now what (the recommendation, who, by), since the last issue (what moved, and each carried ask), what would change it (the one or two readings that would flip the recommendation), what we don't know (the gaps, counted, never padded), what else is here (at most two findings from a named source the readers did not ask about, when their question depends on them) and the next question (the split they will ask for next). Each part cites a reading. Leave a part out when no reading backs it, and leave out a part the brief's rules turn off.
41
+ - `questions`: map each question of the brief after the first to the section that answers it, or give the gap.
42
+ 7. Read what `pmos report write` prints. A refusal names the check: fix the report and run it again. When it passes, it writes `<slot>.md` and `<slot>.checks.json`; the deck, the PDF and the PowerPoint are drawn from the same model, in the report's saved look.
43
+ 8. Learn. When the PM's change in the chat is about the words (shorter, plainer, another tone) and it would apply to every issue, keep it: `pmos report rule <id> add wording "<the rule, one line>"`. When the PM removes a part beyond the ask, `pmos report rule <id> add off <part>`. Then change report.json and run `pmos report write` again. A change to a number is a change to a reading: read the source again and file it.
44
+ 9. Finish by saying in one line where the draft is and how many readings, findings and gaps it has. Do not commit; the PM's Sync carries the files.