create-pmos 0.112.0 → 0.113.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.112.0",
3
+ "version": "0.113.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@3389fa8
1
+ kernel-source: pmos@6d04787
2
2
  assembled: 2026-10-08
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-08T04:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
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-08T04:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
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-08T04:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T05:19:33Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Today: what needs you"
4
4
  description: "The Agenda month and the calendar it remembers, the meeting notes of a meeting, the two-column day (your jobs and the agent's jobs), the eight kinds of card on Today, the one button on each card, the count, the empty state, and the pmos today command."
5
- timestamp: 2026-10-07
5
+ timestamp: 2026-10-08
6
6
  ---
7
7
 
8
8
  # Today: what needs you
@@ -52,6 +52,8 @@ Each day of the month answers three questions:
52
52
 
53
53
  The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
54
54
 
55
+ A meeting's detail shows one focus. Under the title is one line: the time, how many people join, and a star when the meeting is key. Under it are the goal of the meeting and, for a meeting with prep, one line of progress (the steps done, a bar, the free minutes before the meeting). Only the first open step is expanded, with what its source says, its link and, for a note step, a box for your note. The one primary button follows it: **I have read it**, **Mark done** or **Done**, then **Mark ready** when every step is done, then **Take notes** once the meeting has started. Press the arrow at the end of another step to open it. **More** holds the rare presses: **Mark ready** while steps are left, **Remind me**, **Skip prep**, the key mark and **Add as a goal for today**. **Details** holds the people, the kind of meeting and how often it met, the initiative, the goal it counts toward, During and After, the mail and the links.
56
+
55
57
  A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. PMOS starts to listen at once and keeps the note small at the bottom right. Press it to open the note as a pop-up. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the pop-up, and the meeting's row shows **notes**. See [Notes](notes.md).
56
58
 
57
59
  The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.112.0",
3
+ "version": "0.113.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.112.0",
9
+ "version": "0.113.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.112.0",
3
+ "version": "0.113.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",
@@ -3608,18 +3608,31 @@
3608
3608
  .td-day__mark { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 6px 10px; border: 1px solid var(--border); background: var(--muted); font-size: 14px; }
3609
3609
  .td-day__mark[data-day-mark-state="done"] { border-color: var(--foreground); }
3610
3610
  .td-day__ready { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 12px; }
3611
- .td-day__prep { display: grid; gap: 8px; padding: 10px 12px; border: 1px solid var(--border); background: var(--background); }
3611
+ .td-day__prep { display: grid; gap: 10px; }
3612
3612
  .td-day__ended { margin: 0; padding: 8px 12px; border-left: 2px solid var(--foreground); background: var(--muted); font-size: 14px; }
3613
3613
  /* an ended meeting keeps its prep, quieter: the steps stay readable, the past is not a call to act */
3614
- .td-day__detail:has(.td-day__ended) .td-day__prep { background: var(--muted); }
3614
+ .td-day__detail:has(.td-day__ended) .td-day__prep { padding: 10px 12px; background: var(--muted); }
3615
3615
  .td-day__fold { border-top: 1px solid var(--border); padding-top: 6px; }
3616
3616
  .td-day__fold > summary { cursor: pointer; font-size: 12px; color: var(--muted-foreground); }
3617
3617
  .td-day__fold[open] > summary { margin-bottom: 6px; }
3618
- .td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
3619
- @media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary { min-height: 44px; } }
3618
+ .td-day__fold > summary:focus-visible, .td-day__fold[open] > summary:focus-visible, .td-day__ref > summary:focus-visible, .td-day__details > summary:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
3619
+ @media (pointer: coarse) { .td-day__fold > summary, .td-day__fold[open] > summary, .td-day__ref > summary, .td-day__details > summary { min-height: 44px; } }
3620
3620
  .td-day__fold-head { justify-self: start; }
