create-pmos 0.509.0 → 0.511.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 (39) hide show
  1. package/package.json +247 -134
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/board-programs-test.js +3 -0
  7. package/workspace/briefing-premium-board-test.js +6 -7
  8. package/workspace/changelog.json +352 -909
  9. package/workspace/ev-everywhere-test.js +4 -4
  10. package/workspace/guide/home.md +2 -1
  11. package/workspace/guide/initiative-boards.md +3 -1
  12. package/workspace/guide/programs.md +2 -1
  13. package/workspace/guide/settings.md +9 -5
  14. package/workspace/lib/board-premium.js +3 -2
  15. package/workspace/lib/kit-update.js +19 -4
  16. package/workspace/lib/routes-boards.js +15 -1
  17. package/workspace/package-lock.json +2 -2
  18. package/workspace/package.json +1 -1
  19. package/workspace/panels-test.js +68 -1
  20. package/workspace/public/pm/bd-cols.js +2 -1
  21. package/workspace/public/pm/bd-deliveries.js +1 -1
  22. package/workspace/public/pm/bd-prog.js +10 -5
  23. package/workspace/public/pm/bd-span.js +15 -10
  24. package/workspace/public/pm/cozy-board.css +8 -2
  25. package/workspace/public/pm/cozy-ev.css +24 -10
  26. package/workspace/public/pm/cozy-live-pill.css +9 -0
  27. package/workspace/public/pm/cozy-live-pill.js +50 -3
  28. package/workspace/public/pm/cozy-panels.css +9 -0
  29. package/workspace/public/pm/cozy-panels.js +99 -16
  30. package/workspace/public/pm/cozy-settings.js +34 -15
  31. package/workspace/public/pm/cozy-viewer.js +15 -3
  32. package/workspace/public/pm/home-chat.js +4 -0
  33. package/workspace/public/pm/page-chat.css +9 -0
  34. package/workspace/public/pm/page-chat.js +90 -0
  35. package/workspace/public/pm/premium-board.js +9 -0
  36. package/workspace/public/pm/readonly.js +3 -1
  37. package/workspace/public/pm/upd-weeks.js +27 -9
  38. package/workspace/public/premium-board.html +2 -2
  39. package/workspace/test-lists/closed-not-late.json +5 -0
