create-pmos 0.518.0 → 0.520.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 (38) hide show
  1. package/package.json +39 -47
  2. package/payload/AGENTS.md +1 -1
  3. package/payload/KIT-VERSION +1 -1
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/workspace/board-columns-test.js +1 -1
  8. package/workspace/board-programs-test.js +26 -7
  9. package/workspace/briefing-premium-board-test.js +3 -3
  10. package/workspace/calm-timeline-test.js +2 -2
  11. package/workspace/changelog.json +38 -0
  12. package/workspace/guide/agent-capabilities.md +2 -1
  13. package/workspace/guide/descriptions.md +43 -0
  14. package/workspace/guide/index.md +3 -2
  15. package/workspace/guide/initiatives-roadmap.md +1 -0
  16. package/workspace/guide/next-prompt.md +4 -2
  17. package/workspace/guide/programs.md +2 -1
  18. package/workspace/guide/settings.md +1 -1
  19. package/workspace/lib/board-trail.js +8 -3
  20. package/workspace/lib/description-props.js +214 -0
  21. package/workspace/lib/headless.js +12 -2
  22. package/workspace/lib/initiative-cli.js +28 -2
  23. package/workspace/lib/initiative-writer.js +41 -1
  24. package/workspace/lib/proposals.js +13 -4
  25. package/workspace/lib/routes-next-prompt.js +67 -27
  26. package/workspace/lib/routes-proposals.js +5 -0
  27. package/workspace/package-lock.json +2 -2
  28. package/workspace/package.json +1 -1
  29. package/workspace/public/pm/bd-span.js +13 -17
  30. package/workspace/public/pm/cozy-settings.js +8 -7
  31. package/workspace/public/pm/ini-about.js +26 -11
  32. package/workspace/public/pm/ini-props.js +5 -3
  33. package/workspace/public/pm/ini-view-model.js +1 -1
  34. package/workspace/public/pm/ini-view.css +1 -0
  35. package/workspace/public/pm/ini-view.js +1 -1
  36. package/workspace/public/pm/premium-vw-guide.css +7 -2
  37. package/workspace/server.js +15 -1
  38. package/workspace/test-lists/initiative-description-field.json +5 -0