3621
3621
  .td-day__steprow { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3622
- .td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox) { flex-direction: column; align-items: stretch; }
3622
+ .td-day__step2:has(.td-day__ref), .td-day__step2:has(.td-day__notebox), .td-day__step2:has(.td-day__acts) { flex-direction: column; align-items: stretch; }
3623
+ /* the meeting detail with one focus (today-meeting-focus): one open step, the rest one line */
3624
+ .td-day__meta { text-transform: none; letter-spacing: 0; font-size: 12px; font-variant-numeric: tabular-nums; }
3625
+ .td-day__purpose { max-width: 64ch; text-wrap: pretty; }
3626
+ .td-day__step2[data-cur="false"] { border-color: transparent; background: transparent; padding-block: 2px; }
3627
+ .td-day__step2[data-cur="true"][data-done="false"] { border-color: var(--foreground); }
3628
+ .td-day__stepopen { margin-left: auto; }
3629
+ .td-day__free[data-tight="true"] { color: var(--warning); }
3630
+ .td-day__free { font-variant-numeric: tabular-nums; color: var(--muted-foreground); }
3631
+ .td-day__foot { position: relative; }
3632
+ .td-day__foot > [data-day="more-menu"] { position: absolute; right: 0; top: 2px; }
3633
+ .td-day__details > summary { padding-right: 96px; line-height: 28px; }
3634
+ .td-day__notes--quiet, .td-day__notes--live { border: 0; padding: 0; background: none; }
3635
+ .td-day__prepfold { border-top: 0; padding-top: 0; }
3623
3636
  .td-day__ref { display: grid; gap: 4px; padding: 6px 8px; background: var(--muted); font-size: 14px; }
3624
3637
  .td-day__ref > summary { display: flex; justify-content: space-between; gap: 8px; cursor: pointer; font-weight: 600; color: var(--foreground); }
3625
3638
  .td-day__reflines { margin: 0; padding-left: 18px; display: grid; gap: 2px; color: var(--foreground); line-height: 1.45; }
@@ -87,7 +87,15 @@ export function notesBlock(e, opts = {}) {
87
87
  el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openDock(n.id) })));
88
88
  }
89
89
  if (!notesKnown()) return null;
