create-pmos 0.526.0 → 0.528.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 (43) hide show
  1. package/package.json +45 -31
  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/adler-moments-test.js +4 -3
  7. package/workspace/changelog.json +44 -0
  8. package/workspace/ev-everywhere-test.js +2 -6
  9. package/workspace/guide/home.md +7 -6
  10. package/workspace/guide/reports-set-up.md +2 -0
  11. package/workspace/lib/bg-queue.js +5 -2
  12. package/workspace/lib/briefing-premium-agent.js +9 -2
  13. package/workspace/lib/okr-goals.js +23 -3
  14. package/workspace/lib/report-chat.js +31 -4
  15. package/workspace/lib/routes-briefing-premium.js +18 -1
  16. package/workspace/lib/routes-morning-goals.js +5 -1
  17. package/workspace/news-viewers-test.js +2 -1
  18. package/workspace/package-lock.json +2 -2
  19. package/workspace/package.json +1 -1
  20. package/workspace/product-lane-test.js +1 -1
  21. package/workspace/program-charter-test.js +2 -2
  22. package/workspace/public/pm/bd-ini-panel.js +1 -1
  23. package/workspace/public/pm/bd-span.js +10 -7
  24. package/workspace/public/pm/cozy-board.css +1 -0
  25. package/workspace/public/pm/cozy-search.js +10 -17
  26. package/workspace/public/pm/cozy-settings.js +8 -2
  27. package/workspace/public/pm/cozy-today.css +30 -14
  28. package/workspace/public/pm/good-morning.js +30 -17
  29. package/workspace/public/pm/home-app.js +11 -1
  30. package/workspace/public/pm/home-chat.js +6 -2
  31. package/workspace/public/pm/home-data.js +4 -1
  32. package/workspace/public/pm/home-day.js +12 -5
  33. package/workspace/public/pm/premium-board.js +7 -5
  34. package/workspace/public/pm/premium-initiative-head.js +9 -1
  35. package/workspace/public/pm/premium-insights.js +5 -3
  36. package/workspace/public/pm/premium-report-read.js +11 -4
  37. package/workspace/public/pm/premium-report-tab.js +15 -3
  38. package/workspace/public/pm/program-model.js +2 -1
  39. package/workspace/public/pm/report-flow.js +7 -1
  40. package/workspace/public/premium-board.html +4 -4
  41. package/workspace/server.js +1 -1
  42. package/workspace/task-milestones-test.js +2 -2
  43. package/workspace/timeline-holds-test.js +3 -3
@@ -260,7 +260,7 @@ const sprintFilterOn = () => curView() !== 'time' || span === 'sprint';
260
260
  /** What needs a look (batch 24 item 7, canvas BbB): new asks, late, at risk, blocked and idle are ONE pill of dots and numbers. A press opens the
261
261
  * rows; a row opens what its chip opened (the asks, a metric's trend, whose "Show only" filters); a filter that is on turns off at once.
262
262
  * A row that is zero is not listed, and the pill is not drawn when every row is zero (zero is silence). */