@@ -524,7 +524,7 @@ function scaled(rows, M, o) {
524
524
  const msO = { sp, reach: Math.max(1, Math.round(40 / pxDay)), mode: o.msMode || 'pinned', noLabel: !!(C && C.msOn) }; // a row's milestone ◇s closer than ~40 px merge (batch 11 item 3); the Filter's milestones (batch 13 item 4)
525
525
  const ov = overlay(W, X, now, sprint, rel), pxPct = (tpx * (W.fixed ? 1 : ratio)) / 100;
526
526
  const ra = rel && day(rel.iso), sa = sprint && day(sprint.start), sb = sprint && day(sprint.end);
527
- // an undated program member's track says so beside today, where the view opens
527
+ // an undated initiative's track says so beside today, where the view opens
528
528
  const ND = `<span class="tg-nd" style="left:${P(Math.min(90, Math.max(0, X(now))))}">No dates</span>`;
529
529
  const askBtn = none.length ? `<a class="bd-chip tg-ask" id="askDates" href="${esc(o.askHref(none))}">${SV('M4 5h16v11h-9l-5 4v-4H4z')}Ask to propose dates</a>` : '';
530
530
  const ticks = W.ticks.map((k) => `<span style="left:${P(X(k.at))}" data-y="${k.y}" data-base="${esc(k.base)}"${k.plain ? ' data-plain' : ''}><b>${esc(k.label)}</b><small>${esc(k.sub)}</small></span>`).join('');
@@ -533,23 +533,19 @@ function scaled(rows, M, o) {
533
533
  const back = `<button type="button" class="tg-tback" data-today hidden aria-label="Back to today">${SV(CHL)}<span>Today</span>${SV(CHR)}</button>`;
534
534
  const attrs = `data-span="${sp}" data-from="${+W.from}" data-to="${+W.to}" data-ratio="${W.fixed ? 1 : ratio.toFixed(5)}" data-now="${+now}"${W.fixed ? ' data-fixed' : ''}`;
535
535
  const style = `--tw:${Math.round(tpx * (W.fixed ? 1 : ratio))}px`;
536
- // a program keeps its undated initiatives as rows that say "No dates" (feedback batch 8 item 4: initiatives just moved into a program
537
- // were missing under it); a product group keeps only the dated ones, and the foot names the rest
538
- // by key, not by identity: By product copies a program's row ({ ...r, inProgram }), and that copy is never in `list` (batch 11 item 12: a
539
- // program's initiatives were missing from their product on the timeline)
540
- // By product, a program's undated initiative keeps its "No dates" row under its product too (batch 11 item 19: they disappeared once grouped by product)
541
- const onList = new Set(list.map((r) => r.key));
542
- // a product with no dated initiative keeps its head and its initiatives as "No dates" rows, so the product is not missing from the Board
543
- // (batch 14 item 2: pmos-microsites, all undated outside its program, had no group at all); those rows leave the foot's names
544
- const keepOf = (R) => { const k = R.filter((r) => onList.has(r.key) || r.inProgram); return k.length ? k : R.slice(); };
545
- const shownNd = new Set(o.groups ? o.groups.filter((g) => g.kind !== 'program' && !g.rows.some((r) => onList.has(r.key) || r.inProgram)).flatMap((g) => g.rows.map((r) => r.key)) : []);
546
- const footNone = none.filter((r) => !shownNd.has(r.key));
536
+ // every undated initiative is a row that says "No dates" beside today, in every grouping and in no program too (batch 26 item 8, Wawan:
537
+ // the foot alone hid them; this replaces batch 8 item 4's program-only rule and design B6). By product copies a program's row
538
+ // ({ ...r, inProgram }), so a row is matched by key, not by identity (batch 11 item 12)
539
+ const shownKeys = new Set(o.groups ? o.groups.flatMap((g) => (g.kind === 'program' ? g.products.flatMap((x) => x.rows) : g.rows).map((r) => r.key)) : rows.map((r) => r.key));
540
+ // the foot keeps the count and "Ask to propose dates"; it names only an undated initiative no group draws (none, as the Board groups today)
541
+ const footNone = none.filter((r) => !shownKeys.has(r.key));
547
542
  // a timeline reads in time (batch 11 item 22): each group's rows by start, then end; undated ones last. The health dot still says late or at risk
548
543
  const at = (r) => { const x = extentOf(r, now); return x ? [+x.a, +x.b] : [Infinity, Infinity]; };
549
544
  // a pressed column title sorts each group by that column instead (StsA8Board, as in the List)
550
545
  const byTime = (L) => (C && C.order ? L.slice().sort(C.order) : L.map((r) => [at(r), r]).sort((p, q) => p[0][0] - q[0][0] || p[0][1] - q[0][1]).map((x) => x[1]));
551
546
  list.splice(0, list.length, ...byTime(list));
552
- const G = o.groups ? o.groups.map((g) => (g.kind === 'program' ? { ...g, products: g.products.filter((x) => x.rows.length).map((x) => ({ ...x, rows: byTime(x.rows) })) } : { ...g, rows: byTime(keepOf(g.rows)) })).filter((g) => g.rows.length || g.kind === 'program') : null;
547
+ const flat = list.concat(none);
548
+ const G = o.groups ? o.groups.map((g) => (g.kind === 'program' ? { ...g, products: g.products.filter((x) => x.rows.length).map((x) => ({ ...x, rows: byTime(x.rows) })) } : { ...g, rows: byTime(g.rows) })).filter((g) => g.rows.length || g.kind === 'program') : null;
553
549
  const empty = '<p class="tg-empty">No initiative on screen has a date on record.</p>';
554
550
  const ext = (r) => { const x = extentOf(r, now); return x ? ` data-a="${X(x.a).toFixed(3)}" data-b="${X(x.b).toFixed(3)}"` : ''; };
555
551
  // a product's head carries its lane from Quarter on (batch 11 item 17); Sprint on a phone, and a page that gives no head cell, keep the plain head
@@ -561,9 +557,9 @@ function scaled(rows, M, o) {
561
557
  if (g.kind !== 'program') return lane(g) + (o.folded(g.key) ? '' : g.rows.map(one).join(''));
562
558
  const st = o.progState(g.program), shut = o.folded(g.key), tr = shut ? rollUp(g.rows, progSpan(g), X, W, o, msO, pxPct, now) : '';
563
559
  return `<div class="tgq-r tg-pg${shut ? ' closed' : ''}" data-prow="${esc(g.program.id)}"><div class="tgq-n tg-pgn">${progName(g, st, o, shut ? offDots(g.rows, now) : '')}</div><div class="tg-p">${ov}${tr}</div></div>` + (shut ? '' : g.products.map((x) => x.rows.map(one).join('')).join(''));
564
- }).join('') : list.map(one).join('');
560
+ }).join('') : flat.map(one).join('');
565
561
  const top = `<div class="tgq-h"><div class="tg-p">${ticks}${pill}</div></div>`;
566
- const below = none.length ? `<div class="tg-none">${footNone.length ? `<span>${footNone.length} ${footNone.length === 1 ? 'has' : 'have'} no dates</span>` : ''}${askBtn}</div>` : '';
562
+ const below = none.length ? `<div class="tg-none"><span>${none.length} ${none.length === 1 ? 'has' : 'have'} no dates</span>${askBtn}</div>` : '';
567
563
  // the Deliveries lane (batch 13 item 4, XcalmA6Phone): a phone shows the next deliveries above the rows (o.lane draws it; none: nothing)
568
564
  const lanePh = o.lane ? o.lane({ W, X, rows: list, groups: G, now, phone: true, pxPct }) || '' : '';
569
565
  return { html: lanePh + `<div class="tg tg-s tgq" ${attrs}><div class="tg-scw"><div class="tg-sc" tabindex="0" role="group" aria-label="Timeline" style="${style}"><div class="tg-in">${top}<div class="tg-body">${body || empty}</div></div></div>${back}<i class="tg-fade l" aria-hidden="true"></i><i class="tg-fade r" aria-hidden="true"></i></div></div>${below}`, none };