90
- const take = button({ label: 'Take notes', variant: ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
90
+ // opts.mode (the meeting detail, today-meeting-focus F7): 'live' leads with Take notes as the one primary button and one line;
91
+ // 'quiet' is the button alone, with no heading or help text. Without a mode the block reads as it always did.
92
+ const mode = ended ? null : opts.mode || null;
93
+ const take = button({ label: 'Take notes', variant: mode === 'quiet' ? 'ghost' : ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
94
+ if (mode) {
95
+ return el('div', { class: 'td-day__notes td-day__notes--' + mode, 'data-notes-block': 'none' },
96
+ mode === 'live' && opts.liveText ? el('p', { class: 'td-day__note' }, opts.liveText) : null,
97
+ el('div', { class: 'td-day__acts' }, take));
98
+ }
91
99
  return el('div', { class: 'td-day__notes', 'data-notes-block': 'none' },
92
100
  el('h4', null, 'Meeting notes'),
93
101
  el('p', { class: 'td-day__note' }, ended ? 'No notes were taken in PMOS for this meeting.' : 'Take notes when the meeting starts. PMOS listens at once and keeps the note at the bottom right. It is linked to this event' + (opts.initiative ? ' and its initiative.' : '.')),
@@ -298,22 +298,43 @@ function keyBlock(e) {
298
298
  }
299
299
 
300
300
  /** The series of a meeting: how often it met, what is to come, and the last time, folded. Null when unknown. */
301
- export function memoryBlock(id) {
301
+ export function memoryBlock(id, { compact = false } = {}) {
302
302
  const f = findEvent(id);
303
303
  if (!f) return null;
304
304
  const s = f.data.series[f.e.key];
305
305
  if (!s) return null;
306
306
  const last = s.last && s.last.date !== f.day.date ? s.last : null;
307
+ // compact (the meeting detail's Details, today-meeting-focus F5): no heading and no key presses; the presses are in More
308
+ const said = compact && f.e.mark ? A.key[(f.e.imp ? 'marked' : 'not') + f.e.mark] : null;
307
309
  return el('div', { class: 'td-day__notes', 'data-ag': 'memory' },
308
- el('h4', null, A.means),
310
+ compact ? null : el('h4', null, A.means),
309
311
  el('p', null, A.kindWord[f.e.kind] + '. ' + (s.count > 1 ? A.series(s.past, s.coming) : A.once)),
310
312
  ...(f.e.mark ? [] : whyLines(f.e)).map((t) => el('p', null, t)),
311
- keyBlock(f.e),
313
+ compact ? (said ? el('p', { class: 'td-day__note', 'data-ag': 'key', 'data-ag-key': f.e.imp ? 'true' : 'false' }, said) : null) : keyBlock(f.e),
312
314
  last ? el('details', { class: 'td-ag__last', 'data-ag': 'last' }, el('summary', null, A.lastTime(dayLabel(last.date))),
313
315
  last.notes ? el('p', null, last.notes) : el('p', { class: 'td-day__note' }, A.noLast),
314
316
  last.open && last.open.length ? el('ul', { class: 'td-day__chk' }, ...last.open.map((x) => el('li', null, x))) : null) : null);
315
317
  }
316
318
 
319
+ /** What the meeting detail's one line needs: how many people join, and whether the meeting is key. Null when unknown. */
320
+ export function meetingFacts(id) {
321
+ const f = findEvent(id);
322
+ return f ? { people: f.e.attendees || 0, key: !!f.e.imp } : null;
323
+ }
324
+
325
+ /** The key-mark presses of a meeting as menu items for More (the same presses as keyBlock, same marks). */
326
+ export function keyItems(id) {
327
+ const f = findEvent(id);
328
+ if (!f) return [];
329
+ const e = f.e;
330
+ const K = A.key;
331
+ const it = (label, scope, on, mark) => ({ label, attrs: { 'data-ag-mark': mark }, onSelect: () => setMark(e, scope, on) });
332
+ const items = e.mark ? [it(K.clear, e.mark, null, 'clear')]
333
+ : e.imp ? [it(K.unmark, 'one', false, 'not-one'), e.repeats ? it(K.unmarkAll, 'series', false, 'not-series') : null]
334
+ : [it(K.mark, 'one', true, 'one'), e.repeats ? it(K.markAll, 'series', true, 'series') : null];
335
+ return items.filter(Boolean);
336
+ }
337
+
317
338
  export function meetingDetail(ctx, id, h) {
318
339
  const f = findEvent(id);
319
340
  if (!f) return null;
@@ -385,6 +385,19 @@ export const COPY = {
385
385
  hold: 'Waiting on…',
386
386
  notMine: 'Not mine',
387
387
  skip: 'Skip prep',
388
+ // the meeting detail with one focus (initiative today-meeting-focus, PRD F1 to F7)
389
+ focus: {
390
+ more: 'More', details: 'Details', openStep: 'Open this step',
391
+ press: { read: 'I have read it', do: 'Mark done', note: 'Done' },
392
+ free: (min) => `${min} min free`,
393
+ before: (d, n, at) => `${d} of ${n} before ${at}`,
394
+ remindAt: (when, at) => `Remind me ${when} · ${at}`,
395
+ skipWhy: (why) => `Skip prep · ${why}`,
396
+ people: (n) => (n === 1 ? '1 person' : `${n} people`),
397
+ key: '★ Key',
398
+ live: 'The meeting has started. PMOS listens when you start the note, and links it to this event.',
399
+ prepFold: (d, n) => `Prep · ${d} of ${n} done`,
400
+ },
388
401
  snoozeTo: { hour: 'In 1 hour', later: 'Later today, 16:00', due: 'Before it is due', end: 'End of the day, 17:30', m30: '30 min before it', m15: '15 min before it' },
389
402
  holdWho: 'Who or what you wait on',
390
403
  holdHint: 'For example: Raj, or the cost model',
@@ -31,7 +31,7 @@ import { FIXTURE } from './state.js';
31
31
  import { COPY } from './today-copy.js';
32
32
  import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
33
33
  import { peopleLinks } from './people-link.js';
34
- import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
34
+ import { agendaPanel, meetingDetail, memoryBlock, meetingFacts, keyItems, refreshAgenda, agendaSig, onAgenda } from './today-agenda.js';
35
35
  import { notesBlock, onNotes } from './notes-links.js';
36
36
 
37
37
  const D = COPY.day;
@@ -62,7 +62,7 @@ const ui = {
62
62
  sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
63
63
  revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
64
64
  // today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
65
- groupsOpen: new Set(), holding: null, holdDraft: { who: '', when: 'h2' }, compose: new Map(), verdict: new Map(),
65
+ cur: new Map(), groupsOpen: new Set(), holding: null, holdDraft: { who: '', when: 'h2' }, compose: new Map(), verdict: new Map(),
66
66
  };
67
67
  let rerender = () => {};
68
68
  let notesSub = null; // the redraw when the meeting notes list is read (pm/notes-links.js)
@@ -670,16 +670,35 @@ function markBanner(ctx, id, mark, prep) {
670
670
  }
671
671
  /** The steps the PM checks off. A step the brief already marked done stays done. A step that reads a source shows what
672
672
  * the source says under it (PRD X2), and a note step holds the PM's own note (PRD F3), so no other file needs opening. */
673
- function stepChecks(ctx, id, steps, { refs = [], mark = null } = {}) {
673
+ function stepChecks(ctx, id, steps, { refs = [], mark = null, focus = false } = {}) {
674
674
  if (!steps.length) return null;
675
+ const F = A.focus;
676
+ // focus (the meeting detail, today-meeting-focus F2): only one step is open, the PM's own pick or else the first step left
677
+ const want = focus ? ui.cur.get(id) : null;
678
+ const cur = focus ? steps.find((x) => x.t === want) || steps.find((x) => !x.done) || null : null;
675
679
  return el('ul', { class: 'td-day__steps2', 'data-day': 'steps' }, ...steps.map((st) => {
680
+ const isCur = !focus || (!!cur && st.t === cur.t);
676
681
  const f = field({ kind: 'check', label: st.t, value: !!st.done, attrs: { 'data-day-step': st.t, disabled: st.fixed ? true : null },
677
682
  onCommit: (v) => markPress(ctx.ws, id, { action: 'step', step: st.t, done: !!v }) });
678
683
  const r = Number.isInteger(st.ref) ? refs[st.ref] : null;
679
- return el('li', { class: 'td-day__step2', 'data-done': st.done ? 'true' : 'false' },
680
- el('div', { class: 'td-day__steprow' }, f, st.who ? el('span', { class: 'td-day__who' }, D.who[st.who] || st.who) : null),
681
- r ? refBlock(r, { open: !st.done, step: true }) : null,
682
- st.note ? noteBox(ctx, id, mark) : null);
684
+ const noteEl = st.note && isCur ? noteBox(ctx, id, mark) : null;
685
+ let press = null;
686
+ if (focus && isCur && !st.done) {
687
+ press = button({ label: F.press[st.note ? 'note' : r ? 'read' : 'do'], variant: 'primary', size: 'sm', attrs: { 'data-day-step-press': st.t, onMousedown: (ev) => ev.preventDefault() }, onClick: async () => {
688
+ // a note typed in the box is saved first, in this press, so the box losing focus does not redraw the card under the pointer
689
+ const ta = noteEl && noteEl.querySelector('textarea');
690
+ if (ta && ta.value !== ((mark && mark.note) || '')) await markPress(ctx.ws, id, { action: 'note', text: ta.value }, A.noteSaved);
691
+ ui.cur.delete(id);
692
+ await markPress(ctx.ws, id, { action: 'step', step: st.t, done: true });
693
+ } });
694
+ }
695
+ const opener = focus && !isCur && (r || st.note)
696
+ ? iconButton({ icon: 'chevron-down', ariaLabel: `${F.openStep}: ${st.t}`, size: 'sm', attrs: { 'data-day-step-open': st.t, class: 'td-day__stepopen' }, onClick: () => { ui.cur.set(id, st.t); rerender(); } }) : null;
697
+ return el('li', { class: 'td-day__step2', 'data-done': st.done ? 'true' : 'false', 'data-cur': focus ? String(isCur) : null },
698
+ el('div', { class: 'td-day__steprow' }, f, st.who ? el('span', { class: 'td-day__who' }, D.who[st.who] || st.who) : null, opener),
699
+ r && isCur ? refBlock(r, { open: !st.done, step: true }) : null,
700
+ noteEl,
701
+ press ? el('div', { class: 'td-day__acts' }, press) : null);
683
702
  }));
684
703
  }
685
704
  /** One source in a few lines, in the card: what it says, and a small link to the source for when the PM wants more. */
@@ -838,27 +857,57 @@ function freeBefore(m, e) {
838
857
  return Math.max(0, Math.round((e.s - m.now - busy) / MIN_MS));
839
858
  }
840
859
 
841
- /** A meeting's prep (PRD A1, C1, M1, M6, D2): the verb and the prep line, how ready the PM is, the Before steps to
842
- * check off, the button that fits, and the marks. */
843
- function prepBlock(ctx, e) {
860
+ /** A meeting's prep (today-act-prepare A1, C1, M1, M6, D2; today-meeting-focus F1 to F3): the prep sentence, one line of how
861
+ * ready the PM is, the Before steps with only the first open one expanded, and the one primary press that fits. The
862
+ * other presses are in More. `fold` (the meeting has started) folds all of it under one line. */
863
+ function prepBlock(ctx, e, { fold = false } = {}) {
844
864
  const { m } = ctx;
865
+ const F = A.focus;
845
866
  const steps = e.before.map((st) => ({ t: String(st.t || ''), who: st.who, done: st.done, ref: st.ref, note: !!st.note, fixed: !!(e.steps && e.steps.before && e.steps.before.find((x) => x.t === st.t && x.done)) }));
846
867
  const refs = Array.isArray(e.refs) ? e.refs : [];
847
868
  const d = steps.filter((x) => x.done).length; const total = steps.length;
848
869
  const left = total - d;
849
870
  const free = freeBefore(m, e);
850
- const fit = !total || e.ended ? null : !left ? A.fit.ok : free >= left * 15 ? A.fit.time(left, free) : A.fit.short(left, free);
871
+ const tight = !!left && free < left * 15;
851
872
  const open = !e.mark || !e.mark.state;
852
873
  const link = Array.isArray(e.links) ? e.links.find((l) => isUrl(l.url)) : null;
853
- return el('div', { class: 'td-day__prep', 'data-day': 'prep' },
854
- el('div', { class: 'td-day__jobtop' }, verbChip('prepare'), e.ended ? null : dueChip(m, e.mark, e.s, true)),
874
+ const body = el('div', { class: 'td-day__prep', 'data-day': 'prep' },
855
875
  e.prep ? el('p', { class: 'td-day__todo', 'data-day': 'todo' }, e.prep) : null,
856
876
  markBanner(ctx, e.id, e.mark, true),
857
- total ? el('div', { class: 'td-day__ready', 'data-day': 'readiness' }, meter({ done: d, total, label: A.readiness(d, total) }), fit ? el('span', { class: 'td-day__dim' }, fit) : null) : null,
858
- total ? el('div', { class: 'td-day__phase' }, el('h4', null, A.beforeTitle(e.time)), stepChecks(ctx, e.id, steps, { refs, mark: e.mark })) : null,
877
+ total ? el('div', { class: 'td-day__ready', 'data-day': 'readiness' }, meter({ done: d, total, label: F.before(d, total, e.time) }),
878
+ e.ended ? null : left ? el('span', { class: 'td-day__free', 'data-tight': String(tight), title: tight ? A.fit.short(left, free) : A.fit.time(left, free) }, F.free(free)) : el('span', { class: 'td-day__dim' }, A.fit.ok)) : null,
879
+ stepChecks(ctx, e.id, steps, { refs, mark: e.mark, focus: true }),
859
880
  refsBlock(refs, steps),
860
- open && link && left && !e.ended ? el('div', { class: 'td-day__acts', 'data-day': 'cta' }, primaryLink((link.label || link.url) + ' ↗', link.url, { 'data-day-link': 'prep' })) : null,
861
- marksBar(ctx, { id: e.id, title: e.title, kind: 'event', start: e.s, strong: total > 0 && !left }));
881
+ open && link && left && !e.ended ? el('div', { class: 'td-day__acts', 'data-day': 'cta' }, extLink((link.label || link.url) + ' ↗', link.url, { 'data-day-link': 'prep' })) : null,
882
+ open && !left ? el('div', { class: 'td-day__acts', 'data-day': 'marks' }, button({ label: A.ready, variant: 'primary', size: 'sm', icon: 'check', disabled: ui.busy.has('mark:' + e.id), attrs: { 'data-day-mark': 'done' },
883
+ onClick: () => markPress(ctx.ws, e.id, { action: 'done' }, A.did.ready(e.title)) })) : null);
884
+ return fold ? el('details', { class: 'td-day__fold td-day__prepfold', 'data-day': 'prep-fold' }, el('summary', null, F.prepFold(d, total)), body) : body;
885
+ }
886
+
887
+ /** The presses that are rarely needed, in one menu (today-meeting-focus F4): Mark ready while steps are left, Remind me, Skip
888
+ * prep, the key mark and, from `extra`, Add as a goal. Each press writes the same record, with the same toast and Undo, as before. */
889
+ function moreMenu(ctx, e, extra) {
890
+ const { m, ws } = ctx;
891
+ const F = A.focus;
892
+ const st = e.mark && e.mark.state;
893
+ const items = [];
894
+ if (e.hadPrep) {
895
+ const left = e.before.filter((x) => !x.done).length;
896
+ if (st !== 'done' && left > 0) items.push({ label: A.ready, attrs: { 'data-day-mark': 'done' }, onSelect: () => markPress(ws, e.id, { action: 'done' }, A.did.ready(e.title)) });
897
+ for (const [l, t] of snoozeTimes(m, { start: e.s })) {
898
+ items.push({ label: F.remindAt(l, hhmm(t)), attrs: { 'data-day-mark': 'snooze', 'data-day-snooze': hhmm(t) },
899
+ onSelect: () => markPress(ws, e.id, { action: 'snooze', until: new Date(t).toISOString() }, A.did.snooze(hhmm(t))) });
900
+ }
901
+ if (st !== 'dismissed') for (const w of ['nothing', 'elsewhere']) items.push({ label: F.skipWhy(A.why[w]), attrs: { 'data-day-mark': 'dismiss', 'data-day-why': w },
902
+ onSelect: () => markPress(ws, e.id, { action: 'dismiss', why: w }, A.did.dismiss(A.why[w])) });
903
+ }
904
+ const rest = [...keyItems(e.id), ...extra];
905
+ if (items.length && rest.length) items.push({ sep: true });
906
+ items.push(...rest);
907
+ if (!items.length) return null;
908
+ const b = button({ label: F.more, size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', attrs: { 'data-day': 'more-menu', 'aria-haspopup': 'menu' },
909
+ onClick: () => menu({ anchor: b, ariaLabel: F.more, items }) });
910
+ return b;
862
911
  }
863
912
 
864
913
  /* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
@@ -878,6 +927,7 @@ function stepItem(st) {
878
927
 
879
928
  function eventDetail(ctx, e) {
880
929
  const { m } = ctx;
930
+ const F = A.focus;
881
931
  const n = e.goal ? m.goalNo.get(e.goal) : null;
882
932
  const g = n ? m.allGoals[n - 1] : null;
883
933
  const ini = e.initiative && e.initiative.id ? e.initiative : null;
@@ -885,6 +935,8 @@ function eventDetail(ctx, e) {
885
935
  const notes = e.notes || {};
886
936
  const w = notes.wispr || null;
887
937
  const mail = notes.mail || null;
938
+ const started = Number.isFinite(e.s) && e.s <= m.now && !e.ended;
939
+ const facts = meetingFacts(e.id);
888
940
  const mailBlock = mail ? el('div', { class: 'td-day__notes', 'data-day': 'mail' }, el('div', { class: 'td-day__src' }, D.mail, ' · ', mail.from || '', ' ', isUrl(mail.link) ? el('a', { href: mail.link, target: '_blank', rel: 'noopener noreferrer' }, D.openLink) : null),
889
941
  el('p', null, el('b', null, (mail.subject || '') + '. '), mail.gist || '')) : null;
890
942
  const after = e.ended ? el('div', { class: 'td-day__notes', 'data-day': 'notes' }, el('h4', null, D.notesTitle),
@@ -893,29 +945,35 @@ function eventDetail(ctx, e) {
893
945
  list(D.decisions, (w.decisions || []).map(String), 'td-day__chk--ok'),
894
946
  list(D.actions, (w.actions || []).map((a) => (typeof a === 'string' ? a : stepItem(a))))] : note(D.noWispr),
895
947
  mailBlock || note(D.noMail)) : null;
896
- const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text))
897
- : [note(D.tieHow, { 'data-day': 'tie' }), m.allGoals.length < m.maxGoals ? el('div', { class: 'td-day__acts' }, button({ label: D.addAsGoal, size: 'sm', attrs: { 'data-day-act': 'add-goal' },
898
- onClick: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } })) : null];
899
- return [
900
- dhead(D.kind.meeting, e.title),
901
- e.ended ? el('p', { class: 'td-day__ended', 'data-day': 'ended', role: 'status' }, pill(D.pill.ended, ''), ' ', A.endedAt(hhmm(e.end || e.f))) : null,
902
- el('p', { class: 'td-day__dim td-day__mono' }, `${e.time}${e.end ? '–' + hhmm(e.end) : ''}${e.with ? ' · ' + e.with : ''}`),
948
+ const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text)) : null;
949
+ const addGoalItem = !g && m.allGoals.length < m.maxGoals
950
+ ? [{ label: D.addAsGoal, attrs: { 'data-day-act': 'add-goal' }, onSelect: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } }] : [];
951
+ // the one line under the title: the time, how many people join, and a star for a key meeting (F5)
952
+ const meta = [`${e.time}${e.end ? '–' + hhmm(e.end) : ''}`, facts && facts.people ? F.people(facts.people) : null, facts && facts.key ? F.key : null].filter(Boolean).join(' · ');
953
+ const duringAfter = [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem))];
954
+ const links = Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null;
955
+ const details = [
903
956
  e.with ? peopleLinks(ctx.ws, e.with, { 'data-day': 'people' }) : null,
904
- e.purpose ? el('p', null, el('b', null, D.purpose + ': '), e.purpose) : null,
905
- e.hadPrep ? prepBlock(ctx, e) : null,
957
+ memoryBlock(e.id, { compact: true }),
906
958
  ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
907
959
  ctx.openIni ? button({ label: D.openInitiative, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'open-initiative' }, onClick: () => ctx.openIni(ini.id) }) : null) : null,
908
960
  tie,
909
- memoryBlock(e.id),
961
+ duringAfter,
962
+ e.ended ? null : mailBlock,
963
+ links,
964
+ ];
965
+ const more = moreMenu(ctx, e, addGoalItem);
966
+ return [
967
+ el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind td-day__meta', 'data-day': 'meta' }, meta), el('h3', null, e.title)),
968
+ e.ended ? el('p', { class: 'td-day__ended', 'data-day': 'ended', role: 'status' }, pill(D.pill.ended, ''), ' ', A.endedAt(hhmm(e.end || e.f))) : null,
969
+ e.purpose ? el('p', { class: 'td-day__purpose', 'data-day': 'purpose' }, e.purpose) : null,
970
+ started ? notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'live', liveText: F.live }) : null,
971
+ e.hadPrep ? prepBlock(ctx, e, { fold: started }) : null,
910
972
  after,
911
- notesBlock(e, { ws: ctx.ws, initiative: ini }),
912
- // with prep, the Before steps are the checklist above; During, After and the mail fold away (PRD D2)
913
- // once it ends, the plan stays where it was; the mail moves up into the notes above, so it is not shown twice
914
- e.hadPrep && ((steps.during || []).length || (steps.after || []).length || (!e.ended && mailBlock))
915
- ? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.moreMeeting((steps.during || []).length, (steps.after || []).length)),
916
- list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), e.ended ? null : mailBlock) : null,
917
- !e.hadPrep ? [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), e.ended ? null : mailBlock] : null,
918
- Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null,
973
+ started ? null : notesBlock(e, { ws: ctx.ws, initiative: ini, mode: 'quiet' }),
974
+ el('div', { class: 'td-day__foot', 'data-day': 'foot' },
975
+ el('details', { class: 'td-day__fold td-day__details', 'data-day': 'details' }, el('summary', null, F.details), ...details),
976
+ more),
919
977
  ];
920
978
  }
921
979
 
@@ -1281,6 +1339,6 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
1281
1339
 
1282
1340
  export function daySig(ws) {
1283
1341
  const c = cache.get(ws) || {};
1284
- return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
1342
+ return JSON.stringify([forced, ui.open, ui.section, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], [...ui.cur], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
1285
1343
  + dataSig(c.data) + goalsSig() + agendaSig();
1286
1344
  }