create-pmos 0.147.0 → 0.148.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.147.0",
3
+ "version": "0.148.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@1f1a8b3
1
+ kernel-source: pmos@878518d
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-12T02:42:43Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T02:49:09Z"}
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-12T02:42:43Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T02:49:09Z"}
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-12T02:42:43Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T02:49:09Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -17,9 +17,11 @@ In **Notes**, the list is at the left. The notes are in groups, one for each ini
17
17
 
18
18
  Press a row to open the note at the right. The note has three stages. Press **Before**, **During** or **After** to change the stage.
19
19
 
20
- - **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 and the people. 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**.
20
+ Under the title, the note shows its meeting and its initiative. Press **Link to a meeting** or **Link to an initiative** to link the note. When the note has a link, press **Change** to link it to another one.
21
+
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**.
21
23
  - **During** shows the live notes while the note is recorded or paused. At other times it shows the transcript.
22
- - **After** 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 and the highlights. 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. 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.
23
25
 
24
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.
25
27
 
@@ -82,11 +84,18 @@ Press a marker to show its detail in the side column. The detail shows the title
82
84
  Press a meeting to show its context in the side column. Under the title, a row shows where the meeting lives, the people and the initiative. Each line has a link to its source.
83
85
 
84
86
  - For a past meeting, **What came out of it** lists the decisions and actions of its note. **What moved after it** lists the items that changed within 48 hours and refer to the meeting or its initiative.
85
- - For a coming meeting, **Prepared** lists what is ready, and **To prepare** lists what is still open.
87
+ - For a coming meeting, the detail shows these parts. A part with no record is left out.
88
+ - **In short**: the purpose from today's brief, or the initiative and its status. It also counts the items that are blocked, late or due today.
89
+ - **What matters today**: the items of the initiative that are blocked, late or due in the next 2 days, and your own items on it. Each line opens the item.
90
+ - **Since the last one**: the last note of the same meeting (the same title or calendar series). It shows the summary, the decisions and the next steps, and how many earlier notes there are. **Open the last note** opens it in **Notes**. For a private note, only its title shows.
91
+ - **To prepare**: what is prepared, what is still open, and how to be ready, from today's brief. The links from the brief follow.
92
+ - A last line names the sources that PMOS used.
93
+
94
+ The initiative of a coming meeting is the one on the event. If the event has none, it is the one in today's brief. If the brief has none, it is the one on the last note of the meeting.
86
95
 
87
96
  When the meeting has a note, the detail has **Open the note**. It opens the note in **Notes**.
88
97
 
89
- When PMOS holds nothing for a meeting, the detail says so. It does not guess.
98
+ When PMOS holds nothing for a past meeting, the detail says so. For a coming meeting, **What PMOS knows** tells you what is missing (no initiative, no earlier note) and what to do: link an initiative, or take a note in the meeting. It does not guess.
90
99
 
91
100
  ## Free time
92
101
 
@@ -46,7 +46,7 @@ The live transcript keeps each sentence once:
46
46
 
47
47
  **Stop** ends the meeting for good. PMOS asks first: **Cancel**, **Pause instead** (while recording) or **Stop for good**. A stopped meeting cannot be resumed. PMOS writes the final notes at once.
48
48
 
49
- If you start a note by mistake and stop it within a minute, or before anything was heard, and you typed nothing, PMOS deletes the note. The meeting you paused before it can be resumed again.
49
+ If you start a note by mistake and stop it before anything was heard, and you typed nothing, PMOS deletes the note. A short note with speech in it is kept. The meeting you paused before it can be resumed again.
50
50
 
51
51
  ## While the meeting is held
52
52
 
@@ -31,6 +31,9 @@ function localIso(ms) {
31
31
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
32
32
  const minuteKey = (ms, title) => Math.floor(ms / 60000) + '|' + calMem.normTitle(title);
33
33
 
34
+ /** The brief names an initiative as { id, why }; memory and imports as an id. The event keeps the id. */
35
+ const iniId = (v) => (v && typeof v === 'object' ? (typeof v.id === 'string' && v.id ? v.id : null) : (typeof v === 'string' && v ? v : null));
36
+
34
37
  /** A remembered or briefed event as an imported one. `src` says where it came from. */
35
38
  function asEvent(e, src) {
36
39
  const s = Date.parse(e.start);
@@ -38,7 +41,14 @@ function asEvent(e, src) {
38
41
  const f = e.end && !Number.isNaN(Date.parse(e.end)) ? Date.parse(e.end) : s + 30 * 60000;
39
42
  const people = e.with ? String(e.with).split(/\s*,\s*/).filter(Boolean) : [];
40
43
  return { id: String(e.id || calMem.seriesKey(e) + '@' + s), at: localIso(s), end: localIso(f), title: String(e.title), attendees: people.slice(0, 12),
41
- initiative: e.initiative || null, agent_prep_notes: typeof e.prep === 'string' ? e.prep : '', pm_notes: [], src, label: 'reported', trust: 'untrusted' };
44
+ initiative: iniId(e.initiative), ...(e.initiative && typeof e.initiative === 'object' && e.initiative.why ? { initiative_why: String(e.initiative.why) } : {}),
45
+ agent_prep_notes: typeof e.prep === 'string' ? e.prep : '', pm_notes: [], src, label: 'reported', trust: 'untrusted',
46
+ // what the brief says the PM needs to walk in (the meeting card reads it): purpose, steps, links and references
47
+ ...(typeof e.purpose === 'string' && e.purpose.trim() ? { purpose: e.purpose } : {}),
48
+ ...(e.steps && typeof e.steps === 'object' && !Array.isArray(e.steps) ? { steps: e.steps } : {}),
49
+ ...(Array.isArray(e.links) && e.links.length ? { links: e.links } : {}),
50
+ ...(Array.isArray(e.refs) && e.refs.length ? { refs: e.refs } : {}),
51
+ ...(e.series ? { series: String(e.series) } : {}) };
42
52
  }
43
53
 
44
54
  function readAll(runtimeDir, { now = Date.now() } = {}) {
@@ -370,17 +370,104 @@ function contextFrom(events, range, src, opts = {}) {
370
370
  }
371
371
  const kept = capKinds(items);
372
372
  context[e.id] = { phase: past ? 'past' : 'upcoming', held: kept.length > 0, items: kept };
373
+ if (!past) { const b = briefOf(e, notes, s, { today, inis: opts.inis }); if (b) context[e.id].brief = b; }
373
374
  }
374
375
  return { context, context_total: inside.length };
375
376
  }
