create-pmos 0.150.0 → 0.151.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.150.0",
3
+ "version": "0.151.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@3f66dcc
1
+ kernel-source: pmos@ccceced
2
2
  assembled: 2026-10-12
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T03:34:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T03:34:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T03:34:16Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -21,7 +21,7 @@ Under the title, the note shows its meeting and its initiative. Press **Link to
21
21
 
22
22
  - **Before** shows what the note kept about its meeting: the agenda, what the meeting is for, what to do to be ready, what to read first, the people, and the meeting: its time, its place and who organized it. A tick shows an agenda item that was heard in the transcript. A note with no meeting says so, and has **Link to a meeting** and **Link to an initiative**.
23
23
  - **During** shows the live notes while the note is recorded or paused. At other times it shows the transcript.
24
- - **After** shows **Replay** first when the sound of the note is kept on this machine. Then it shows the overview, the decisions with why, the next steps with owner and due date, the open questions, the statements that differ from a file, the highlights and the screenshots of the shared screen (**Screen**; press one to see it large). The transcript is folded at the bottom. When the final notes are written, click a text to correct it. Leave the text to save it. Press `Esc` to cancel. **Make task** adds a next step to the note's initiative.
24
+ - **After** shows **Replay** first when the sound of the note is kept on this machine. Then it shows the overview, the decisions with why, the next steps with owner and due date, the open questions, the statements that differ from a file, the highlights and the screenshots of the shared screen (**Screen**; press one to see it large). The transcript is folded at the bottom. Click a text to correct it: the summary, a decision and its why, or a next step, its owner and its due day. Press `Enter` or leave the text to save it. Press `Esc` to cancel. Empty an owner or a due day to clear it. A decision caught while the meeting was recorded has no why. Before the final notes are written, press **Write a summary of the meeting** to write your own summary; empty it to go back to the caught one. **Make task** adds a next step to the note's initiative.
25
25
 
26
26
  A note that is recorded now opens on **During**. Other notes open on **After**. **Show in the calendar** goes back to the grid, on the day of the meeting, with the meeting selected. The copy button puts the note on the clipboard as Markdown.
27
27
 
@@ -903,11 +903,11 @@ function summarize(note) {
903
903
  // PM labelled it; without bullets, from the lines as before
904
904
  const bl = ((note && note.bullets) || []).filter((b) => b && b.text);
905
905
  const bitems = bl.map((b, k) => ({ i: n + k, bid: b.id, text: b.text, t: Number.isFinite(b.t) ? b.t : null, src: 'bullet', cat: b.cat || null, by_pm: b.cat_by === 'pm',
906
- dropped: !!b.dropped, open: !!b.open, owner: typeof b.owner === 'string' && b.owner ? b.owner : null }));
906
+ dropped: !!b.dropped, open: !!b.open, owner: typeof b.owner === 'string' && b.owner ? b.owner : null, due: typeof b.due === 'string' && b.due ? b.due : null }));
907
907
  const caught = bl.length ? bitems.concat(items.filter((x) => x.src === 'transcript' && x.by_pm)) : items;
908
908
  const decisions = cap(caught.filter((x) => kindOf(x, 'decision')).filter(once), 'decisions');
909
909
  const actions = cap(caught.filter((x) => kindOf(x, 'action')).filter(once), 'actions')
910
- .map((x) => ({ ...x, owner: x.owner || ownerOf(x.text, names), due: dueOf(x.text) }));
910
+ .map((x) => ({ ...x, owner: x.owner || ownerOf(x.text, names), due: x.due || dueOf(x.text) }));
911
911
  const questions = cap(caught.filter((x) => kindOf(x, 'question')).filter(once), 'questions');
912
912
  const points = cap(bitems.filter((x) => !x.dropped && !x.open && (x.cat === 'topic' || x.cat === 'statement')).filter(once), 'points');
913
913
  const facts = items.filter((x) => x.src === 'transcript' && isFact(x)).filter(once);
@@ -1569,7 +1569,7 @@ function updateBullet(dir, { id, bid, text, patch = {} } = {}) {
1569
1569
  return { ok: true, bullet: b };
1570
1570
  }
1571
1571
  /** The PM's correction to one bullet: its label (the seven), `drop` (out of its section and the summary), or its words. */
