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.
- package/package.json +39 -47
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/board-columns-test.js +1 -1
- package/workspace/board-programs-test.js +26 -7
- package/workspace/briefing-premium-board-test.js +3 -3
- package/workspace/calm-timeline-test.js +2 -2
- package/workspace/changelog.json +38 -0
- package/workspace/guide/agent-capabilities.md +2 -1
- package/workspace/guide/descriptions.md +43 -0
- package/workspace/guide/index.md +3 -2
- package/workspace/guide/initiatives-roadmap.md +1 -0
- package/workspace/guide/next-prompt.md +4 -2
- package/workspace/guide/programs.md +2 -1
- package/workspace/guide/settings.md +1 -1
- package/workspace/lib/board-trail.js +8 -3
- package/workspace/lib/description-props.js +214 -0
- package/workspace/lib/headless.js +12 -2
- package/workspace/lib/initiative-cli.js +28 -2
- package/workspace/lib/initiative-writer.js +41 -1
- package/workspace/lib/proposals.js +13 -4
- package/workspace/lib/routes-next-prompt.js +67 -27
- package/workspace/lib/routes-proposals.js +5 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-span.js +13 -17
- package/workspace/public/pm/cozy-settings.js +8 -7
- package/workspace/public/pm/ini-about.js +26 -11
- package/workspace/public/pm/ini-props.js +5 -3
- package/workspace/public/pm/ini-view-model.js +1 -1
- package/workspace/public/pm/ini-view.css +1 -0
- package/workspace/public/pm/ini-view.js +1 -1
- package/workspace/public/pm/premium-vw-guide.css +7 -2
- package/workspace/server.js +15 -1
- 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
|
|
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
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
const
|
|
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
|
|
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('') :
|
|
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"
|
|
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('') :
|
|
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"
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
283
|
-
const
|
|
284
|
-
|
|
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
|
|
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(/[
|
|
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
|
-
|
|
118
|
-
const long =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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${
|
|
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}
|
package/workspace/server.js
CHANGED
|
@@ -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
|
+
}
|