376
377
 
378
+ /* ── the card before a meeting (initiative c3-meeting-card) ───────────────────────────────────────────────────────
379
+ * What PMOS already holds about a coming meeting, put together for the Calendar's detail card. No model runs: each part is
380
+ * read from a record, and a part with no record is left out. null when nothing at all is known.
381
+ * { initiative:{id,title,status}|null, gist:[sentence], matters:[{tone,tag,text,meta,ref}], last:{id,title,at,private,
382
+ * overview,decisions,actions}|null, earlier, purpose, prep, links:[{label,url}], refs:[{label,source,link}], sources:[name] } */
383
+ const MATTERS_CAP = 5;
384
+ const iniIdOf = (v) => (v && typeof v === 'object' ? str(v.id) : str(v)) || null;
385
+
386
+ /** The earlier notes of the same meeting: the same calendar series or the same title, started before this one, newest first.
387
+ * The notes that match this occurrence are not earlier notes. */
388
+ function seriesNotes(e, notes, current) {
389
+ const start = Date.parse(e.at || e.start);
390
+ const title = norm(e.title);
391
+ const mine = new Set((current || []).map((n) => n.id));
392
+ return notes.filter((n) => {
393
+ const m = n && n.meeting; if (!m || mine.has(n.id)) return false;
394
+ const t = Date.parse(m.start); if (!Number.isFinite(t) || !(t < start)) return false;
395
+ if (e.series && m.series && String(e.series) === String(m.series)) return true;
396
+ return title.length >= 2 && norm(m.title) === title;
397
+ }).sort((a, b) => Date.parse(b.meeting.start) - Date.parse(a.meeting.start));
398
+ }
399
+
400
+ /** The initiative's items worth raising: blocked, late, or due within WARN_DAYS, not done. Own items on it likewise. */
401
+ function mattersOf(iniId, src, today) {
402
+ if (!iniId || !today) return [];
403
+ const soon = addDays(today, WARN_DAYS);
404
+ const dueTag = (d) => (d < today ? ['bad', 'Late'] : d === today ? ['warn', 'Due today'] : ['warn', 'Due soon']);
405
+ const out = [];
406
+ for (const p of src.planItems || []) {
407
+ if (!p || !p.id || p.initiative !== iniId || p.state === 'done' || p.state === 'withdrawn') continue;
408
+ const due = isDay(p.due) ? day10(p.due) : null;
409
+ const meta = [str(p.owner), due && 'due ' + due].filter(Boolean).join(' · ');
410
+ if (p.blocked) out.push({ rank: 0, tone: 'bad', tag: 'Blocked', text: str(p.title) + (str(p.blocked.reason) ? ': ' + str(p.blocked.reason) : ''), meta, ref: { type: 'plan-item', id: p.id, initiative: iniId } });
411
+ else if (due && due <= soon) { const [tone, tag] = dueTag(due); out.push({ rank: due < today ? 1 : 2, due, tone, tag, text: str(p.title), meta, ref: { type: 'plan-item', id: p.id, initiative: iniId } }); }
412
+ }
413
+ for (const o of src.ownItems || []) {
414
+ if (!o || !o.id || o.initiative !== iniId || o.done || o.done_at) continue;
415
+ const due = isDay(o.due) ? day10(o.due) : null;
416
+ if (!due || due > soon) continue;
417
+ const [tone, tag] = dueTag(due);
418
+ out.push({ rank: due < today ? 1 : 2, due, tone, tag, text: str(o.title), meta: 'yours · due ' + due, ref: { type: 'item', id: o.id } });
419
+ }
420
+ out.sort((a, b) => a.rank - b.rank || String(a.due || '').localeCompare(String(b.due || '')) || (a.text < b.text ? -1 : 1));
421
+ return out.slice(0, MATTERS_CAP).map(({ rank, due, ...x }) => x);
422
+ }
423
+
424
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many || one + 's'}`;
425
+
426
+ function briefOf(e, current, s, { today, inis } = {}) {
427
+ const earlier = seriesNotes(e, s.notes || [], current);
428
+ const lastNote = earlier[0] || null;
429
+ const iniId = iniIdOf(e.initiative) || (earlier.find((n) => n.initiative) || {}).initiative || null;
430
+ const row = iniId ? (inis || []).find((g) => g && g.id === iniId) : null;
431
+ const initiative = iniId ? { id: iniId, title: str(row && row.title) || iniId, status: str(row && row.status) || null } : null;
432
+ const matters = mattersOf(iniId, s, today);
433
+ const purpose = str(e.purpose) || null;
434
+ const why = e.initiative && typeof e.initiative === 'object' ? str(e.initiative.why) : str(e.initiative_why);
435
+ const gist = [];
436
+ if (purpose) gist.push(/[.!?]$/.test(purpose) ? purpose : purpose + '.');
437
+ else if (initiative) gist.push(`For ${initiative.title}` + (initiative.status && !['no plan', 'on time'].includes(initiative.status) ? `, which is ${initiative.status}.` : '.'));
438
+ if (why) gist.push(/[.!?]$/.test(why) ? why : why + '.');
439
+ const blocked = matters.filter((x) => x.tag === 'Blocked').length; const late = matters.filter((x) => x.tag === 'Late').length;
440
+ const dueNow = matters.filter((x) => x.tag === 'Due today').length;
441
+ const parts = [blocked && plural(blocked, 'item') + ' blocked', late && plural(late, 'item') + ' late', dueNow && plural(dueNow, 'item') + ' due today'].filter(Boolean);
442
+ if (parts.length) gist.push(parts.join(', ').replace(/^./, (c) => c.toUpperCase()) + '.');
443
+ let last = null;
444
+ if (lastNote) {
445
+ last = { id: lastNote.id, title: str(lastNote.title) || str(lastNote.meeting.title), at: lastNote.meeting.start, private: !!lastNote.private };
446
+ if (!lastNote.private) {
447
+ last.overview = str(lastNote.overview) || null;
448
+ last.decisions = (lastNote.decisions || []).map((d) => str(d && d.text)).filter(Boolean).slice(0, 4);
449
+ last.actions = (lastNote.actions || []).filter((a) => str(a && a.text)).slice(0, 5).map((a) => ({ text: str(a.text), owner: str(a.owner) || null, due: str(a.due) || null }));
450
+ }
451
+ }
452
+ const links = (Array.isArray(e.links) ? e.links : []).filter((l) => l && str(l.url) && /^https?:\/\//.test(l.url)).slice(0, 5).map((l) => ({ label: str(l.label) || str(l.url), url: str(l.url) }));
453
+ const refs = (Array.isArray(e.refs) ? e.refs : []).filter((r) => r && str(r.label)).slice(0, 5).map((r) => ({ label: str(r.label), source: str(r.source) || null, link: /^https?:\/\//.test(str(r.link)) ? str(r.link) : null }));
454
+ const prep = str(e.agent_prep_notes) || null;
455
+ const sources = ['the calendar'];
456
+ if (purpose || why || prep || links.length || refs.length) sources.push("today's brief");
457
+ if (earlier.length) sources.push(plural(earlier.length, 'earlier note') + ' of this meeting');
458
+ if (initiative) sources.push('the ' + initiative.id + ' plan');
459
+ if (matters.some((x) => x.ref.type === 'item')) sources.push('your items');
460
+ if (!gist.length && !matters.length && !last && !links.length && !refs.length && !prep && !initiative) return null;
461
+ return { initiative, gist: gist.slice(0, 3), matters, last, earlier: earlier.length, purpose, prep, links, refs, sources };
462
+ }
463
+
377
464
  /* ── all three ─────────────────────────────────────────────────────────────────────────────────────────────────── */
378
465
 
379
- function build({ range, today, nowMs, events, dated, src, own, planItemsDue }) {
466
+ function build({ range, today, nowMs, events, dated, src, own, planItemsDue, inis }) {
380
467
  const l = layersFrom(dated, range, today);
381
468
  const free = freeFrom(events, range, today, { nowMs, own: own || (dated && dated.own) || [], planItems: planItemsDue || (dated && dated.planItems) || [] });
382
- const c = contextFrom(events, range, src, { today, nowMs });
469
+ const c = contextFrom(events, range, src, { today, nowMs, inis });
383
470
  return { layers: l.layers, layers_total: l.layers_total, layers_note: l.layers_note, free, context: c.context, context_total: c.context_total };
384
471
  }
385
472
 
386
- module.exports = { layersFrom, freeFrom, contextFrom, build, toneOf, prepOf, spanOf, prepRisks, minutesOn, localDay, minuteOfDay, LAYER_CAP, CONTEXT_CAP, MIN_BLOCK, WINDOW_MS };
473
+ module.exports = { layersFrom, freeFrom, contextFrom, briefOf, seriesNotes, mattersOf, build, toneOf, prepOf, spanOf, prepRisks, minutesOn, localDay, minuteOfDay, LAYER_CAP, CONTEXT_CAP, MIN_BLOCK, WINDOW_MS };
@@ -55,7 +55,9 @@ function handle(query, deps = {}) {
55
55
  const items = typeof deps.readItems === 'function' ? (deps.readItems() || {}) : {};
56
56
  const src = typeof deps.readContextSources === 'function' ? (deps.readContextSources() || {}) : {};
57
57
  const nowMs = Number.isFinite(deps.nowMs) ? deps.nowMs : (deps.today ? undefined : Date.now());
58
- const extra = layersLib.build({ range, today: nowDay, nowMs, events, dated, src, own: items.own, planItemsDue: items.planItems });
58
+ // the initiative of a coming meeting: its title from the facts, its status from the Gantt row when the row is in range
59
+ const iniRows = inis.filter((i) => i && i.id).map((i) => { const g = gantt.find((x) => x.id === i.id); return { id: i.id, title: i.title || i.id, status: g ? g.status : null }; });
60
+ const extra = layersLib.build({ range, today: nowDay, nowMs, events, dated, src, own: items.own, planItemsDue: items.planItems, inis: iniRows });
59
61
  const tidy = tidyLib.tidyFrom({ range, today: nowDay, nowMs, events, eventsSource: source, inis, gantt,
60
62
  planItems: dated.planItems || items.planItems || [] });
61
63
  return { status: 200, body: { ok: true, view, range, unit: b.unit, cells: b.cells, gantt, strategy, events_source: source, today: nowDay, ...extra, ...tidy } };
@@ -118,8 +120,8 @@ function readers(root, runtimeDir) {
118
120
  },
119
121
  readContextSources() {
120
122
  const src = { notes: [], planItems: [], ownItems: [] };
121
- try { src.planItems = planRows().map((p) => ({ id: p.id, initiative: p.initiative, title: p.title, createdAt: p.createdAt, doneAt: p.doneAt })); } catch { /* no plan ledger: no moved items */ }
122
- try { src.ownItems = ownRows().map((o) => ({ id: o.id, title: o.title, initiative: o.initiative || null, created_at: o.created_at || null, done_at: o.done_at || null })); } catch { /* no own items */ }
123
+ try { src.planItems = planRows().map((p) => ({ id: p.id, initiative: p.initiative, title: p.title, createdAt: p.createdAt, doneAt: p.doneAt, state: p.state, blocked: p.blocked ? { reason: p.blocked.reason } : null, due: p.due || null, owner: p.owner || null })); } catch { /* no plan ledger: no moved items */ }
124
+ try { src.ownItems = ownRows().map((o) => ({ id: o.id, title: o.title, initiative: o.initiative || null, created_at: o.created_at || null, done_at: o.done_at || null, due: o.due || null, done: !!o.done })); } catch { /* no own items */ }
123
125
  try { src.notes = linkedNotes(runtimeDir); } catch { /* no notes: no decisions or actions */ }
124
126
  return src;
125
127
  },
@@ -165,8 +167,11 @@ function linkedNotes(runtimeDir) {
165
167
  const fin = n.final_notes && typeof n.final_notes === 'object' ? n.final_notes : null;
166
168
  const sec = n.summary && n.summary.sections ? n.summary.sections : null;
167
169
  const pick = (a, b) => (Array.isArray(a) && a.length ? a : Array.isArray(b) ? b : []);
170
+ const ov = fin && typeof fin.overview === 'string' && fin.overview.trim() ? fin.overview
171
+ : sec && Array.isArray(sec.overview) ? sec.overview.slice(0, 3).map((x) => String((x && x.text) || '')).filter(Boolean).join(' ') : '';
168
172
  out.push({ id: n.id, title: n.title || '', private: !!n.private, initiative: n.initiative && n.initiative.id ? String(n.initiative.id) : null,
169
- meeting: { event_id: n.meeting.event_id || null, start: n.meeting.start, title: n.meeting.title },
173
+ meeting: { event_id: n.meeting.event_id || null, start: n.meeting.start, title: n.meeting.title, series: n.meeting.context && n.meeting.context.series || null },
174
+ overview: String(ov).replace(/\s+/g, ' ').trim().slice(0, 600),
170
175
  decisions: pick(fin && fin.decisions, sec && sec.decisions).map((x) => ({ text: x && x.text })),
171
176
  actions: pick(fin && fin.actions, sec && sec.actions).map((x) => ({ text: x && x.text, owner: x && x.owner || null, due: x && x.due || null })) });
172
177
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.147.0",
3
+ "version": "0.148.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.147.0",
9
+ "version": "0.148.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.147.0",
3
+ "version": "0.148.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",
@@ -533,7 +533,7 @@ function openRef(ui, ref, extra) {
533
533
 
534
534
  /* the home chip: where the record lives (the mockup's UI.homeChip) */
535
535
  const homeChip = (name) => el('span', { class: 'chip2', title: 'Where this lives', 'data-cal3': 'home' }, c3ic('repo', 'sm'), name || 'its home');
536
- const peopleOf = (e) => [].concat(e.people || e.with || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
536
+ const peopleOf = (e) => [].concat(e.people || e.with || e.attendees || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
537
537
 
538
538
  function closeBtn(ui) {
539
539
  return button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { class: 'cal3-close', 'data-cal3': 'close' }, onClick: () => { ui.st.sel = null; ui.redraw('[data-cal3-chip="meetings"]'); } });
@@ -581,22 +581,78 @@ function meetingDetail(ui, hit) {
581
581
  };
582
582
  let body;
583
583
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
584
- else if (!rec || rec.held === false) body = [fold('moon', past ? 'Nothing held for this meeting: no note, decisions or linked items yet.' : 'Nothing to prepare for this meeting.', { 'data-cal3': 'nothing-held' })];
584
+ else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
585
585
  else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
586
- else if (!items.some((x) => x.kind === 'prepared' || x.kind === 'todo')) body = [el('p', { class: 'dim cal3-note', 'data-cal3': 'nothing-to-prepare' }, 'Nothing to prepare for this meeting.')];
587
- else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
586
+ else body = upcomingBody(ui, rec, items);
587
+ const b = !past && rec && rec.brief ? rec.brief : null;
588
+ const iniId = typeof e.initiative === 'string' ? e.initiative : b && b.initiative ? b.initiative.id : null;
589
+ const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
590
+ const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
591
+ const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
588
592
  return [
589
593
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
590
594
  el('b', { class: 'cal3-title' }, e.title || e.id),
591
595
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
592
- el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
593
- e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
596
+ el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
597
+ iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
598
+ iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
594
599
  noteLink(ui, e),
595
600
  ...body,
596
- el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
601
+ el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
597
602
  ];
598
603
  }
599
604
 
605
+ /* the card before a meeting (initiative c3-meeting-card, the mockup the PM approved on 2026-10-12): what it is for, what
606
+ * matters today, what happened last time and what to bring. Every part comes from the server's `brief` (lib/calendar-layers.js
607
+ * briefOf) or the prep items; a part with no record is left out. When PMOS knows little, it says what is missing. */
608
+ const STATUS_TONE = { 'at risk': 'warn', delayed: 'bad', overdue: 'bad' };
609
+ const listWords = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
610
+ const ctxBox = (key, title, ...kids) => el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, ...[].concat(title)), ...kids);
611
+ const ul = (rows) => el('ul', { class: 'cal3-cl', role: 'list' }, ...rows);
612
+ const ci = (attrs, ...kids) => el('li', { class: 'cal3-ci', ...attrs }, ...kids);
613
+
614
+ function upcomingBody(ui, rec, items) {
615
+ const { h } = ui;
616
+ const b = rec && rec.brief ? rec.brief : null;
617
+ const prep = items.filter((x) => x.kind === 'prepared' || x.kind === 'todo');
618
+ const out = [];
619
+ if (b && b.gist && b.gist.length) out.push(el('div', { class: 'cal3-ctx cal3-gist', 'data-cal3-ctx': 'gist' }, el('h4', null, c3ic('spark', 'sm'), 'In short'), el('p', { class: 'cal3-p' }, b.gist.join(' '))));
620
+ if (b && b.matters && b.matters.length) out.push(ctxBox('matters', 'What matters today', ul(b.matters.map((x) => ci({ 'data-cal3-matter': x.tag },
621
+ el('span', { class: 'tag cal3-tone-' + x.tone }, x.tag), el('span', { class: 'cal3-ct' }, x.text), x.meta ? el('span', { class: 'dim mono cal3-at' }, x.meta) : null,
622
+ refLink(ui, x.ref, { initiative: x.ref.initiative, title: x.text }))))));
623
+ if (b && b.last) {
624
+ const l = b.last;
625
+ const open = ui.r.ctx.overlay ? null : el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-last-note', title: 'Open the note: ' + (l.title || 'Untitled note'),
626
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, l.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the last note');
627
+ out.push(ctxBox('last', 'Since the last one · ' + h.dayLabel(String(l.at).slice(0, 10)),
628
+ l.private ? el('p', { class: 'dim cal3-note' }, 'That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.')
629
+ : l.overview ? el('p', { class: 'cal3-p' }, l.overview) : el('p', { class: 'dim cal3-note' }, 'That note has no summary yet.'),
630
+ !l.private && ((l.decisions || []).length || (l.actions || []).length) ? ul([
631
+ ...(l.decisions || []).map((t) => ci({ 'data-cal3-item': 'last-decision' }, el('span', { class: 'tag' }, 'Decision'), el('span', { class: 'cal3-ct' }, t))),
632
+ ...(l.actions || []).map((a) => ci({ 'data-cal3-item': 'last-action' }, el('span', { class: 'tag cal3-tone-warn' }, 'From last time'), el('span', { class: 'cal3-ct' }, a.text),
633
+ a.owner || a.due ? el('span', { class: 'dim mono cal3-at' }, [a.owner, a.due && 'due ' + a.due].filter(Boolean).join(' · ')) : null))]) : null,
634
+ el('div', { class: 'chips2' }, open, b.earlier > 1 ? el('span', { class: 'dim', 'data-cal3': 'earlier' }, b.earlier + ' earlier notes of this meeting') : null)));
635
+ }
636
+ if (prep.length || (b && b.prep)) out.push(ctxBox('prepare', 'To prepare', ul([
637
+ ...(b && b.prep ? [ci({ 'data-cal3-item': 'ready' }, el('span', { class: 'tag' }, 'To be ready'), el('span', { class: 'cal3-ct' }, b.prep))] : []),
638
+ ...prep.filter((x) => !(b && b.prep && /^Prep notes from your agent/.test(x.text))).map((x) => ci({ 'data-cal3-item': x.kind },
639
+ el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
640
+ x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
641
+ x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
642
+ if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
643
+ ...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
644
+ ...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
645
+ : el('span', { class: 'chip2' }, r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null))));
646
+ const noIni = !(b && b.initiative); const noLast = !(b && b.last);
647
+ if (noIni || noLast) {
648
+ const missing = noIni && noLast ? 'No initiative is linked and there is no earlier note from this meeting.' : noIni ? 'No initiative is linked to this meeting.' : 'There is no earlier note from this meeting.';
649
+ out.push(ctxBox('knows', 'What PMOS knows', fold('info', missing + (out.length ? '' : ' Nothing is recorded to prepare.'), { 'data-cal3': 'knows' }),
650
+ ul([noIni ? ci({}, el('span', { class: 'tag' }, 'Link'), el('span', { class: 'cal3-ct' }, 'Link an initiative, and its blocked, late and due items show here.')) : null,
651
+ ci({}, el('span', { class: 'tag' }, 'Note'), el('span', { class: 'cal3-ct' }, 'Take a note in this meeting, and the next one opens with its summary and next steps.'))].filter(Boolean))));
652
+ }
653
+ return out;
654
+ }
655
+
600
656
  /* the free blocks of the range in words, with the suggested use or none; nothing is written */
601
657
  function freeList(ui, range) {
602
658
  const { M, h } = ui;
@@ -103,7 +103,21 @@ const CONTEXT = {
103
103
  'm4@2026-10-08': { phase: 'upcoming', held: true, items: [
104
104
  { kind: 'prepared', text: 'Three scope options with their dates', ref: init('checkout') },
105
105
  { kind: 'todo', text: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', ref: item('item-demo010') },
106
- ] },
106
+ ],
107
+ // the card before a meeting (initiative c3-meeting-card): what the server's briefOf puts together
108
+ brief: {
109
+ initiative: { id: 'checkout', title: 'Checkout v2', status: 'at risk' },
110
+ gist: ['For Checkout v2, which is at risk.', '1 item blocked, 1 item due today.'],
111
+ matters: [
112
+ { tone: 'bad', tag: 'Blocked', text: 'Sprint scope: waiting on the scope decision', meta: 'Dara · due 2026-10-08', ref: { type: 'plan-item', id: 'checkout-scope', initiative: 'checkout' } },
113
+ { tone: 'warn', tag: 'Due today', text: 'Confirm the 1 Oct numbers', meta: 'yours · due 2026-10-08', ref: item('item-demo001') },
114
+ ],
115
+ last: { id: 'note-demo-sp1', title: 'Sprint planning', at: '2026-10-01T14:00:00', private: false,
116
+ overview: 'The team split the checkout flows into 3 core and 2 later. Dara asked for a scope decision before this sprint.',
117
+ decisions: ['Payments stays in the first release.'], actions: [{ text: 'Send the 3 scope options', owner: 'You', due: 'Wed 7 Oct' }] },
118
+ earlier: 3, purpose: null, prep: null, links: [], refs: [],
119
+ sources: ['the calendar', '3 earlier notes of this meeting', 'the checkout plan', 'your items'],
120
+ } },
107
121
  'm5@2026-10-08': { phase: 'upcoming', held: false, items: [] },
108
122
  };
109
123
 
@@ -251,11 +251,11 @@ const MEET = [
251
251
  { id: 'm1', t: 'Standup', s: 9.5, e: 10, kind: 'ready', routine: true },
252
252
  { id: 'm2', t: 'Checkout sync', s: 10.5, e: 11.25, kind: 'prep ready' },
253
253
  { id: 'm3', t: 'Customer interview', s: 13, e: 14, kind: 'review prep' },
254
- { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00' },
254
+ { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00', att: ['Dara', 'Priya', 'Sam'] },
255
255
  { id: 'm5', t: 'Leadership update', s: 16.5, e: 17, kind: 'ready' },
256
256
  ];
257
257
  const hh = (x) => pad(Math.floor(x)) + ':' + pad(Math.round((x % 1) * 60));
258
- const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
258
+ const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.att ? { attendees: m.att } : {}), ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
259
259
  function eventsOn(date) {
260
260
  const dow = new Date(utcOf(date)).getUTCDay();
261
261
  if (date === TODAY) return MEET.map((m) => ev(date, m));
@@ -11,7 +11,7 @@
11
11
  * links and references, people. A note with no meeting says so and offers the main app's link pickers.
12
12
  * During the main app's live block while the note is recorded or paused (view-notes.js renderLive), else the transcript.
13
13
  * After the Notes card (decisions with why, next steps with owner, due and Make task, questions, conflicts, highlights,
14
- * the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
14
+ * the screenshots of the shared screen, the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
15
15
  *
16
16
  * Nothing is invented: a part the note does not hold is left out or says so. The mockup's Rejected column, Settle, told and
17
17
  * not told, Route actions and the drafts have no data or route yet, so they are not drawn.
@@ -30,6 +30,7 @@ import { groupNotes, partsOf, conflictsOf, whenShort } from './dest-notes.js';
30
30
  import * as dock from './note-dock.js';
31
31
  import * as V from './view-notes.js';
32
32
  import { makeTask } from './notes-task.js';
33
+ import { screenStrip } from './notes-screen-view.js';
33
34
 
34
35
  const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
35
36
  const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
@@ -91,13 +92,13 @@ async function read(s) {
91
92
  if (seq !== s.seq) return;
92
93
  if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
93
94
  if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
94
- if (one && one.ok && one.note && one.note.id === s.open) s.note = one.note;
95
+ if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; }
95
96
  draw(s);
96
97
  }
97
98
 
98
99
  function choose(s, id) {
99
100
  if (s.open === id) return;
100
- s.open = id; s.note = null; s.phase = null;
101
+ s.open = id; s.note = null; s.phase = null; s.shots = []; s.rec = null;
101
102
  draw(s); read(s);
102
103
  }
103
104
 
@@ -164,14 +165,45 @@ function noteCol(s) {
164
165
  el('div', { class: 'stg', role: 'group', 'aria-label': 'Note stage', 'data-cv': 'phase' },
165
166
  PHASES.map(([k, ic, l]) => btn('sgb', { 'data-cal3-phase': k, 'aria-pressed': String(ph === k), title: l }, [c3ic(ic), el('span', null, l)], () => { s.phase = k; draw(s); })))),
166
167
  el('div', { class: 'chips2 dim cal3-sm' },
167
- [dayOf(startOf(n)), len, iniName(n.initiative) || 'No initiative'].filter(Boolean).join(' · '),
168
- ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('cal', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev))
169
- : linked(n) ? null : el('span', { class: 'tag' }, 'no meeting'),
170
- btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))));
168
+ [dayOf(startOf(n)), len].filter(Boolean).join(' · '),
169
+ btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))),
170
+ links(s, n, ev));
171
171
  const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
172
172
  return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
173
173
  }
174
174
 
175
+ /* the note's meeting and initiative, each with its link: what it is linked to, then Link or Change (view-notes.js pickers) */
176
+ function links(s, n, ev) {
177
+ const adhoc = n.meeting && n.meeting.kind === 'adhoc';
178
+ const when = ev ? [dayOf(ev.start), hm(ev.start) + (ev.end ? ' to ' + hm(ev.end) : '')].filter(Boolean).join(' ') : '';
179
+ return el('div', { class: 'cal3-nlinks cal3-sm', 'data-cal3-notes': 'links' },
180
+ el('span', { class: 'chips2' }, c3ic('cal', 'sm'),
181
+ ev ? el('span', { 'data-cal3-notes': 'meeting' }, el('b', null, ev.title), ' · ' + when)
182
+ : el('span', { class: 'dim', 'data-cal3-notes': 'meeting' }, adhoc ? 'A meeting that is not on the calendar' : 'No meeting'),
183
+ !ev && !adhoc ? el('span', { class: 'tag' }, 'no meeting') : null,
184
+ btn('chip2 lk', { 'data-cal3-notes': 'meeting-link', title: ev || adhoc ? 'Link the note to another meeting' : 'Link the note to a meeting on the calendar' }, ev || adhoc ? 'Change' : 'Link to a meeting', () => V.linkMeeting(vctx(s), n.id)),
185
+ ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('next', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev)) : null),
186
+ el('span', { class: 'chips2' }, c3ic('init', 'sm'),
187
+ n.initiative ? el('span', { 'data-cal3-notes': 'initiative' }, el('b', null, iniName(n.initiative))) : el('span', { class: 'dim', 'data-cal3-notes': 'initiative' }, 'No initiative'),
188
+ btn('chip2 lk', { 'data-cal3-notes': 'initiative-link', title: n.initiative ? 'Link the note to another initiative' : 'Link the note to an initiative' }, n.initiative ? 'Change' : 'Link to an initiative', () => V.linkInitiative(vctx(s), n.id))));
189
+ }
190
+
191
+ /* the replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on (as dest-notes.js does) */
192
+ function replayOf(s, n) {
193
+ const rec = s.rec;
194
+ if (!rec || n.private) { s.audio = null; return null; }
195
+ const key = n.id + ':' + rec.bytes + ':' + (rec.cleaned ? 1 : 0);
196
+ if (s.audio && (s.audio.key === key || !s.audio.el.paused)) return s.audio.el;
197
+ const a = el('audio', { controls: true, preload: 'metadata', class: 'nt-audio cal3-audio', 'data-cal3-notes': 'audio', src: '/api/notes/recording?id=' + encodeURIComponent(n.id) + '&b=' + rec.bytes + (rec.cleaned ? '&c=1' : '') });
198
+ s.audio = { key, el: a };
199
+ return a;
200
+ }
201
+ function seek(s, ms) {
202
+ const a = s.audio && s.audio.el;
203
+ if (!a) return;
204
+ try { a.currentTime = Math.max(0, ms / 1000); a.play(); } catch { /* the audio is not ready */ }
205
+ }
206
+
175
207
  const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
176
208
  const linkButtons = (s, n) => el('div', { class: 'chips2' },
177
209
  btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
@@ -206,7 +238,9 @@ function before(s, n) {
206
238
  if (names.length) rows.push(prepRow('user', 'People', el('span', { class: 'chips2 cal3-wrapl' }, names.map(avatar))));
207
239
  else if (c && c.attendees) rows.push(prepRow('user', 'People', c.attendees + ' attendees. Names are not kept while calendar names are off.'));
208
240
  const when = (ev.start ? dayOf(ev.start) + ' ' + hm(ev.start) : '') + (ev.end ? ' to ' + hm(ev.end) : '');
209
- rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null].filter(Boolean).join(' · ')));
241
+ rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.where ? 'where: ' + c.where : null, c && c.organizer ? 'organizer: ' + c.organizer : null,
242
+ c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null, c && c.external ? 'with people outside the company' : null,
243
+ c && c.attendees && names.length ? c.attendees + ' attendees' : null].filter(Boolean).join(' · ')));
210
244
  return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
211
245
  c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
212
246
  }
@@ -252,6 +286,7 @@ function after(s, n, mode) {
252
286
  const kids = [
253
287
  el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
254
288
  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
+ ...(() => { 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.'))] : []; })(),
255
290
  ...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 : '')))),
256
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' })
257
292
  : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
@@ -268,7 +303,10 @@ function after(s, n, mode) {
268
303
  const hl = n.highlights || [];
269
304
  if (hl.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'highlights' }, el('h4', null, 'Highlights'),
270
305
  el('ul', { class: 'cal3-cl', role: 'list' }, hl.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'dim mono cal3-at' }, typeof x.t === 'number' ? V.fmtTime(x.t) : ''), el('span', { class: 'tag' }, x.label || 'Highlight'), el('span', { class: 'cal3-ct' }, x.text))))));
271
- if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
306
+ if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length && !(s.shots || []).length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
307
+ // the screenshots of the shared screen, after Stop, as the main app's Notes shows them (notes-screen-view.js)
308
+ const shots = mode || n.private ? [] : s.shots || [];
309
+ if (shots.length) kids.push(el('div', { 'data-cal3-notes': 'screen' }, screenStrip({ id: n.id, frames: shots, seek: s.audio ? (ms) => seek(s, ms) : null, replay: s.audio ? s.audio.el : null })));
272
310
  const tr = transcriptOf(n);
273
311
  if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
274
312
  el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
@@ -529,6 +529,9 @@
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 .cal3-nlinks{display:grid;gap:var(--sp-1);margin-top:var(--sp-1)}
533
+ .c3 .cal3-replay{display:grid;gap:var(--sp-1)}
534
+ .c3 .cal3-replay audio{width:100%;max-width:100%}
532
535
  /* a primitives.js button drawn as a console3 control: the primitive's own height, padding, nowrap and hover come off */
533
536
  .c3 .cal3-b{height:auto;min-height:0;line-height:inherit;font-weight:inherit;justify-content:flex-start;text-align:left}
534
537
  .c3 .cal3-b.chip2{padding:0;border:0;background:transparent;white-space:nowrap}
@@ -1900,3 +1903,7 @@
1900
1903
  .c3 .kp.k-foreground{--c:var(--foreground)}
1901
1904
  .c3 .kp.k-muted-foreground{--c:var(--muted-foreground)}
1902
1905
  .c3 .psess{display:inline-flex;gap:6px;align-items:center}
1906
+ /* the card before a meeting (initiative c3-meeting-card) */
1907
+ .c3 .cal3-ctx .cal3-p{margin:0;font-size:var(--fs-2);line-height:1.5;color:var(--foreground)}
1908
+ .c3 .cal3-ctx.cal3-gist{padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--muted)}
1909
+ .c3 .cal3-ctx h4{display:flex;align-items:center;gap:6px}
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * PAUSE keeps the meeting open and resumable for an hour (the server ends it by itself after that and writes the final
10
10
  * notes). STOP ends it for good, asks first, and writes the final notes now. A stopped meeting cannot be resumed. A note
11
- * that is stopped within a minute, or before anything was heard, with nothing typed, is deleted instead (a recording started
12
- * by mistake must not take the place of the meeting before it). Resume applies to the most recent meeting only.
11
+ * that is stopped before anything was heard, with nothing typed, is deleted instead (a recording started by mistake must not
12
+ * take the place of the meeting before it). A short note with speech in it is kept. Resume applies to the most recent meeting only.
13
13
  *
14
14
  * What is held is read, not remembered: the listening note comes from pm/view-notes.js, and a paused meeting from the note
15
15
  * rows (GET /api/notes: state, resume_until, sessions), read again every REREAD_MS and after each action.
@@ -28,7 +28,6 @@ import { state, setState, subscribe, FIXTURE } from './state.js';
28
28
  import { meetingOf, refreshNotes, noteById, notesRows, onNotes } from './notes-links.js';
29
29
 
30
30
  const REREAD_MS = 30000;
31
- const MIN = 60000;
32
31
  let V = null; // pm/view-notes.js, read the first time it is needed
33
32
  let mounted = false;
34
33
  const viewCtx = { state, setState, FIXTURE };
@@ -163,14 +162,12 @@ export async function resumeNow() {
163
162
  return ok;
164
163
  }
165
164
 
166
- /** Is this note one that was started by mistake? Within a minute, or nothing heard, nothing typed, no highlight, one stretch. Pure. */
167
- export function discardable(n, now = Date.now()) {
165
+ /** Is this note one that was started by mistake? Nothing heard, nothing typed, no highlight, one stretch. A short note with speech is kept. Pure. */
166
+ export function discardable(n) {
168
167
  if (!n || String(n.body || '').trim() || (n.highlights || []).length) return false;
169
168
  const s = n.sessions || [];
170
169
  if (s.length !== 1) return false;
171
- const from = Date.parse(s[0].from); const to = s[0].to ? Date.parse(s[0].to) : now;
172
- const heard = (n.transcript || []).some((x) => !x.dropped);
173
- return !heard || to - from < MIN;
170
+ return !(n.transcript || []).some((x) => !x.dropped);
174
171
  }
175
172
 
176
173
  /** The "Stop for good?" pop-up. Resolves 'stop', 'pause' (only while recording) or null. */
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative c3-meeting-card: the console3 Calendar's card before a meeting (In short, What matters today, Since the last one, To prepare, What PMOS knows).",
3
+ "panels": ["pm-c3-meeting-card-test.js"],
4
+ "seconds": { "pm-c3-meeting-card-test.js": 40 }
5
+ }