@@ -581,11 +577,11 @@ function scaled(rows, M, o) {
581
577
  if (g.kind !== 'program') return lane(g) + (o.folded(g.key) ? '' : g.rows.map((r) => one(r, ' tg-gr')).join(''));
582
578
  const lp = g.products[g.products.length - 1], last = lp ? lp.rows[lp.rows.length - 1] : null;
583
579
  return progRow(g, o.progState(g.program), X, W, ov, o, msO, pxPct, now) + (o.folded(g.key) ? '' : g.products.map((x) => (g.products.length > 1 ? o.subHtml(x.name) : '') + x.rows.map((r) => one(r, ' tg-kid' + (r === last ? ' last' : ''))).join('')).join(''));
584
- }).join('') : list.map((r) => one(r)).join('');
580
+ }).join('') : flat.map((r) => one(r)).join('');
585
581
  // the sprint band and the release keep their labels under the axis on Quarter, where a week is wide enough to read them
586
582
  const axl = sp === 'quarter' ? `${sa && sb && sb > W.from && sa < W.to ? `<span class="tg-axl sb" style="left:${P(X(sa))}">Jira ${esc(sprint.name.toLowerCase())} · ${esc(md(sa))} – ${esc(md(sb))}</span>` : ''}${ra && ra > W.from && ra < W.to ? `<span class="tg-axl rl" style="left:${P(X(ra))}">${SV(FLAG)}${esc(rel.name)} · ${esc(md(ra))}</span>` : ''}` : '';
587
583
  const axis = axl ? `<div class="tg-ax"><span class="tg-n"></span><div class="tg-p">${axl}</div></div>` : '';
588
- const foot = none.length ? `<div class="tg-ft">${footNone.length ? `<span class="tg-none"><b>${footNone.length} ${footNone.length === 1 ? 'has' : 'have'} no dates:</b> <span>${esc(footNone.slice(0, 4).map((r) => r.title).join(', '))}${footNone.length > 4 ? ' and ' + (footNone.length - 4) + ' more' : ''}</span>${askBtn}</span>` : `<span class="tg-none">${askBtn}</span>`}</div>` : '';
584
+ const foot = none.length ? `<div class="tg-ft"><span class="tg-none"><b>${none.length} ${none.length === 1 ? 'has' : 'have'} no dates${footNone.length ? ':' : ''}</b>${footNone.length ? ` <span>${esc(footNone.slice(0, 4).map((r) => r.title).join(', '))}${footNone.length > 4 ? ' and ' + (footNone.length - 4) + ' more' : ''}</span>` : ''}${askBtn}</span></div>` : '';
589
585
  // the Deliveries lane (batch 13 item 4, XcalmA6Lane): one row under the axis, a list per column (o.lane draws it; none: nothing)
590
586
  const laneH = o.lane ? o.lane({ W, X, rows: list, groups: G, now, phone: false, pxPct }) || '' : '';
591
587
  // the columns' width is the name column's (--nw: the sticky block, the left fade and the Deliveries fold all read it)
@@ -270,18 +270,19 @@ function claudeSec() {
270
270
  return '<h4>Claude login and profile</h4><div data-claude-setup></div>'
271
271
  + trow('API key', keySub, keyRight)
272
272
  + (k.present && C.off ? trow('Runs on the API', 'Off: PMOS_GRILL_API is off in the environment.', '<span class="ev-pill">Off</span>') : '')
273
- + sharpenRows(k.present);
273
+ + sharpenRows();
274
274
  }
275
275
  /* Settings › Claude › Sharpen prompts (batch 26 item 2; pm/next-prompt-ui.js, lib/routes-next-prompt.js): the suggestions after a reply, Sharpen as I
276
- * type ('auto' is on when Claude runs on the API key, where a sharpen takes about 2 s), the style, and the PM's own rules for Your own.
276
+ * type (on by default: both run as a background `claude -p` in the PM's own Claude session, Wawan 2026-11-04), the style, and Your own rules.
277
277
  * The same style and switch are in the chat's Sharper card menu; a save here tells the chat (the event pmos:prompt-prefs). */
278
278
  const NPK = { 'prompt.next': ['nextSet', 'next'], 'prompt.sharpen': ['sharpenSet', 'sharpen'], 'prompt.style': ['style', 'style'], 'prompt.rules': ['rulesText', 'rules'] }; // row key → [GET field, POST key]
279
279
  const setVal = (k, d) => { const v = T.np ? T.np[NPK[k][0]] : undefined; return v !== undefined && v !== null ? v : d; };