@@ -0,0 +1,90 @@
1
+ /* pm/page-chat.js — the chat as a panel on a page that has no chat column (the Board; batch 26 item 4, Wawan 2026-11-03: "User should be
2
+ * able to open chat panel on board page").
3
+ *
4
+ * "Discuss in chat" on the Board's initiative or program panel opens the chat about that item in a Viewer of its own, beside the panel
5
+ * (1600 px and wider) or sliding over it (a laptop), through the panels engine (cozy-panels.js openBeside). The page stays where it is.
6
+ * It is Home's chat (home-chat.js mountChat), not a second one: the same conversation key Home opens for that item (cozy-viewer.js
7
+ * aboutPrefix: init:<id> for an initiative, board:<id> for a program), so the words are the same on both pages. One chat panel at a time:
8
+ * Discuss on another item changes the conversation in it. A file or a link the chat opens goes in the other Viewer (outward), as on Home.
9
+ * ⋯ keeps "Open in a new tab": the same conversation on Home. A phone goes to Home as before (cozy-viewer.js chatHere is desktop only).
10
+ *
11
+ * openChat({ id, kind, title }, { ws? }) → true when it opened (false: no item to talk about; the caller goes to Home)
12
+ * The chat's styles are Home's (cozy-home.css, home-line.css); they load once, the first time the chat opens. */
13
+ import * as VW from './cozy-viewer.js';
14
+ import { pmGet, pmPost, pmHeaders } from './http.js';
15
+
16
+ export const CHAT_ID = 'page-chat';
17
+ const CLAUDE = { key: 'claude', name: 'Claude', initial: 'C' };
18
+ const CHAT_ICON = 'M5 6.5A2.5 2.5 0 0 1 7.5 4h9A2.5 2.5 0 0 1 19 6.5v7a2.5 2.5 0 0 1-2.5 2.5H11l-4 3.5V16h.5A2.5 2.5 0 0 1 5 13.5z';
19
+ const FLAG = 'M5 21V4h11l-2 4 2 4H5';
20
+ const SHEETS = ['./cozy-home.css', './home-line.css', './page-chat.css'];
21
+ const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
22
+ const Q = { el: null, chat: null, t: null, mod: null };
23
+
24
+ /** The conversation key Home opens for the item (cozy-viewer.js discuss: ?about=). */
25
+ export function keyOf(it) {
26
+ const raw = String((it && (it.id || it.title)) || '');
27
+ const k = String((it && it.kind) || 'item').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
28
+ const id = raw.replace(/[^A-Za-z0-9._:-]+/g, '-').replace(/^[^A-Za-z0-9]+/, '').slice(0, 100);
29
+ return id ? VW.aboutPrefix(k) + ':' + id : '';
30
+ }
31
+ /** The chat's target (home-chat.js mountChat open): the item's name is its scope; Claude answers. */
32
+ export function targetOf(it, ws) {
33
+ const title = String((it && it.title) || 'this').slice(0, 80);
34
+ return { key: keyOf(it), ws: ws || undefined, title, about: title, scope: { text: title, icon: FLAG }, role: CLAUDE, rich: false,
35
+ placeholder: 'Message Claude about ' + title, empty: 'Ask about ' + title + '. Claude reads its records and works in a real Claude Code session.' };
36
+ }
37
+ /** Home's own address for the same conversation (⋯ › Open in a new tab). */
38
+ const homeHref = (it, key) => '/premium.html?' + new URLSearchParams({ about: key, label: String(it.title || '').slice(0, 80) }).toString();
39
+
40
+ function sheets() {
41
+ for (const s of SHEETS) {
42
+ const href = new URL(s, import.meta.url).href;
43
+ if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((l) => l.href === href)) { const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href; document.head.appendChild(l); }
44
+ }
45
+ }
46
+ const toast = (m) => { const t = document.getElementById('toast'); if (!t) return; t.textContent = m; t.classList.add('on'); setTimeout(() => t.classList.remove('on'), 4200); };
47
+ function host() {
48
+ return {
49
+ esc, toast, pmGet, pmPost, pressHeaders: () => { const h = pmHeaders(); delete h['Content-Type']; return h; },
50
+ roles: () => [], seen: () => null, markSeen: () => {}, session: () => null, queued: () => [],
51
+ touch: (sid) => { if (sid) pmPost('/api/session/touch', { id: sid }).then(() => document.dispatchEvent(new CustomEvent('cz:live'))).catch(() => {}); },
52
+ onScope: () => {}, onTo: () => {}, onLink: () => {},
53
+ workItem: () => toast('Make a work-item from Home, in the initiative’s own chat.'),
54
+ viewerClosed: () => false, reopenViewer: () => {}, desk: () => true, head: () => '',
55
+ // a finished turn: the page reads its records again, so what Claude changed shows with no refresh (as Home, batch 25 item 3)
56
+ turnEnded: () => document.dispatchEvent(new CustomEvent('cz:chat-turn')),
57
+ };
58
+ }
59
+ async function mod() {
60
+ if (!Q.mod) Q.mod = import('./home-chat.js');
61
+ return Q.mod;
62
+ }
63
+ const shownAt = () => VW.panels.list().find((p) => p.open && p.item && p.item.id === CHAT_ID) || null;
64
+
65
+ export async function openChat(it, o = {}) {
66
+ const key = keyOf(it); if (!key) return false;
67
+ sheets();
68
+ const M = await mod();
69
+ if (!Q.el) {
70
+ Q.el = document.createElement('section'); Q.el.className = 'hm-chat pc-chat'; Q.el.setAttribute('aria-label', 'Conversation');
71
+ Q.chat = M.mountChat(Q.el, host());
72
+ }
73
+ Q.t = targetOf(it, o.ws);
74
+ const item = {
75
+ id: CHAT_ID, kind: 'Chat', title: Q.t.title, icon: CHAT_ICON, url: homeHref(it, key), outward: true, fresh: true,
76
+ render: (body) => { body.classList.add('pc-body'); body.appendChild(Q.el); Q.chat.open(Q.t); },
77
+ onClose: () => { if (Q.chat) Q.chat.stop(); },
78
+ };
79
+ const at = shownAt();
80
+ if (at) { // one chat panel: it changes to this item's conversation in place (its title and its Home address with it)
81
+ VW.open({ ...item, into: at.id });
82
+ if (window.czPanels && (window.czPanels.list().find((p) => p.id === at.id) || {}).mode === 'away') window.czPanels.slideOver();
83
+ setTimeout(() => Q.chat && Q.chat.focus(), 60);
84
+ return true;
85
+ }
86
+ const P = window.czPanels;
87
+ if (!(P && P.openBeside && P.openBeside(item))) VW.open(item);
88
+ setTimeout(() => Q.chat && Q.chat.focus(), 60);
89
+ return true;
90
+ }
@@ -95,6 +95,11 @@ mountLivePill();
95
95
  * name drags beside, or ⌘/Ctrl-click opens it beside (a By stage card's drag moves the stage, so it is no source). fit(): the width the
96
96
  * view needs whole, so the Viewer opens no wider than that leaves. */
97
97
  mountPanels({ page: document.getElementById('stage'), key: 'board', sources: BP2.SOURCES, fit: BP2.fit });
98
+ /* Discuss in chat on an initiative's or a program's panel opens Home's chat about it in a panel of its own, beside the panel or sliding over
99
+ * it (batch 26 item 4, page-chat.js); the Board stays. A finished turn reads the board again. A phone goes to Home as before. */
100
+ VW.chatHere((it) => { const r = M && (M.rows || []).find((x) => x.id === it.id); import('./page-chat.js').then((C) => C.openChat(it, { ws: (r && r.ws) || (M && M.board.ws) || undefined })); return true; });
101
+ document.addEventListener('cz:chat-turn', () => { clearTimeout(chatT); chatT = setTimeout(() => load(), 400); });
102
+ let chatT = null;
98
103
  // a product head's goal dots (PgBoard .gd) count that product's open initiatives, the way Home's product header does
99
104
  PG.goalsFrom((name) => (M ? M.initiatives.list.rows.filter((r) => !M.board.all || r.product === name) : null));
100
105
  const SV = (d) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${d}"/></svg>`;
@@ -639,6 +644,8 @@ $('#ibBody').addEventListener('click', (e) => {
639
644
  const tk = e.target.closest('[data-tk]'); if (tk) { openTicket(tk.dataset.ini, tk.dataset.tk, tk); return; }
640
645
  if (e.target.closest('[data-act], [data-hold], [data-ms-pick], [data-fc-pick]')) return; // pm/bd-acts.js handles its own presses (a ⏸ on the timeline opens its hold there; a ◇ its milestones, pm/bd-pin-card.js)
641
646
  const pg = e.target.closest('[data-prog]'); if (pg) { PG.openProgram(pg.dataset.prog, pg); return; }
647
+ // a product head's tile and name (batch 26 item 3): that product's page in Home's Viewer, the one place the product page is drawn
648
+ const pd = e.target.closest('[data-prodopen]'); if (pd) { location.assign(productHref(pd.dataset.prodws)); return; }
642
649
  // a counted milestone ◇ lists its milestones right under it; on a phone the row's bar lane opens them in a sheet (batch 11 item 3)
643
650
  const mc = e.target.closest('[data-ms-row]'); if (mc) { msPop(mc, rowOf(mc.dataset.msRow), mc.dataset.msIds.split(' ')); return; }
644
651
  // a product lane's counted ◇ lists its milestones with their initiatives; on a phone a tap on the lane lists them all in a sheet (batch 11 item 17)
@@ -693,6 +700,8 @@ function askHref(r, q) {
693
700
  const ws = (r && r.ws) || M.board.ws; if (ws) p.set('ws', ws);
694
701
  return '/premium.html?' + p.toString();
695
702
  }
703
+ /** A product's page (Home's Viewer, home-app.js openProductPage): Home on that product's workspace with ?ini=product. ws '': this board's own. */
704
+ const productHref = (ws) => { const w = ws || (M && !M.board.all ? M.board.ws : ''); return '/premium.html?' + (w ? 'ws=' + encodeURIComponent(w) + '&' : '') + 'ini=product'; };
696
705
  const pageHref = (r) => { const ws = r.ws || M.board.ws, b = r.boardId || M.board.id; return '/premium-initiative.html?id=' + encodeURIComponent(r.id) + (ws ? '&ws=' + encodeURIComponent(ws) + '&board=' + encodeURIComponent(b) : ''); };
697
706
 
698
707
  /* ── the initiative and its Jira ticket open in the Viewer's shell A, floating over the page (pm/bd-ini-panel.js; canvas XvABoard, XvOptAJira).
@@ -47,7 +47,9 @@ const REMOVE = [
47
47
  * milestone opens the program's charter in the workspace; a share does not hold the charter. */
48
48
  const PLAIN = ['button.dl-e[aria-label*="Program milestone"]:not([aria-label*="press to open"])', 'button.cz-vw-rn', 'button.iv-tt', 'button.iv-dc', 'button.iv-stc', 'button.iv-dtc', 'button.iv-wf', 'button.iv-cb', 'button.tg-end',
49
49
  // the program page (batch 21 item 2): a task's check box, a goal (its Viewer is in Intelligence), a report (its file is the publisher's), a program check
50
- 'button.pp-ck', 'button.pv-gr', 'button.db-rep', 'button.pp-dm'];
50
+ 'button.pp-ck', 'button.pv-gr', 'button.db-rep', 'button.pp-dm',
51
+ // a product head's tile and name (batch 26 item 3): its page is in the publisher's Home, which a share does not hold
52
+ 'button[data-prodopen]'];
51
53
  /** A fold whose body goes with it: Documents and Linked are not listed. */
52
54
  const WITH_BODY = ['[data-iv-fold="docs"]', '[data-iv-docs]', '[data-iv-fold="linked"]'];
53
55
 
@@ -1,8 +1,11 @@
1
1
  /* pm/upd-weeks.js — the published kits' change log, grouped by calendar week (Settings › Updates; initiative premium-updates, the weekly log).
2
2
  * Pure: no DOM, no network. The two Settings screens (cozy-settings.js, premium-shell.js) draw what it returns.
3
- * weeksOf(entries, now, mine) → [{ key, name, range, kits: [oldest, newest], newer, groups: { new, imp, fix }, count }] newest week first
4
- * lineOf(item) → { b, s } a bold name and the sentence under it (the PR's title and its summary), or just the sentence
5
- * An entry is { v, at, prs: [{ n, t, s, k }] } as scripts/kit-changelog.js writes it. A week runs Monday to Sunday in this computer's time zone.
3
+ * weeksOf(entries, now, mine) → [{ key, name, range, kits: [oldest, newest], newer, groups: { new, imp, fix }, count, items, sum }] newest week first
4
+ * items: every line of the week as one flat list, the newest kit first; sum: up to three summary lines, from the highlights (top)
5
+ * storiesOf(w) → { kit, latest: [items of the newest kit], earlier: [the rest, highlights first] } design E: the newest kit at version level
6
+ * lineOf(item) → { b, s } a bold name and the sentence under it: Claude's story (h, d) when the kit has one, else the PR's title and
7
+ * its summary, or just the sentence
8
+ * An entry is { v, at, prs: [{ n, t, s, k, h, d, top }] } as scripts/kit-changelog.js writes it. A week runs Monday to Sunday in this computer's time zone.
6
9
  * Each PR becomes one line in New, Improved or Fixed by its kind k; chore, docs, test and CI PRs (k = x) are left out, and a week with no line
7
10
  * is not listed (zero is silence). A kit published before kinds were kept has no k, so its title decides: a fix word means Fixed, else Improved. */
8
11
  export const KINDS = [['new', 'New'], ['imp', 'Improved'], ['fix', 'Fixed']];
@@ -15,9 +18,14 @@ export function kindOf(p) {
15
18
  if (p && (p.k === 'new' || p.k === 'imp' || p.k === 'fix' || p.k === 'x')) return p.k;
16
19
  return FIX.test((p && (p.t || p.s)) || '') ? 'fix' : 'imp';
17
20
  }
21
+ /** A title and a summary are the same line when they differ only in code ticks, case, spacing or the closing stop
22
+ * ("`pmos x` names y" and "Pmos x names y."): the line is then the title once, without its ticks, so a command keeps its own case. */
23
+ const same = (a, b) => { const f = (x) => x.replace(/`/g, '').replace(/[.!?]+$/, '').replace(/\s+/g, ' ').trim().toLowerCase(); return f(a) === f(b); };
18
24
  export function lineOf(p) {
19
- const s = String((p && p.s) || ''), t = String((p && p.t) || '');
20
- return t && t !== s ? { b: t, s } : { b: '', s };
25
+ if (p && p.h) { const d = String(p.d || ''), ps = String(p.s || ''); return { b: String(p.h), s: d || (same(p.h, ps) || same(String(p.t || ''), ps) ? '' : ps) }; }
26
+ const s = String((p && p.s) || ''), t = String((p && p.t) || '').replace(/`([^`]*)`/g, '$1');
27
+ if (!t) return { b: '', s };
28
+ return same(t, s) ? { b: '', s: t } : { b: t, s };
21
29
  }
22
30
  const mondayOf = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - ((d.getDay() + 6) % 7));
23
31
  const keyOf = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
@@ -33,15 +41,16 @@ export function weeksOf(entries, now = Date.now(), mine = null) {
33
41
  for (const e of Array.isArray(entries) ? entries : []) {
34
42
  if (!e || !e.v) continue;
35
43
  const d = e.at ? new Date(e.at) : null, dated = d && !isNaN(d), mon = dated ? mondayOf(d) : null, key = dated ? keyOf(mon) : 'undated';
36
- if (!by.has(key)) by.set(key, { key, mon, kits: [], newer: false, groups: { new: [], imp: [], fix: [] }, seen: new Set() });
44
+ if (!by.has(key)) by.set(key, { key, mon, kits: [], newer: false, groups: { new: [], imp: [], fix: [] }, items: [], seen: new Set() });
37
45
  const w = by.get(key);
38
46
  w.kits.push(e.v);
39
47
  if (mine && cmpVer(e.v, mine) > 0) w.newer = true;
40
48
  for (const p of e.prs || []) {
41
- const k = kindOf(p); if (k === 'x') continue;
49
+ const k = p.h && p.top ? 'new' : kindOf(p); if (k === 'x') continue; // a highlight is told as a New entry
42
50
  if (p.n && w.seen.has(p.n)) continue;
43
51
  if (p.n) w.seen.add(p.n);
44
- w.groups[k].push({ n: p.n || null, t: p.t || '', s: p.s || '', v: e.v });
52
+ const it = { n: p.n || null, k, t: p.t || '', s: p.s || '', v: e.v, ...(p.h ? { h: p.h } : {}), ...(p.d ? { d: p.d } : {}), ...(p.h && p.top ? { top: p.top } : {}) };
53
+ w.groups[k].push(it); w.items.push(it);
45
54
  }
46
55
  }
47
56
  const out = [];
@@ -50,7 +59,16 @@ export function weeksOf(entries, now = Date.now(), mine = null) {
50
59
  if (!count) continue;
51
60
  const sorted = [...w.kits].sort(cmpVer);
52
61
  const name = !w.mon ? 'Date not recorded' : keyOf(w.mon) === keyOf(thisMon) ? 'This week' : keyOf(w.mon) === keyOf(lastMon) ? 'Last week' : 'Week of ' + dm(w.mon);
53
- out.push({ key: w.key, name, range: w.mon ? rangeOf(w.mon) : '', kits: [sorted[0], sorted[sorted.length - 1]], newer: w.newer, groups: w.groups, count, mon: w.mon });
62
+ const items = w.items.map((it, i) => [it, i]).sort((a, b) => cmpVer(b[0].v, a[0].v) || a[1] - b[1]).map(([it]) => it);
63
+ const sum = items.filter((it) => it.top).slice(0, 3).map((it) => it.top);
64
+ out.push({ key: w.key, name, range: w.mon ? rangeOf(w.mon) : '', kits: [sorted[0], sorted[sorted.length - 1]], newer: w.newer, groups: w.groups, count, items, sum, mon: w.mon });
54
65
  }
55
66
  return out.sort((a, b) => (a.mon && b.mon ? b.mon - a.mon : a.mon ? -1 : b.mon ? 1 : 0)).map(({ mon, ...w }) => w);
56
67
  }
68
+
69
+ /** Design E: the week's newest kit on its own, at version level, then the rest of the week as one flat list with the highlights first. */
70
+ export function storiesOf(w) {
71
+ const kit = w && w.kits ? w.kits[1] : null, items = (w && w.items) || [];
72
+ const latest = items.filter((it) => it.v === kit), rest = items.filter((it) => it.v !== kit);
73
+ return { kit, latest, earlier: [...rest.filter((it) => it.top), ...rest.filter((it) => !it.top)] };
74
+ }
@@ -7,8 +7,8 @@
7
7
  <title>PMOS · Board</title>
8
8
  <!-- The premium Board in the Agreed design (initiative premium-agreed-design, B1 B2 B3 N1 N2). One toolbar and no page header; each initiative is a row
9
9
  across the six stages and two milestones, with progress toward its current output from Jira. It reads GET /api/board-premium. The floating place menu
10
- (pm/cozy-nav.js) replaces the rail on screen; the rail stays in the markup so premium-shell.js keeps its wiring. The Board has no chat: "Ask about this"
11
- sends the question to Home. -->
10
+ (pm/cozy-nav.js) replaces the rail on screen; the rail stays in the markup so premium-shell.js keeps its wiring. Discuss in chat on a panel opens Home's chat
11
+ about it in a panel on the Board (pm/page-chat.js); a phone goes to Home. -->
12
12
  <link rel="preload" href="/vendor/nunito-var.woff2" as="font" type="font/woff2" crossorigin />
13
13
  <link rel="stylesheet" href="/pm/cozy.css" />
14
14
  <link rel="stylesheet" href="/pm/cozy-pick.css" />
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Batch 26 item 5 (Fix red light issue): a closed initiative (its workflow's terminal stage, lib/routes-boards.js closedOf) is never late on the Board: no line to Today, no late dot or ◇, no edge tab; a one-day plan draws its bar.",
3
+ "modules": ["closed-not-late.test.js"],
4
+ "seconds": { "closed-not-late.test.js": 2 }
5
+ }