263
- function needHtml(C, open) {
263
+ function needHtml(C, open, phone) {
264
264
  const rows = [];
265
265
  if (C.asks) rows.push({ k: 'asks', dot: 'you m-info', n: C.asks, name: plural(C.asks, 'New ask', 'New asks'), say: C.asks + ' new ' + plural(C.asks, 'ask', 'asks') });
266
266
  for (const [k, tone, mo, name] of CHIP_F) if (C[k]) rows.push({ k, dot: tone + (mo ? ' ' + mo : ''), n: C[k], name, say: C[k] + ' ' + name.toLowerCase() });
@@ -271,15 +271,17 @@ function needHtml(C, open) {
271
271
  const item = (r) => (r.k === 'asks'
272
272
  ? `<button type="button" class="bd-nr" id="chAsks" aria-haspopup="dialog"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`
273
273
  : `<button type="button" class="bd-nr${F.chips.has(r.k) ? ' on' : ''}" data-f="${r.k}" aria-pressed="${F.chips.has(r.k)}"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`);
274
+ // the words beside each dot: "3 late · 1 at risk" (workspace grill W4); a phone has room for the numbers only
275
+ const parts = rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${esc(phone ? String(r.n) : r.say)}</span>`).concat(C.none ? [`<span><i class="cz-dot q"></i>${esc(phone ? String(C.none) : C.none + ' no signal')}</span>`] : []);
274
276
  return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).concat(C.none ? [C.none + ' no signal'] : []).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
275
- + rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + (C.none ? `<span><i class="cz-dot q"></i>${C.none}</span>` : '') + SV(I.chev) + '</button>'
277
+ + parts.join(phone ? '' : '<span class="bd-nsep" aria-hidden="true">·</span>') + SV(I.chev) + '</button>'
276
278
  + `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}${nosig}</span></span>`;
277
279
  }
278
280
  const menuOpen = (id) => !!$('#' + id) && !$('#' + id).hidden;
279
281
  function drawChips() {
280
282
  const J = M.jira || {}, C = M.chips || {}, sp = J.sprint, rel = J.release, phone = small();
281
283
  const jira = J.readAt ? 'Jira · read ' + ago(J.readAt) : 'Jira · not read';
282
- const sprint = [sp ? sp.name : null, rel ? rel.name + ' ships ' + rel.at.replace(/^(\d+) (\w+)$/, '$2 $1') : null].filter(Boolean).join(' · ') || 'No sprint on record';
284
+ const sprint = [sp ? sp.name : null, rel ? rel.name + ' ships ' + rel.at.replace(/^(\d+) (\w+)$/, '$2 $1') : null].filter(Boolean).join(' · ') || ''; // no sprint and no release on record: no chip (workspace grill W4, zero is silence)
283
285
  const thisSprint = sp && (view === 'track' || view === 'time' || phone) && sprintFilterOn() ? `<button type="button" class="bd-chip${F.chips.has('sprint') ? ' on' : ''}" data-f="sprint" aria-pressed="${F.chips.has('sprint')}" title="${F.chips.has('sprint') ? 'Shows only the initiatives with work in ' + esc(sp.name) + '. Press to show all.' : 'Press to show only the initiatives with work in ' + esc(sp.name) + '.'}">${esc(sp.name)} only</button>` : ''; // it named no sprint and read like a heading (testing fixes)
284
286
  // All products (PgBoard): one Jira control with the oldest read of any product, and every product's new asks; the sprint stays per product
285
287
  // AdM2: Adler's S face carries the read's mood: calm when Jira was read within a day and a half, honest when it was not, glasses on while
@@ -288,7 +290,7 @@ function drawChips() {
288
290
  const jMood = jiraReading ? 'thinking' : J.readAt && Date.now() - Date.parse(J.readAt) < 36 * 3600000 ? 'handled' : 'honest';
289
291
  const jSay = jira + '. ' + (ALL ? 'Press to read Jira again for every product' : 'Press to read Jira again');
290
292
  const jiraChip = `<button type="button" class="bd-chip bd-jf" id="chJira" title="${esc(jSay)}" aria-label="${esc(jSay)}">${faceOr(jMood, 's', SV(I.refresh), { label: false })}${jMood === 'honest' ? '<i class="bd-pip" aria-hidden="true"></i>' : ''}<span class="vh">${esc(jira)}</span></button>`;
291
- const need = needHtml(C, menuOpen('bdNeedMenu'));
293
+ const need = needHtml(C, menuOpen('bdNeedMenu'), phone);
292
294
  if (phone) {
293
295
  $('#bdChipset').innerHTML = thisSprint + jiraChip + need + `<button type="button" class="bd-chip" id="chShared">Shared${SH_N ? ' ' + SH_N : ''}</button>`;
294
296
  return;
@@ -298,7 +300,7 @@ function drawChips() {
298
300
  const lo = menuOpen('bdLgdMenu');
299
301
  const lgd = view === 'time' && span !== 'sprint' ? `<span class="bd-grpw2 bd-lgdw"><button type="button" class="bd-ib bd-lgdb" id="bdLgdBtn" aria-haspopup="true" aria-expanded="${lo}" aria-controls="bdLgdMenu" aria-label="What the bars mean" title="What the bars mean">${SV(I.info)}</button>`
300
302
  + `<span class="bd-grpm bd-lgdm" id="bdLgdMenu"${lo ? '' : ' hidden'}><span class="bd-mlbl">What the bars mean</span>${legendHtml(!byProduct && !!groups(visible()), hasHold(visible()), { ...planKeys(visible()), ...gapKeys(visible()), range: visible().some((r) => fcRange(r)) })}</span></span>` : '';
301
- $('#bdChipset').innerHTML = (ALL ? '' : `<button type="button" class="bd-chip" id="chSprint" aria-haspopup="dialog">${SV(I.cal)}<span>${esc(sprint)}</span></button>`)
303
+ $('#bdChipset').innerHTML = (ALL || !sprint ? '' : `<button type="button" class="bd-chip" id="chSprint" aria-haspopup="dialog">${SV(I.cal)}<span>${esc(sprint)}</span></button>`)
302
304
  + thisSprint + '<span class="bd-sp"></span>' + need + lgd + jiraChip;
303
305
  }
304
306
  const closeMenu = (id, btn) => { const m = $('#' + id); if (m && !m.hidden) { m.hidden = true; const b = $('#' + btn); if (b) b.setAttribute('aria-expanded', 'false'); return true; } return false; };
@@ -70,6 +70,14 @@ export function gateOf(steps) {
70
70
  return parts.length ? { label: 'Gate to Build', parts } : null;
71
71
  }
72
72
  const STEP_SAY = { spec: 'PRD and rubric written', contract: 'Your go on the done criteria', review: 'The Review Gate failed: weigh its findings', accept: 'Your sign-off', route: 'Lane chosen', commit: 'Your Commit' };
73
+ /** What each "your turn" press sends, in the PM's words, not the step's internal name ("the contract step", workspace grill W4). */
74
+ const ASK_SAY = {
75
+ contract: 'Show me the done criteria for this initiative: what must be true for it to count as done, and how each one is checked. Say which ones I have not agreed yet, read the evidence in this repository, say what you could not find, and change nothing.',
76
+ review: 'Show me the review findings for this initiative: what passed, what did not, and the evidence for each. Change nothing.',
77
+ accept: 'Is this initiative ready for my sign-off? Show me what was built against its done criteria, say what you could not find, and change nothing.',
78
+ spec: 'Walk me through the PRD for this initiative: the problem, who it is for, and what is still open. Say what you could not find and change nothing.',
79
+ commit: 'Show me the Commit for this initiative: what we promise, by when, and what it depends on. Change nothing.',
80
+ };
73
81
  const CTA_SAY = { contract: 'Review the done criteria', review: 'Open the findings', accept: 'Give your sign-off', spec: 'Ask about the PRD', commit: 'Review the Commit' };
74
82
  const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
75
83
  const cap1 = (s) => (s ? String(s).charAt(0).toUpperCase() + String(s).slice(1) : '');
@@ -165,7 +173,7 @@ export function headModel(M, now = Date.now()) {
165
173
  // one CTA: your step when it waits on you, else a question for the read-only agent
166
174
  const cta = turn === 'you' ? (CTA_SAY[step] || ('Open ' + (step || 'the next step'))) : turn === 'stalled' ? 'Ask what is stuck' : turn === 'jira' ? 'Ask about the tickets' : 'Ask what is next';
167
175
  const ask = turn === 'you' && isFailedReview(m) ? 'The Review Gate failed for this initiative. Read its latest verdict and findings in planning/runs/evals.jsonl, list each failing dimension with the evidence, and say what would fix it. Change nothing.'
168
- : turn === 'you' ? 'What do I need to do for the ' + (step || 'next') + ' step of this initiative? Read the evidence in this repository, say what you could not find, and change nothing.'
176
+ : turn === 'you' ? ASK_SAY[step] || 'What do I need to do next on this initiative? Read the evidence in this repository, say what you could not find, and change nothing.'
169
177
  : turn === 'stalled' ? 'What is stuck in this initiative, and what would move it? Read this repository, say what you could not find, and change nothing.'
170
178
  : turn === 'jira' ? 'Where do this initiative\'s Jira tickets stand, from the last Jira read? Name the assignee of each where the read holds one, and change nothing.'
171
179
  : 'What happens next in this initiative, and who does it? Read this repository and change nothing.';
@@ -80,7 +80,7 @@ const I = {
80
80
  const byKey = (k) => (PF && PF.charts[k]) || (M && (M.charts || []).find((c) => c.key === k)) || null;
81
81
  const savedDash = () => (M.dashboards || []).filter((d) => d.saved);
82
82
  const wsQ = () => (M && M.board.ws ? 'ws=' + encodeURIComponent(M.board.ws) + '&' : '');
83
- const homeAsk = (kind, q) => '/premium.html?' + wsQ() + 'ask=' + encodeURIComponent(kind) + '&q=' + encodeURIComponent(q);
83
+ const homeAsk = (kind, q, label) => '/premium.html?' + wsQ() + 'ask=' + encodeURIComponent(kind) + '&q=' + encodeURIComponent(q) + (label ? '&label=' + encodeURIComponent(String(label).slice(0, 80)) : '');
84
84
  const iniHref = (id) => '/premium-initiative.html?' + wsQ() + 'id=' + encodeURIComponent(id);
85
85
  const SRC_NAME = { analytics: 'Product analytics', jira: 'Jira query', file: 'A file or export', typed: 'You type it in' };
86
86
  /** A goal's KR id as the design says it: "KR <objective>.<its place in the objective>", in the order of the records; '' with no objective. */
@@ -497,7 +497,9 @@ function drawStrip() {
497
497
  const sp = $('#insHero .sp');
498
498
  if (sp && crit) { sp.classList.remove('ad'); sp.innerHTML = OK.SV(OK.P.alert, 20); }
499
499
  else if (sp) { sp.classList.add('ad'); sp.innerHTML = AD.adler(f.report || (f.okr && f.tone === 'drafts') ? 'needs-you' : (f.okr ? f.tone === 'behind' : behind) || /^Straight answer/.test(f.text) || AD.moodOfText(f.text) === 'honest' ? 'honest' : 'handled', 40, { label: false }); }
500
- $('#askWhy').href = homeAsk('insights', 'Why? ' + f.text);
500
+ // Ask why opens a chat titled from the goal the line is about, else the product's goals, never "ins:insights" (workspace grill W4)
501
+ const goalOf = goalsShown().find((g) => g.title && (f.text || '').includes(g.title));
502
+ $('#askWhy').href = homeAsk('insights', 'Why? ' + f.text, goalOf ? goalOf.title : ((M.board && M.board.name) || 'Product') + ' goals');
501
503
  $('#askWhy').hidden = !!(f.report || f.impact); $('#askWhy').style.display = f.report || f.impact ? 'none' : ''; // a report Claude proposes carries its own presses: See the draft, Not now, ✓ Set it up
502
504
  if (f.report) $('#insAct').innerHTML = RI.stripActs(f.report);
503
505
  const mv = !f.report && f.move ? f.move : null;
@@ -1050,7 +1052,7 @@ function openKr(key) {
1050
1052
  + `<p class="note">${esc(k.decision && !k.need ? 'A decision: it is done when it is decided' + (k.due ? ', due ' + md(day(k.due)) : '') + '.' : k.need ? 'Nothing reads it yet: it needs ' + k.need + '. I will not guess a number.' : 'The OKR file names no source for it.')}</p></div>`
1051
1053
  + (k.source ? `<h3 class="ins-h3">How I would read it</h3><p class="note">${esc('The OKR file says: ' + k.source + '.')}</p>` : '')
1052
1054
  + (k.need && !k.decision ? `<div class="ins-krcon"><span class="sp" aria-hidden="true">${SV(I.spark)}</span><span>${esc('Connect ' + k.need + ' and I test a read before I keep it.')}</span><button type="button" class="cz-ib sm solid" data-krcon aria-label="Open Settings, Connectors" title="Open Connectors">${SV(I.chk)}</button></div>` : '')
1053
- + `<h3 class="ins-h3">Moved by</h3>${k.moves.length ? `<div class="ins-mv">${k.moves.map((r) => initRow(r)).join('')}</div>` : '<p class="note">No initiative is anchored to it yet.</p>'}</div>`;
1055
+ + `<h3 class="ins-h3">Moved by</h3>${k.moves.length ? `<div class="ins-mvl">${k.moves.map((r) => initRow(r)).join('')}</div>` : '<p class="note">No initiative is anchored to it yet.</p>'}</div>`;
1054
1056
  el.addEventListener('click', (e) => { if (e.target.closest('[data-krcon]')) import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); });
1055
1057
  } });
1056
1058
  }
@@ -560,7 +560,12 @@ function draw() {
560
560
  }
561
561
  const ae = document.activeElement, keep = ae && b.contains(ae) ? ae.getAttribute('data-r') : null;
562
562
  syncMore();
563
- b.innerHTML = panelHtml();
563
+ const html = panelHtml();
564
+ // batch 27 item 13 (flicker while a draft is written): the same panel is not built again, and a new one keeps the scroll
565
+ if (R.fromFile && html === b.__rpHtml) return;
566
+ const top = b.scrollTop;
567
+ b.innerHTML = html; b.__rpHtml = html; R.draws = (R.draws || 0) + 1;
568
+ b.scrollTop = top;
564
569
  wire(b);
565
570
  fit(b);
566
571
  const f = R.pop && R.pop !== R.popWas ? b.querySelector('.rp-pops button:not([disabled]), .rp-pops a, .rp-pops input') : null; // a popover that just opened takes the focus
@@ -1009,7 +1014,7 @@ export async function openReport(row, hand = {}) {
1009
1014
  // the template behind one icon (batch 22 item 14, canvas RfH1): format, readers, when it goes out, and its history
1010
1015
  presses: me.setup ? [{ act: 'chat', label: 'Talk to Claude about this report', icon: RF_ICON.chat }, { act: 'template', label: 'Template: format, readers, look, sources', icon: RF_ICON.tune }] : [],
1011
1016
  onPress: (a) => { if (R !== me) return; if (a === 'template') openTemplate(); else if (a === 'chat') openChat(); },
1012
- render: (b) => { syncMore(); b.innerHTML = panelHtml(); wire(b); }, onClose: () => { if (R === me && !me.reopen) { present(false); R = null; } } });
1017
+ render: (b) => { syncMore(); b.__rpHtml = b.innerHTML = panelHtml(); wire(b); }, onClose: () => { if (R === me && !me.reopen) { present(false); R = null; } } });
1013
1018
  vOpen(head());
1014
1019
  try { R.text = await textOf('/api/doc?' + q({ ws: R.ws, path: row.path })); R.W = wordsOf(R.text, R.name); if (!hand.name && R.W.title) R.name = R.W.title; } catch (x) { R.err = String((x && x.message) || x); R.W = { title: R.name, sections: [] }; }
1015
1020
  if (R !== me) return me;
@@ -1032,14 +1037,16 @@ const CHECK_MS = 2500;
1032
1037
  /** Read the report again when its file changed on disk (a chat or a draft run wrote it). Stops when another report opens or the panel closes. */
1033
1038
  async function watchFile(me) {
1034
1039
  const stamp = async () => { const j = await Promise.resolve(pmGet('/api/doc-sheet/stamp?' + q({ ws: me.ws, path: me.row.path }))).catch(() => null); return j && j.ok ? j.hash : null; };
1035
- let seen = await stamp();
1040
+ let seen = await stamp(), pending = false;
1036
1041
  while (R === me) {
1037
1042
  await new Promise((ok) => setTimeout(ok, CHECK_MS));
1038
1043
  if (R !== me) return;
1039
1044
  if (document.hidden || me.edit || me.busy || me.pop || (me.cuts && me.cuts.on === 'sponsors')) continue; // the sponsors cut is read from the file, not the file
1040
1045
  const h = await stamp();
1041
1046
  if (R !== me || !h) continue;
1042
- if (seen && h !== seen) { await reread(); if (R === me && !me.edit && !me.pop) { draw(); readReview(); } }
1047
+ // a session writes the draft in many steps: read it when the file has been still for one more check, so a burst of writes is one redraw
1048
+ if (seen && h !== seen) { seen = h; pending = true; continue; }
1049
+ if (pending) { pending = false; await reread(); if (R === me && !me.edit && !me.pop) { me.fromFile = true; draw(); me.fromFile = false; readReview(); } }
1043
1050
  seen = h;
1044
1051
  }
1045
1052
  }
@@ -76,6 +76,10 @@ export function openReportTab(block, o = {}) {
76
76
  vzStyle(); rpStyle();
77
77
  const id = 'report:' + (block.id || block.proposal.name).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
78
78
  let t = T.get(id);
79
+ // Wawan, 2026-11-04 ("I expect the drafting continue when the feedback continue from chat"): a change Claude writes in the report's own chat, or
80
+ // in the chat the report is open beside, goes to the report's session as the answer lands. The PM's message is the press. An answer that was
81
+ // there when the chat opened opens nothing (home-chat.js autoOpen), so a reload never passes a change twice.
82
+ const linked = RF.passesBySelf(o, block.id, t ? t.chat : null);
79
83
  // a proposal that is set up already opens its report, never the questions again (Wawan, 2026-11-01, batch 22 item 13: "it goes back here again")
80
84
  if (t && block.proposal && t.block.id && !t.gone) return current(t);
81
85
  if (!t && block.proposal) { setTimeout(() => adoptSetUp(id, block.proposal.name), 0); }
@@ -85,12 +89,19 @@ export function openReportTab(block, o = {}) {
85
89
  T.set(id, t);
86
90
  if (block.id && t.panel === 'manage') load(t);
87
91
  } else if (o.chat) t.chat = o.chat;
88
- // a change passes on the PM's press of the chip only; an answer that opens the view by itself shows the report as it is
89
- if (block.id && block.change && !t.passed && !o.auto && !PASSED.has(JSON.stringify(block))) { PASSED.add(JSON.stringify(block)); t.passed = true; passChange(t); return show(t); }
92
+ // a change passes on the PM's press of the chip, or by itself in a chat linked to this report (above); any other answer that opens the view by
93
+ // itself shows the report as it is
94
+ if (block.id && block.change && !t.passed && (!o.auto || linked) && !PASSED.has(JSON.stringify(block))) { PASSED.add(JSON.stringify(block)); t.passed = true; passChange(t, !!o.auto); return show(t); }
90
95
  if (block.id && t.panel === 'current') return current(t);
91
96
  if (block.id && t.panel === 'template') { const v = show(t); loadFlow(t).then(() => { redraw(t); if (!o.auto) makeLook(t); }); return v; }
92
97
  return show(t);
93
98
  }
99
+ /** The set-up report shown beside the chat `chatKey`, by id, or null (Wawan, 2026-11-04). The chat sends it with each message, so feedback in any
100
+ * words reaches that report's draft (lib/report-chat.js wrap). Only a tab opened from that chat counts, and only while it is the view on screen. */
101
+ export function reportBeside(chatKey) {
102
+ const c = vCur(); const t = c && T.get(c.id);
103
+ return t && chatKey && t.chat === chatKey && t.block && t.block.id && !t.block.proposal && !t.gone ? t.block.id : null;
104
+ }
94
105
  /** A chat change to a set-up report's sections or look opens its template (canvas TplA), where it waits as one ✦. */
95
106
  const tplAsk = (b) => !!(b && b.id && !b.change && ((Array.isArray(b.sections) && b.sections.length) || (typeof b.design === 'string' && b.design.trim())));
96
107
  /** The look the PM asked for in words is made first (a design job, as Design's turn), once per chat change, on the PM's press of the chip; when
@@ -147,9 +158,10 @@ async function waitFor(t) {
147
158
  }
148
159
  /** A change Claude passed from the chat: to the report's own session, on the latest draft by its file, then the report opens (RfH2).
149
160
  * An issued latest cycle is frozen: the change is not passed, and the PM is told to change the template instead. */
150
- async function passChange(t) {
161
+ async function passChange(t, auto) {
151
162
  const F = await loadFlow(t);
152
163
  const to = F && F.ok ? RF.changeTargetOf(F) : {};
164
+ if (auto && !to.path && !to.frozen) { PASSED.delete(JSON.stringify(t.block)); t.passed = false; return current(t); } // no draft yet: the chip's press starts the first one
153
165
  let said;
154
166
  if (to.frozen) said = `The ${to.frozen} report is issued, so it is frozen. The change was not passed on: change the template for the next cycle.`;
155
167
  else {
@@ -32,7 +32,8 @@ const at = (stage) => { const k = stageKey(stage); return k ? STEPS.findIndex((s
32
32
  const DELIVER = STEPS.findIndex((s) => s.key === 'deliver');
33
33
  const MONITOR = STEPS.findIndex((s) => s.key === 'monitor');
34
34
  const dayOf = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
35
- export const shortDay = (iso) => { const d = dayOf(iso); return d ? MON[d.getMonth()] + ' ' + d.getDate() : ''; };
35
+ /** "Nov 3", and the short year when the date is not this year ("Jun 23 ’27", workspace grill W4). */
36
+ export const shortDay = (iso, now = new Date()) => { const d = dayOf(iso); return d ? MON[d.getMonth()] + ' ' + d.getDate() + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '') : ''; };
36
37
 
37
38
  const ownWf = (row) => !!(row && row.wf && row.wf.id && row.wf.id !== 'product-feature' && Array.isArray(row.wfStages) && row.wfStage);
38
39
  /** The ◆ stage a milestone waits on for one initiative: the charter's pick, else its workflow's first ◆ stage (Product feature: Deliver). */
@@ -38,10 +38,16 @@ export function chatOf(st = {}, id = '') {
38
38
  placeholder: 'Change the report',
39
39
  context: { title: name, subject: 'A report set up in PMOS (report id ' + id + ')' + (settings ? ': ' + settings : ''),
40
40
  say: 'When the PM asks to change the draft, answer in a few lines and end with ONE fenced code block with the language tag pmos-report holding {"kind":"report","id":"' + id
41
- + '","change":"the change, in the PM\'s words"}. The PM presses its chip to pass it to the report\'s own session. Do not change the draft yourself. The format, readers, schedule, look and sources change with the template icon in the report\'s header.',
41
+ + '","change":"the change, in the PM\'s words, complete"}. PMOS passes it to the report\'s own session as your answer lands, and the draft beside the chat changes; say so in one line. Do not change the draft yourself. The format, readers, schedule, look and sources change with the template icon in the report\'s header.',
42
42
  facts: [['Report id', id]].concat(draft ? [['Latest draft', draft]] : []), files: draft ? [draft] : [] } };
43
43
  }
44
44
 
45
+ /** Whether a change Claude wrote passes to the report's session as the answer lands, without the chip (Wawan, 2026-11-04): only for an answer
46
+ * that opened by itself (o.auto), in the report's own chat or in the chat its tab was opened from (tabChat). Pure. */
47
+ export function passesBySelf(o = {}, id = '', tabChat = null) {
48
+ return !!(o.auto && o.chat && id && (o.chat === chatOf({}, id).key || (tabChat && tabChat === o.chat)));
49
+ }
50
+
45
51
  export const FORMAT_NAMES = { 'google-slides': ['Slides', 'Read aloud or presented.'], 'google-doc': ['Document', 'Read alone, with tables and footnotes.'],
46
52
  pdf: ['PDF', 'A fixed page to read or print.'], 'google-sheets': ['Sheets', 'The numbers as a table.'], confluence: ['Confluence page', 'A page in your wiki.'] };
47
53
  export const CADENCES = [['weekly', 'Every week'], ['bi-weekly', 'Every two weeks'], ['monthly', 'Every month'], ['daily', 'Every day']];
@@ -31,10 +31,10 @@
31
31
  <span class="bd-title" aria-hidden="true">Board</span>
32
32
  <div class="bd-tools">
33
33
  <div class="bd-seg bd-views" id="ibTabs" role="group" aria-label="View">
34
- <button type="button" class="bd-ib" data-v="stages" aria-pressed="false" aria-label="By stage" title="By stage"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h4v16H4zM10 4h4v10h-4zM16 4h4v13h-4z"/></svg></button>
35
- <button type="button" class="bd-ib" data-v="track" aria-pressed="true" aria-label="Stage track" title="Stage track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z"/></svg></button>
36
- <button type="button" class="bd-ib" data-v="time" aria-pressed="false" aria-label="Timeline" title="Timeline"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h9M8 12h11M6 18h7"/></svg></button>
37
- <button type="button" class="bd-ib" data-v="list" aria-pressed="false" aria-label="List" title="List"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg></button>
34
+ <button type="button" class="bd-ib" data-v="stages" aria-pressed="false" aria-label="By stage: a column for each stage" title="By stage: a column for each stage"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4h4v16H4zM10 4h4v10h-4zM16 4h4v13h-4z"/></svg></button>
35
+ <button type="button" class="bd-ib" data-v="track" aria-pressed="true" aria-label="Stage track: each initiative on its stages" title="Stage track: each initiative on its stages"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v4H3zM3 11h18v4H3zM3 17h12v3H3z"/></svg></button>
36
+ <button type="button" class="bd-ib" data-v="time" aria-pressed="false" aria-label="Timeline: each initiative on its dates" title="Timeline: each initiative on its dates"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h9M8 12h11M6 18h7"/></svg></button>
37
+ <button type="button" class="bd-ib" data-v="list" aria-pressed="false" aria-label="List: every initiative, one row each" title="List: every initiative, one row each"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg></button>
38
38
  </div>
39
39
  <!-- the grouping (PgBoard, PgBoardProduct): programs first, or By product; shown only when a program holds an initiative of this board -->
40
40
  <span class="bd-grpw2 bd-hide-s" id="bdGrpw" hidden></span>
@@ -11098,7 +11098,7 @@ writeTmuxConf(); // must exist before restore may reattach or
11098
11098
  // premium-session-end: asked only once the restore has FINISHED. The running server restores in slices after it listens, so
11099
11099
  // checking right after the call saw no live pane on every launch and spawned one more `shell`, which tmux then kept: one idle
11100
11100
  // shell per restart, piling up in the session list.
11101
- restorePersisted({ progressive: !REQUIRED_AS_LIBRARY, onDone: () => firstShell(sessions.values(), { canHost: HOME_ENABLED || workspaces.size > 0, spawn: () => spawnSession({ name: 'shell', agent: 'shell' }) }) });
11101
+ restorePersisted({ progressive: !REQUIRED_AS_LIBRARY, onDone: () => firstShell(sessions.values(), { canHost: HOME_ENABLED || workspaces.size > 0, spawn: () => spawnSession({ name: 'shell', agent: 'shell', origin: 'first-shell' }) }) });
11102
11102
 
11103
11103
  if (require.main === module) {
11104
11104
  // pm-os-absorb Run 2 (R25, X11): one nonce per launch, held in memory only. Never in process.env (so no PTY inherits
@@ -100,8 +100,8 @@ const ITEMS = [
100
100
  const { ctx, page: p, errors } = await open();
101
101
  const sc = await rowOf(p, 'board:scarlet');
102
102
  ok('B one ◇ per milestone: the done Pilot filled, Feature delivered open with "◆ Deliver" in its name; no ◇ for a plain task or one with no due date',
103
- sc && sc.tm.length === 2 && /tg-tm dn\|Pilot with 2 newsrooms · Dec 11 · done/.test(sc.tm[0]) && /^tg-tm\|Feature delivered · Jan 29 · ◆ Deliver$/.test(sc.tm[1]), JSON.stringify(sc));
104
- ok('B said once: the Evaluate task IS the end ◇ and takes its name; it is not drawn again', sc && /^tg-end\|KPI evaluated · Mar 31 · ◆ Evaluate$/.test(sc.end) && !sc.tm.some((x) => /KPI/.test(x)), JSON.stringify(sc));
103
+ sc && sc.tm.length === 2 && /tg-tm dn\|Pilot with 2 newsrooms · Dec 11 · done/.test(sc.tm[0]) && /^tg-tm\|Feature delivered · Jan 29(?: ’\d\d)? · ◆ Deliver$/.test(sc.tm[1]), JSON.stringify(sc));
104
+ ok('B said once: the Evaluate task IS the end ◇ and takes its name; it is not drawn again', sc && /^tg-end\|KPI evaluated · Mar 31(?: ’\d\d)? · ◆ Evaluate$/.test(sc.end) && !sc.tm.some((x) => /KPI/.test(x)), JSON.stringify(sc));
105
105
  ok('S Year puts no date under a ◇: its hover names it (batch 13 item 4)', sc && !sc.tml.length, JSON.stringify(sc && sc.tml));
106
106
  const pk = await rowOf(p, 'board:passkey');
107
107
  ok('B a late milestone: an orange outline, and the row\'s dot turns orange', pk && /tg-tm late\|Beta to staff · Oct 16 · late/.test(pk.tm[0]) && !pk.tml.length && /cz-dot late/.test(pk.dot), JSON.stringify(pk));
@@ -102,7 +102,7 @@ const ROWS = [
102
102
  ok('A the done part fills the pieces in order and skips the gap: the first piece fills before the second starts', otp && otp.bars[0].done > 0 && (otp.bars[0].done === 100 || otp.bars[1].done === 0), JSON.stringify(otp && otp.bars));
103
103
  const rc = await rowOf(p, 'Recovery codes');
104
104
  ok('A a plain gap in the dated work breaks the bar with a hairline that is no hold: no label (batch 11 item 13)', rc && rc.bars.length === 2 && !rc.ho.length && !rc.old
105
- && rc.ng.length === 1 && rc.ng[0].tag === 'BUTTON' && rc.ng[0].label === 'Nothing planned, Oct 21 to Jan 9 · 12 wk. Plan skipped' && rc.ng[0].sk, JSON.stringify(rc));
105
+ && rc.ng.length === 1 && rc.ng[0].tag === 'BUTTON' && /^Nothing planned, Oct 21 to Jan 9(?: ’\d\d)? · 12 wk\. Plan skipped$/.test(rc.ng[0].label) && rc.ng[0].sk, JSON.stringify(rc));
106
106
  const pk = await rowOf(p, 'Passkey sign-in');
107
107
  ok('A a row with no hold and no gap keeps one whole bar', pk && pk.bars.length === 1 && !/x[lr]/.test(pk.bars[0].cls), JSON.stringify(pk));
108
108
  const lg = await p.evaluate(() => (document.querySelector('#bdChipset .tg-lgd') || {}).textContent || '');
@@ -223,11 +223,11 @@ const ROWS = [
223
223
  // the press: a future gap's card with its words and + New task with its dates
224
224
  await p.click('#ibBody [data-row="board:recovery-codes"] .tg-ng');
225
225
  const card = await p.evaluate(() => { const c = document.querySelector('.tg-gpc'); return c ? { words: [...c.querySelectorAll('p')].map((x) => x.textContent), add: (c.querySelector('.gp-add') || {}).getAttribute && c.querySelector('.gp-add').getAttribute('aria-label') } : null; });
226
- ok('X a future gap opens a card with its words and + New task with the gap\'s dates', card && card.words.join('|') === 'Nothing planned, Oct 21 to Jan 9 · 12 wk|Plan skipped' && card.add === 'New task, Oct 21 to Jan 9', JSON.stringify(card));
226
+ ok('X a future gap opens a card with its words and + New task with the gap\'s dates', card && /^Nothing planned, Oct 21 to Jan 9(?: ’\d\d)? · 12 wk\|Plan skipped$/.test(card.words.join('|')) && /^New task, Oct 21 to Jan 9(?: ’\d\d)?$/.test(card.add), JSON.stringify(card));
227
227
  await shot(p, 'timeline-gap-card-1440');
228
228
  await p.click('.tg-gpc .gp-add'); await p.waitForTimeout(400);
229
229
  const add = await p.evaluate(() => { const f = document.querySelector('#ivAddTask'); return f ? f.placeholder : null; });
230
- ok('X + opens the initiative with New task ready, the gap\'s dates named', add === 'A task for Recovery codes, Oct 21 to Jan 9', String(add));
230
+ ok('X + opens the initiative with New task ready, the gap\'s dates named', /^A task for Recovery codes, Oct 21 to Jan 9(?: ’\d\d)?$/.test(String(add)), String(add));
231
231
  ok('X the gap press threw nothing', !errors.length, errors.join('\n'));
232
232
  await ctx.close();
233
233
  }