280
- function sharpenRows(api) {
280
+ function sharpenRows() {
281
281
  const next = setVal('prompt.next', true) !== false, sh = setVal('prompt.sharpen', 'auto'), style = setVal('prompt.style', 'engineer'), rules = String(setVal('prompt.rules', '') || '');
282
- const on = sh === 'on' || (sh === 'auto' && api);
283
- const shSub = sh === 'auto' ? (api ? 'On, because Claude runs on your API key: the sharper version shows in about 2 seconds.' : 'Off for now: without an API key each one takes 10 seconds or more. Turn it on to use it anyway.')
284
- : on ? 'When you pause on a rough prompt, a sharper version shows above the box.' : 'Off. No spinner and no Sharper card.';
282
+ const on = sh !== 'off' && !(T.np && T.np.sharpen === false && sh === 'auto');
283
+ const down = T.np && T.np.apiDown;
284
+ const shSub = !on ? (down && down.auth ? 'Off for now: your API key was refused. Replace it under API key above.' : 'Off. No spinner and no Sharper card.')
285
+ : 'When you pause on a rough prompt, Claude writes a sharper version above the box. It runs in your Claude session and takes a few seconds.';
285
286
  const styleSub = style === 'polish' ? 'Clearer and shorter, your meaning kept.' : style === 'own' ? (rules.trim() ? 'Your rules below.' : 'Write your rules below first.') : 'Builds the best prompt for the job.';
286
287
  return '<h4>Sharpen prompts</h4>'
287
288
  + trow('Suggest the next prompt', 'After a reply, up to two prompts above the box. A tap puts one in the box. Nothing sends.', sw(next, 'data-ev-np="prompt.next"', 'Suggest the next prompt'))
@@ -380,7 +381,7 @@ async function press(e) {
380
381
  }
381
382
  if (t.dataset.evSeg === 'prompt.style') { await savePrompt('prompt.style', t.dataset.v, 'Saved. Sharper prompts follow this style.'); return; }
382
383
  if (t.dataset.evNp === 'prompt.next') { const on = setVal('prompt.next', true) === false; await savePrompt('prompt.next', on, on ? 'Saved. Claude suggests the next prompt after a reply.' : 'Saved. No suggestions after a reply.'); return; }
383
- if (t.dataset.evNp === 'prompt.sharpen') { const api = !!(T.cl && T.cl.key && T.cl.key.present), sh = setVal('prompt.sharpen', 'auto'), was = sh === 'on' || (sh === 'auto' && api); await savePrompt('prompt.sharpen', was ? 'off' : 'on', was ? 'Saved. Sharpen as I type is off.' : 'Saved. Sharpen as I type is on.'); return; }
384
+ if (t.dataset.evNp === 'prompt.sharpen') { const sh = setVal('prompt.sharpen', 'auto'), was = sh !== 'off' && !(T.np && T.np.sharpen === false && sh === 'auto'); await savePrompt('prompt.sharpen', was ? 'off' : 'on', was ? 'Saved. Sharpen as I type is off.' : 'Saved. Sharpen as I type is on.'); return; }
384
385
  if ('evRulesave' in t.dataset) { const ta = T.el.querySelector('#evRules'); await savePrompt('prompt.rules', ta ? ta.value.slice(0, 1000) : '', 'Saved. Your own rules are used when Sharpen as is Your own.'); return; }
385
386
  if (t.dataset.evSeg && t.dataset.evSeg.startsWith('adler.')) { const r = await AD.savePref(pmPost, t.dataset.evSeg, t.dataset.v); T.msg = r && r.ok ? 'Saved.' : (r && r.error) || 'Not saved.'; draw(); return; }
386
387
  if (t.dataset.evDress) {
@@ -7,6 +7,12 @@
7
7
  * aboutHtml(v, o, open) → the markup: the header press (data-iv-mc="ab") with its one-line summary, then the two columns when open
8
8
  * resetAbout() forget the descriptions read (for the tests)
9
9
  *
10
+ * The description (initiative-description-field): the record's own `description:` line (v.desc, from the board trail) wins. When the PRD's
11
+ * paragraph says more than it, the PRD's shows once below it, marked "From the PRD". With no line, the PRD's paragraph shows as before. With
12
+ * neither, "Ask Claude to draft it" opens Home chat with a prompt that names the id, so the kept text is saved to the record
13
+ * (`pmos initiative describe`). Claude also drafts it by itself under the ✦ (lib/description-props.js).
14
+ * saysMore(desc, prd) → true when the PRD's paragraph is fuller than the description and not in it yet. Pure.
15
+ *
10
16
  * The success measure is the one Claude drafts (pm/metric-drafts.js, GET /api/metric-drafts): its proposal keeps its own card and presses; a
11
17
  * kept measure is listed here as rows, each with its Now → Target. A row that also carries `base` (where it started) and `pace` (how far it
12
18
  * should be by today, 0 to 1) draws as a track with a tick at the pace and a verdict: On pace, Ahead of pace, Behind pace, Short of target,
@@ -21,7 +27,7 @@ const STALE_DAYS = 30;
21
27
  const MAX_CHARS = 700;
22
28
 
23
29
  /* ── the description ── */
24
- const plain = (s) => String(s).replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*_`]+/g, '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
30
+ const plain = (s) => String(s).replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*`]+/g, '').replace(/(^|\W)_+(?=\w)|(?<=\w)_+(?=\W|$)/g, '$1').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
25
31
  /** The description a PRD gives. */
26
32
  export function descOf(md) {
27
33
  let t = String(md || '').replace(/\r\n?/g, '\n');
@@ -64,6 +70,9 @@ export function want(ws, path, onChange) {
64
70
  return one;
65
71
  }
66
72
  const descFor = (ws, path) => READ.get((ws || '') + '|' + path) || null;
73
+ const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
74
+ /** The PRD says more: not already in the description, and fuller (the same rule as lib/description-props.js). */
75
+ export function saysMore(desc, prd) { const d = norm(desc), q = norm(prd); return !!q && !d.includes(q.slice(0, 60)) && !q.includes(d) ? q.length > d.length + 40 : false; }
67
76
  export const resetAbout = () => { READ.clear(); OPEN.clear(); LASTWS = ''; };
68
77
  const OPEN = new Set(); // initiative ids whose description is open past three lines, for the session
69
78
 
@@ -96,7 +105,7 @@ const rowHtml = (r, o) => {
96
105
  const homeHref = (ws, id, q) => { const u = new URLSearchParams(); if (ws) u.set('ws', ws); u.set('ask', id); u.set('q', q); return '/premium.html?' + u.toString(); };
97
106
  /** The folded line: the first measure's dot and value, or the one next action. */
98
107
  function summaryOf(v, M, d, o) {
99
- const none = !d || d.state !== 'ok';
108
+ const none = !v.desc && (!d || d.state !== 'ok');
100
109
  if (M && M.mode === 'kept') {
101
110
  const r = M.rows[0], t = trackOf(r, o), st = t ? t.state : r.now == null ? 'unread' : '';
102
111
  const val = r.now == null && !r.nowTxt ? 'not read yet' : nowText(r) + (r.targetTxt ? ' → ' + r.targetTxt : '');
@@ -108,18 +117,24 @@ function summaryOf(v, M, d, o) {
108
117
  }
109
118
  export function aboutHtml(v, o, open) {
110
119
  const id = v.id || v.key || '', ws = o.ws || LASTWS;
111
- const prd = v.links && v.links.prd ? v.links.prd : '';
120
+ const prd = v.links && v.links.prd ? v.links.prd : v.prd || ''; // the trail's PRD path (v.prd); v.links is the task links on the Board
112
121
  const d = prd ? descFor(ws, prd) : null;
113
122
  const M = MD.measuresOf(id);
114
123
  v = { ...v, docState: !prd ? 'none' : d ? d.state : 'loading' };
115
124
  const sum = summaryOf(v, M, d, o);
116
- let desc;
117
- if (d && d.state === 'ok') {
118
- const long = d.text.length > 260, shut = long && !OPEN.has(id);
119
- desc = `<p class="ab-p${shut ? ' cl' : ''}">${esc(d.text)}</p>${long ? `<button type="button" class="ab-lk" data-ab-more="${esc(id)}" aria-expanded="${!shut}">${shut ? 'Show more' : 'Show less'}</button>` : ''}`
120
- + (o.doc ? `<button type="button" class="ab-lk" data-iv-doc="${esc(prd)}">Open the PRD ${SVG(P.go, 14, 2.2)}</button>` : '');
121
- } else if (prd && (!d || d.state === 'loading')) desc = '<p class="ab-p q">Reading the PRD…</p>';
122
- else desc = `<p class="ab-p q">No description yet.</p><a class="ab-lk" href="${esc(homeHref(ws, id, 'Draft the description of ' + (v.title || id) + ': '))}">${SVG(P.chat, 14)} Ask Claude to draft it</a>`;
125
+ let desc, fromPrd = false;
126
+ const para = (key, text, cls = '') => {
127
+ const long = text.length > 260, shut = long && !OPEN.has(key);
128
+ return `<p class="ab-p${cls}${shut ? ' cl' : ''}">${esc(text)}</p>${long ? `<button type="button" class="ab-lk" data-ab-more="${esc(key)}" aria-expanded="${!shut}">${shut ? 'Show more' : 'Show less'}</button>` : ''}`;
129
+ };
130
+ const openPrd = o.doc ? `<button type="button" class="ab-lk" data-iv-doc="${esc(prd)}">Open the PRD ${SVG(P.go, 14, 2.2)}</button>` : '';
131
+ const own = v.desc ? String(v.desc) : '';
132
+ if (own) {
133
+ // the record's own description wins; the PRD's paragraph shows once, below, only when it says more
134
+ desc = para(id, own) + (d && d.state === 'ok' && saysMore(own, d.text) ? `<h5 class="ab-from">From the PRD</h5>${para(id + '|prd', d.text)}${openPrd}` : '');
135
+ } else if (d && d.state === 'ok') { fromPrd = true; desc = para(id, d.text) + openPrd; }
136
+ else if (prd && (!d || d.state === 'loading')) desc = '<p class="ab-p q">Reading the PRD…</p>';
137
+ else desc = `<p class="ab-p q">No description yet.</p><a class="ab-lk" href="${esc(homeHref(ws, id, 'Draft the description of ' + (v.title || id) + ' (' + id + ') from what you can read, and save it to the initiative once I keep it: '))}">${SVG(P.chat, 14)} Ask Claude to draft it</a>`;
123
138
  let met;
124
139
  if (M && M.mode === 'open') met = MD.iniHtml(id, ws, v.title);
125
140
  else if (M && M.mode === 'kept') met = `<div class="ab-ml">${M.rows.map((r) => rowHtml(r, o)).join('')}</div>`
@@ -127,7 +142,7 @@ export function aboutHtml(v, o, open) {
127
142
  else met = `<p class="ab-p q">No success measure yet.</p><a class="ab-lk" href="${esc(homeHref(ws, id, 'Propose a success measure for ' + (v.title || id) + ': '))}">${SVG(P.chat, 14)} Ask Claude to propose one</a>`;
128
143
  const anchor = v.anchor ? `<span>Anchored to ${esc(v.anchor)}</span>` : '';
129
144
  return `<section class="iv-ab" aria-label="About"><button type="button" class="iv-ab-h" data-iv-mc="ab" aria-expanded="${!!open}"><b>About</b>${sum}<span class="chev" aria-hidden="true">${SVG(P.down, 16, 2)}</span></button>`
130
- + (open ? `<div class="iv-ab-b"><div class="ab-d"><h4>Description${d && d.state === 'ok' ? ' <span>From the PRD</span>' : ''}</h4>${desc}</div><div class="ab-m"><h4>Success measure ${anchor}</h4>${met}</div></div>` : '') + '</section>';
145
+ + (open ? `<div class="iv-ab-b"><div class="ab-d"><h4>Description${fromPrd ? ' <span>From the PRD</span>' : ''}</h4>${desc}</div><div class="ab-m"><h4>Success measure ${anchor}</h4>${met}</div></div>` : '') + '</section>';
131
146
  }
132
147
  /** The banner's one measure chip (R2): the first kept measure's dot and value, or a proposal's mark; '' when there is none. */
133
148
  export function chipHtml(v, o = {}) {
@@ -36,7 +36,7 @@ const md = (iso) => { const m = DAY.exec(String(iso || '')); return m ? MON[+m[2
36
36
  const span = (a, b) => (a && b && a !== b ? md(a) + ' – ' + md(b) : md(a || b));
37
37
  const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
38
38
  const clip = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
39
- const GROUP = { outcome: 'Expected outcome', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
39
+ const GROUP = { description: 'Description', outcome: 'Expected outcome', found: 'Found elsewhere', dates: 'Dates', status: 'Looks done', names: 'Names', links: 'Links', grill: 'Claude grilled it' };
40
40
 
41
41
  /* ── the chip and the live look ── */
42
42
  export function chipOf(p) {
@@ -94,10 +94,11 @@ const titleOf = (p) => {
94
94
  if (p.group === 'found') return p.type === 'stage' ? 'Stage: ' + ((p.data && p.data.stage) || '') : p.type === 'docs' ? plural(n, 'document', 'documents') : p.type === 'tasks' ? plural(n, 'task', 'tasks') : p.type === 'link' ? 'Already in another initiative' : p.type === 'maybe' ? 'Maybe this initiative' : plural(n, 'thing', 'things') + ' not placed';
95
95
  if (p.group === 'status') return plural(n, 'task looks', 'tasks look') + ' done';
96
96
  if (p.group === 'names') return 'Tidy ' + plural(n, 'name', 'names');
97
+ if (p.group === 'description') return p.type === 'enrich' ? 'A fuller description from the PRD' : 'What this initiative is';
97
98
  if (p.group === 'outcome') return p.type === 'date' ? 'A date to check the outcome' : 'What should change once it ships';
98
99
  return '';
99
100
  };
100
- const iconOf = (p) => (p.group === 'outcome' ? I.goal : p.group === 'found' ? (p.type === 'stage' ? I.stage : p.type === 'docs' ? I.doc : p.type === 'tasks' ? I.tasks : p.type === 'link' ? I.link : I.maybe)
101
+ const iconOf = (p) => (p.group === 'description' ? I.doc : p.group === 'outcome' ? I.goal : p.group === 'found' ? (p.type === 'stage' ? I.stage : p.type === 'docs' ? I.doc : p.type === 'tasks' ? I.tasks : p.type === 'link' ? I.link : I.maybe)
101
102
  : p.group === 'dates' ? I.cal : p.group === 'status' ? I.done : p.group === 'names' ? I.name : I.link);
102
103
  const ib = (attr, label, icon, cls = '') => `<button type="button" class="pp-ib${cls ? ' ' + cls : ''}" ${attr} aria-label="${esc(label)}" title="${esc(label)}">${SVG(icon, 16, 2.2)}</button>`;
103
104
  function itemHtml(p, x) {
@@ -110,8 +111,9 @@ function itemHtml(p, x) {
110
111
  } else if (p.group === 'dates') { t = x.title || x.task; m = (x.from && x.from.due ? md(x.from.due) + ' → ' : '') + span(x.to && x.to.start, x.to && x.to.due) + (x.jira ? ' · Jira: a drafted comment' : '') + (x.ms ? ' · ◇ a promise' : ''); }
111
112
  else if (p.group === 'status') { t = x.title || taskTitle(p.ini, x.task); m = x.jira ? 'Jira: a drafted comment' : ''; }
112
113
  else if (p.group === 'outcome') { t = x.title; m = x.due ? 'Check on ' + md(x.due) : ''; }
114
+ else if (p.group === 'description') { t = (p.data && p.data.text) || x.title; m = p.type === 'enrich' && p.data && p.data.from ? 'Now: ' + clip(p.data.from, 120) : ''; }
113
115
  else if (p.group === 'names') { t = x.to; m = 'Was: ' + (x.from || '') + (x.jira ? ' · Jira: a drafted comment' : ''); }
114
- return `<div class="pp-it"><span class="w"><b>${esc(clip(t, 90))}</b>${m ? `<small>${esc(m)}</small>` : ''}</span>${x.sig ? ib(`data-pp-dropit="${esc(x.sig)}"`, 'Leave this one out', I.x) : ''}</div>`;
116
+ return `<div class="pp-it"><span class="w"><b>${esc(clip(t, p.group === 'description' ? 700 : 90))}</b>${m ? `<small>${esc(m)}</small>` : ''}</span>${x.sig ? ib(`data-pp-dropit="${esc(x.sig)}"`, 'Leave this one out', I.x) : ''}</div>`;
115
117
  }
116
118
  function linkHtml(p, l) {
117
119
  const pick = S.choices[l.key] || l.rec;
@@ -322,7 +322,7 @@ export function iniView(r, ctx = {}) {
322
322
  summary.wf = r.wf || { id: PF_ID, name: 'Product feature', icon: 'feature' };
323
323
  summary.program = ctx.program || null;
324
324
  const today2 = (n) => String(n).padStart(2, '0');
325
- return { anchor: r.kr || '', vs: r.__vs || null, links: r.__vs && r.__vs.draft ? null : r.links || null, id: r.id || null, key: r.key, title: r.title, today: today.getFullYear() + '-' + today2(today.getMonth() + 1) + '-' + today2(today.getDate()), summary, stage, tasks, docs: ctx.docs != null ? ctx.docs : null, linked: linkedOf(r), jiraAt: jiraIn ? ctx.jiraAt || '' : '', jiraRead: ctx.jiraReadAt || '',
325
+ return { anchor: r.kr || '', vs: r.__vs || null, links: r.__vs && r.__vs.draft ? null : r.links || null, prd: (r.trail && r.trail.links && r.trail.links.prd) || null, desc: (r.trail && r.trail.desc) || r.desc || null, id: r.id || null, key: r.key, title: r.title, today: today.getFullYear() + '-' + today2(today.getMonth() + 1) + '-' + today2(today.getDate()), summary, stage, tasks, docs: ctx.docs != null ? ctx.docs : null, linked: linkedOf(r), jiraAt: jiraIn ? ctx.jiraAt || '' : '', jiraRead: ctx.jiraReadAt || '',
326
326
  // split or merge (batch 11 item 14): Claude's suggestion for it, and where its work went when it was merged
327
327
  suggest: r.suggest || null, merged: mergedOf(r),
328
328
  // what Claude proposes under the one blue ✦, and what a ✓ kept from elsewhere (batch 12 items 2, 3 and 4; pm/ini-props.js)
@@ -1041,6 +1041,7 @@ button.iv-mt.iv-wf { padding: 0 !important; margin: 0 !important; }
1041
1041
  @container iv (min-width: 760px) { .iv-ab-b { grid-template-columns: minmax(0, 1fr) minmax(0, 460px); gap: 32px; } }
1042
1042
  .ab-d h4, .ab-m h4 { margin: 0 0 6px; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink3); letter-spacing: .02em; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; line-height: var(--lh-label); }
1043
1043
  .ab-d h4 span, .ab-m h4 span { font-weight: var(--fw-strong); }
1044
+ .ab-d h5.ab-from { margin: 12px 0 4px; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-label); }
1044
1045
  .ab-p { margin: 0; font-size: var(--fs-body); line-height: 1.55; max-width: 62ch; color: var(--cz-ink); }
1045
1046
  .ab-p.q { color: var(--cz-ink3); }
1046
1047
  .ab-p.cl { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
@@ -415,7 +415,7 @@ export function wireView(body, h) {
415
415
  const drafts = (id) => { const H = body.__iv || {}; if (id && VIEWED.has(id)) MD.want(((H.where && H.where()) || {}).ws || '', 'initiative', id, () => { if (idOf() === id && H.redraw) H.redraw(); }); };
416
416
  // the description: the PRD, read once per path through /api/doc; the view draws again when it arrives
417
417
  // About draws alone when its description arrives: a full redraw would close a task row the PM has open
418
- const about = (id) => { const H = body.__iv || {}, v = id && VIEWED.get(id), prd = v && v.links && v.links.prd;
418
+ const about = (id) => { const H = body.__iv || {}, v = id && VIEWED.get(id), prd = v && ((v.links && v.links.prd) || v.prd);
419
419
  if (prd) AB.want(((H.where && H.where()) || {}).ws || '', prd, () => { const at = idOf() === id && body.querySelector('.iv-ab'); if (!at) return; const M = MD.measuresOf(id); at.outerHTML = AB.aboutHtml(v, OPTS.get(id) || {}, mcState(id, { proposal: !!(M && M.mode === 'open') }).ab); }); };
420
420
  chip();
421
421
  G.attach(body, body.__iv); // the layout and the drafted tickets of the initiative shown (batch 11 item 20)
@@ -1,10 +1,15 @@
1
+ @font-face{font-family:"Space Grotesk";src:url(/vendor/space-grotesk-var.woff2) format("woff2");font-weight:300 700;font-display:swap}
2
+ @font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-400.woff2) format("woff2");font-weight:400;font-display:swap}
3
+ @font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-500.woff2) format("woff2");font-weight:500;font-display:swap}
4
+ @font-face{font-family:"IBM Plex Mono";src:url(/vendor/ibm-plex-mono-700.woff2) format("woff2");font-weight:600 700;font-display:swap}
5
+ :root{--fw-regular:400;--fw-strong:600;--fs-field:16px;--fs-display:26px;--fs-page:26px;--fs-panel:18px;--fs-section:15px;--fs-row:15px;--fs-body:15px;--fs-meta:13px;--fs-label:13px;--fs-badge:13px;--lh-display:1.2;--lh-page:1.2;--lh-panel:1.3;--lh-section:1.35;--lh-row:1.4;--lh-body:1.6;--lh-meta:1.5;--lh-label:1.35;--lh-badge:1.2}
1
6
  /* 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
7
  * 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
8
  * well for the open page, no colour but the chips' hover. */
4
- .vwdoc.gd{padding:0}
9
+ .vwdoc.gd{padding:0;max-width:none}
5
10
  .gd-wrap{container-type:inline-size;display:flex;align-items:flex-start;gap:20px;position:relative}
6
11
  .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}
12
+ .gd-read{flex:1;min-width:0;max-width:none}
8
13
  .gd-tog{display:none}
9
14
  .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
15
  .gd-find .i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;flex:none}
@@ -3106,6 +3106,20 @@ const outcomeProps = require('./lib/outcome-props.js').create({
3106
3106
  every: TICK_MS,
3107
3107
  mayRun: () => { try { return scheduleOwner.mayRun(); } catch { return true; } },
3108
3108
  warn: (what, e) => warnWriteFail(what, e) });
3109
+ // initiative-description-field: once a day Claude drafts the description of each initiative that has none (or a fuller one once its PRD says
3110
+ // more) under the initiative's ✦, from the PRD's or the record's own words, cited (lib/description-props.js). No session and no model; through
3111
+ // the shared guard. A ✓ of the PM writes the record. Off with PMOS_DESCRIPTIONS=off or pmos ai off descriptions.
3112
+ const descriptionProps = require('./lib/description-props.js').create({
3113
+ workspaces: () => [...workspaces.values()],
3114
+ isLead: (w) => isPlanNeedsTree(w),
3115
+ stateDirFor: (w) => stateDirFor(w),
3116
+ boardOf: (w, id) => require('./lib/routes-boards.js').boardIndex(proposalCtx(), w).of(id),
3117
+ runtime: () => STATE,
3118
+ open: (now) => !proactiveLib.inQuietHours(now, proSettings().quiet),
3119
+ env: process.env,
3120
+ every: TICK_MS,
3121
+ mayRun: () => { try { return scheduleOwner.mayRun(); } catch { return true; } },
3122
+ warn: (what, e) => warnWriteFail(what, e) });
3109
3123
  // batch 16 item 6 ("Claude runs the OKRs"): Claude reads each goal with its recipe (lib/metric-read-runs.js) when its cadence comes round, files the
3110
3124
  // value with its source and time, says honestly what it could not read and drafts the fix. A broken limit opens one alert: a bell line only when
3111
3125
  // the goal's notify is push. At most 2 runs at once, one per product. PMOS_METRIC_READ=off.
@@ -4475,7 +4489,7 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
4475
4489
  // grandchild keeps the output pipes open after the run is stopped.
4476
4490
  // Windows: `claude` installed by npm is claude.cmd, which spawn() cannot start by name (lib/win-command.js).
4477
4491
  const run = winCommand.resolve(base.agent, { env });
4478
- try { child = spawn(run.file, [...run.prefix, ...args], { cwd: workdir, env, stdio: ['pipe', 'pipe', 'pipe'], detached: true }); }
4492
+ try { child = spawn(run.file, [...run.prefix, ...args], { cwd: workdir, env: headlessLib.headlessEnv(env), stdio: ['pipe', 'pipe', 'pipe'], detached: true }); }
4479
4493
  catch (e) {
4480
4494
  s.state = 'exited'; s.note = spawnFailureAdvice(e, base.agent, false);
4481
4495
  say(`\x1b[38;5;203m✗ ${s.note}\x1b[0m\n`); persist(s); pushSessions(); pushWorkspaces(); return s;
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "initiative-description-field: every initiative has its own description. The record writer, the cited drafts, the guard, the daily filer under the ✦ and pmos initiative describe (lib/initiative-writer.js setDescription, lib/description-props.js, lib/proposals.js group description, lib/initiative-cli.js describe).",
3
+ "modules": ["descriptions.test.js"],
4
+ "seconds": { "descriptions.test.js": 3 }
5
+ }