1572
- function editBullet(dir, { id, bid, cat, drop, text, now = Date.now() } = {}) {
1572
+ function editBullet(dir, { id, bid, cat, drop, text, owner, due, now = Date.now() } = {}) {
1573
1573
  const note = load(dir, id);
1574
1574
  if (!note) return err(404, 'no such note');
1575
1575
  if (!BULLET_ID_RE.test(String(bid || ''))) return err(400, 'that is not a bullet id');
@@ -1583,6 +1583,9 @@ function editBullet(dir, { id, bid, cat, drop, text, now = Date.now() } = {}) {
1583
1583
  if (t.length > MAX_BULLET) return err(413, 'that bullet is too long');
1584
1584
  if (t !== b.text) { if (b.orig_text === undefined) b.orig_text = b.text; b.text = t; delete b.check; }
1585
1585
  }
1586
+ // the PM's owner or due day for a next step; an empty one clears it
1587
+ if (owner !== undefined) { const o = String(owner == null ? '' : owner).replace(/\s+/g, ' ').trim().slice(0, 80); if (o) b.owner = o; else delete b.owner; }
1588
+ if (due !== undefined) { const d = String(due == null ? '' : due).replace(/\s+/g, ' ').trim().slice(0, 60); if (d) b.due = d; else delete b.due; }
1586
1589
  if (cat !== undefined) { b.cat = cat; b.cat_by = 'pm'; delete b.open; }
1587
1590
  if (drop === true) b.dropped = true; else if (drop === false) delete b.dropped;
1588
1591
  refreshLocal(note, now);
@@ -1642,14 +1645,36 @@ function removeHighlight(dir, { id, hid, now = Date.now() } = {}) {
1642
1645
  store(dir, note);
1643
1646
  return { ok: true };
1644
1647
  }
1645
- /** The PM's correction to the final notes: the overview, or the text of one decision, next step or open question. The
1646
- * first words the agent wrote are kept as `orig_text`. */
1648
+ /** The PM's correction to the final notes: the overview (before the final notes, the PM's summary of the note), or the text of one decision, next step or open question. The
1649
+ * first words the agent wrote are kept as `orig_text`. `field` corrects another part of one item instead: a next step's
1650
+ * `owner` or `due`, or a decision's `why`; an empty one clears it. */
1647
1651
  const FINAL_SECS = { decision: 'decisions', action: 'actions', question: 'questions' };
1648
- function editFinal(dir, { id, sec, i, text, now = Date.now() } = {}) {
1652
+ const FINAL_FIELDS = { action: { owner: 80, due: 60 }, decision: { why: 400 } };
1653
+ function editFinal(dir, { id, sec, i, text, field, now = Date.now() } = {}) {
1649
1654
  const note = load(dir, id);
1650
1655
  if (!note) return err(404, 'no such note');
1651
1656
  const f = note.final_notes;
1657
+ // before the final notes exist, the PM's summary is kept beside what was caught live; an empty one goes back to that
1658
+ if (!f && sec === 'overview') {
1659
+ const t = clip(text, 2000);
1660
+ if (t) note.overview_pm = t; else delete note.overview_pm;
1661
+ note.updated_at = iso(now);
1662
+ store(dir, note);
1663
+ return { ok: true, final_notes: null, overview_pm: note.overview_pm || null };
1664
+ }
1652
1665
  if (!f) return err(409, 'this note has no final notes yet');
1666
+ if (field !== undefined && field !== 'text') {
1667
+ const max = (FINAL_FIELDS[sec] || {})[field];
1668
+ if (!max) return err(400, 'a ' + (sec || 'note') + ' has no ' + String(field).slice(0, 20) + ' to correct');
1669
+ const list = f[FINAL_SECS[sec]];
1670
+ const x = Array.isArray(list) ? list[Number(i)] : null;
1671
+ if (!x) return err(404, 'no such item');
1672
+ x[field] = clip(text, max) || null;
1673
+ keepVersion(note, now);
1674
+ note.updated_at = iso(now);
1675
+ store(dir, note);
1676
+ return { ok: true, final_notes: f };
1677
+ }
1653
1678
  const t = clip(text, sec === 'overview' ? 2000 : 400);
1654
1679
  if (!t) return err(400, 'the text is empty');
1655
1680
  if (sec === 'overview') {
@@ -7,7 +7,7 @@
7
7
  * [{ id, t, minute, text, owner, cat, cat_by, by: 'agent', open?, check?, dropped? }];
8
8
  * an open bullet has cat null. bullets_state: { agent: 'on'|'off'|'failed',
9
9
  * reason, session, next_at_ms, minute_ms, minutes }
10
- * POST /api/notes/bullet { id, bullet, cat?, drop?, text? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
10
+ * POST /api/notes/bullet { id, bullet, cat?, drop?, text?, owner?, due? } → { ok, bullet, summary } the PM's correction to one bullet (Run 8)
11
11
  * POST /api/notes/new { title?, meeting?, initiative? } → { ok, note, reused } a calendar event that already has a note
12
12
  * gets that note back (reused: true), so one meeting keeps one note (Run 4)
13
13
  * POST /api/notes/link { id, meeting?, initiative?, from_event? } → { ok, note, kept? } the PM's link; null unlinks (Run 4)
@@ -62,7 +62,7 @@
62
62
  * POST /api/notes/highlight-remove { id, hid } → { ok }
63
63
  * POST /api/notes/wispr-fetch { id } → { ok, wispr } the PM asks for Wispr Flow's notes now, after the automatic fetch failed or
64
64
  * found nothing; the agent session is asked at once and again every two minutes for up to 20 minutes
65
- * POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text } → { ok, final_notes } the PM's correction
65
+ * POST /api/notes/final-edit { id, sec: 'overview'|'decision'|'action'|'question', i?, text, field?: 'owner'|'due'|'why' } → { ok, final_notes } the PM's correction
66
66
  *
67
67
  * Notes are the PM's own and live in the per-user runtime directory (ctx.runtimeDir), never in a repository, so no route here
68
68
  * writes into a registered workspace. Every POST goes through pm-guard `requirePagePress` (a body that is not an object or carries
@@ -812,7 +812,7 @@ async function handle(req, res, url, ctx) {
812
812
  const q = queueFinal(ctx, id);
813
813
  return done(q.queued ? { ok: true, queued: true } : { ok: false, status: 409, error: q.reason });
814
814
  }
815
- if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text }));
815
+ if (p === '/api/notes/bullet') return done(notes.editBullet(dir, { id, bid: body.bullet, cat: body.cat, drop: body.drop, text: body.text, owner: body.owner, due: body.due }));
816
816
  if (p === '/api/notes/line') {
817
817
  const pick = (k) => (body[k] === undefined ? undefined : body[k]);
818
818
  return done(notes.editLine(dir, { id, index: Number(body.index), cat: pick('cat'), summary: pick('summary'), tags: pick('tags'), people: pick('people'), text: pick('text') }));
@@ -963,7 +963,7 @@ async function handle(req, res, url, ctx) {
963
963
  }
964
964
  if (p === '/api/notes/highlight-remove') return done(notes.removeHighlight(dir, { id, hid: body.hid }));
965
965
  if (p === '/api/notes/wispr-fetch') return done(fetchWisprNow(ctx, id));
966
- if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text }));
966
+ if (p === '/api/notes/final-edit') return done(notes.editFinal(dir, { id, sec: body.sec, i: body.i, text: body.text, field: body.field }));
967
967
  // summarize-agent: the handoff file first (refused for a private note), then the PM's own agent session
968
968
  let w = null;
969
969
  if (body.ws != null) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.150.0",
3
+ "version": "0.151.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.150.0",
9
+ "version": "0.151.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.150.0",
3
+ "version": "0.151.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -282,20 +282,26 @@ function after(s, n, mode) {
282
282
  const p = partsOf(n);
283
283
  const names = n.meeting && n.meeting.context ? peopleOf(n.meeting.context) : [];
284
284
  const cf = conflictsOf(n);
285
- const td = (text) => el('td', null, text);
286
285
  const kids = [
287
286
  el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
288
287
  el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
289
288
  ...(() => { const rp = mode ? null : replayOf(s, n); return rp ? [el('div', { class: 'cal3-replay', 'data-cal3-notes': 'replay' }, el('h4', null, 'Replay'), rp, el('p', { class: 'dim cal3-sm' }, 'Kept on this machine.'))] : []; })(),
290
289
  ...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
291
- p.overview ? editable(p.from === 'final', 'p', p.overview, 'Overview', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': 'overview' })
292
- : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
290
+ p.overview || !mode ? editable(true, 'p', p.overview, 'Summary', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': p.overview ? 'overview' : 'no-overview' }, p.from === 'final' ? null : 'Write a summary of the meeting')
291
+ : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, 'The overview is written when you stop recording.'),
293
292
  ];
294
- if (p.decisions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'decisions' }, el('thead', null, el('tr', null, el('th', null, 'Decision'), el('th', null, 'Why'))),
295
- el('tbody', null, p.decisions.map((d) => el('tr', { 'data-cal3-notes': 'decision' }, el('td', null, editable(p.from === 'final', 'span', d.text, 'Decision', (t) => saveFinal(s, n, d.sec, d.i, t))), el('td', { class: 'dim' }, d.why || d.by || ''))))));
293
+ // each part of a decision or a next step is corrected in place: in the final notes (/api/notes/final-edit), or in the bullet
294
+ // it was caught from (/api/notes/bullet). A bullet has no why, so a caught decision's why is not shown.
295
+ const fin = p.from === 'final';
296
+ const cell = (x, field, label, ph, cls) => {
297
+ const on = fin || (x.bullet !== undefined && field !== 'why');
298
+ return el('td', cls ? { class: cls } : null, editable(on, 'span', x[field] || '', label, (t) => saveItem(s, n, x, field, t), { 'data-cal3-notes-field': field }, ph));
299
+ };
300
+ if (p.decisions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'decisions' }, el('thead', null, el('tr', null, el('th', null, 'Decision'), fin ? el('th', null, 'Why') : null)),
301
+ el('tbody', null, p.decisions.map((d) => el('tr', { 'data-cal3-notes': 'decision' }, cell(d, 'text', 'Decision'), fin ? cell(d, 'why', 'Why', 'Add why', 'dim') : null)))));
296
302
  if (p.actions.length) kids.push(el('table', { class: 'tbl', 'data-cv': 'actions' }, el('thead', null, el('tr', null, el('th', null, 'Next step'), el('th', null, 'Owner'), el('th', null, 'Due'), el('th'))),
297
- el('tbody', null, p.actions.map((a) => el('tr', { 'data-cal3-notes': 'action' }, el('td', null, editable(p.from === 'final', 'span', a.text, 'Next step', (t) => saveFinal(s, n, a.sec, a.i, t))),
298
- td(a.owner || el('span', { class: 'dim' }, 'No owner')), td(a.due || el('span', { class: 'dim' }, '—')),
303
+ el('tbody', null, p.actions.map((a) => el('tr', { 'data-cal3-notes': 'action' }, cell(a, 'text', 'Next step'),
304
+ cell(a, 'owner', 'Owner', 'No owner'), cell(a, 'due', 'Due', 'No date'),
299
305
  el('td', { class: 'cal3-tdr' }, btn('chip2 lk', { 'data-cal3-notes': 'make-task', title: n.initiative ? 'Add this step to ' + iniName(n.initiative) + ' as a plan item' : 'Link the note to an initiative first' },
300
306
  [c3ic('plus', 'sm'), 'Make task'], (e) => makeTask(n, a, { btn: e.currentTarget }))))))));
301
307
  if (p.questions.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'questions' }, el('h4', null, 'Open questions'),
@@ -313,10 +319,11 @@ function after(s, n, mode) {
313
319
  return el('section', { class: 'card cal3-ncard', 'data-cv': 'notes' }, kids);
314
320
  }
315
321
 
316
- /** A text the PM corrects in place once the notes are final: a click puts the cursor in it, leaving it saves, Esc puts it back. */
317
- function editable(on, tag, text, label, save, attrs) {
318
- if (!on) return el(tag, attrs || null, text);
319
- const node = el(tag, { ...(attrs || {}), contenteditable: 'true', spellcheck: 'true', role: 'textbox', 'aria-label': 'Edit ' + label, 'data-cal3-notes-edit': '' }, text);
322
+ /** A text the PM corrects in place: a click puts the cursor in it, leaving it saves, Esc puts it back. With a placeholder
323
+ * (`ph`), the text may be empty: the placeholder shows, and emptying the text clears it; without one, an empty text is refused. */
324
+ function editable(on, tag, text, label, save, attrs, ph) {
325
+ if (!on) return el(tag, attrs || null, text || (ph ? el('span', { class: 'dim' }, ph) : ''));
326
+ const node = el(tag, { ...(attrs || {}), class: [attrs && attrs.class, ph ? 'cal3-ed' : null].filter(Boolean).join(' ') || null, contenteditable: 'true', spellcheck: 'true', role: 'textbox', 'aria-label': 'Edit ' + label, 'data-cal3-notes-edit': '', 'data-ph': ph || null }, text);
320
327
  let last = text;
321
328
  node.addEventListener('keydown', (e) => {
322
329
  if (e.key === 'Enter' && tag !== 'p') { e.preventDefault(); node.blur(); }
@@ -324,15 +331,27 @@ function editable(on, tag, text, label, save, attrs) {
324
331
  });
325
332
  node.addEventListener('blur', async () => {
326
333
  const v = node.textContent.replace(/\s+/g, ' ').trim();
327
- if (!v || v === last) { node.textContent = last; return; }
328
- if (await save(v)) last = v; else node.textContent = last;
334
+ if ((!v && !ph) || v === last) { node.textContent = last; return; }
335
+ if (await save(v)) { last = v; node.textContent = v; } else node.textContent = last;
329
336
  });
330
337
  return node;
331
338
  }
332
- async function saveFinal(s, n, sec, i, text) {
333
- const r = await pmPost('/api/notes/final-edit', { id: n.id, sec, i, text });
339
+ /* one part of a decision or a next step: in the final notes, or in the bullet it was caught from */
340
+ async function saveItem(s, n, x, field, text) {
341
+ if (x.sec) return saveFinal(s, n, x.sec, x.i, text, field === 'text' ? undefined : field);
342
+ const r = await pmPost('/api/notes/bullet', { id: n.id, bullet: x.bullet, [field]: text });
334
343
  if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return false; }
335
- n.final_notes = r.final_notes;
344
+ const b = (n.bullets || []).find((y) => y && y.id === x.bullet);
345
+ if (b) Object.assign(b, r.bullet);
346
+ x[field] = text;
347
+ toast('Saved');
348
+ return true;
349
+ }
350
+ async function saveFinal(s, n, sec, i, text, field) {
351
+ const r = await pmPost('/api/notes/final-edit', field ? { id: n.id, sec, i, text, field } : { id: n.id, sec, i, text });
352
+ if (!r || !r.ok) { toast('Not saved' + ((r && r.error) ? ': ' + r.error : '')); return false; }
353
+ if (r.final_notes) n.final_notes = r.final_notes;
354
+ if ('overview_pm' in r) { if (r.overview_pm) n.overview_pm = r.overview_pm; else delete n.overview_pm; }
336
355
  toast('Saved');
337
356
  return true;
338
357
  }
@@ -529,6 +529,10 @@
529
529
  .c3 .cal3-ov{margin:0}
530
530
  .c3 .cal3-tdr{text-align:right}
531
531
  .c3 .cal3-ncard{display:grid;gap:var(--sp-2)}
532
+ .c3 [data-cal3-notes-edit]{cursor:text;border-radius:6px;outline-offset:2px}
533
+ @media (hover:hover){.c3 [data-cal3-notes-edit]:hover{background:var(--muted)}}
534
+ .c3 .cal3-ed{display:inline-block;min-width:6ch}
535
+ .c3 .cal3-ed:empty::before{content:attr(data-ph);color:var(--muted-foreground)}
532
536
  .c3 .cal3-nlinks{display:grid;gap:var(--sp-1);margin-top:var(--sp-1)}
533
537
  .c3 .cal3-replay{display:grid;gap:var(--sp-1)}
534
538
  .c3 .cal3-replay audio{width:100%;max-width:100%}
@@ -144,6 +144,7 @@ export function partsOf(n) {
144
144
  const ov = n.summary && n.summary.sections && n.summary.sections.overview;
145
145
  if (Array.isArray(ov)) overview = ov.map((x) => String((x && x.text) || '')).filter(Boolean).join(' ');
146
146
  }
147
+ if (n.overview_pm) overview = n.overview_pm; // the PM's own summary, written before the final notes
147
148
  return { from: 'live', overview, decisions: c.decision, actions: c.action, questions: c.question, topics: [] };
148
149
  }
149
150