create-pmos 0.69.2 → 0.70.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.69.2",
3
+ "version": "0.70.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@80fcbdd
1
+ kernel-source: pmos@9bbf3fc
2
2
  assembled: 2026-10-07
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-07T03:49:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
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-07T03:49:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
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-07T03:49:30Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: notes"
4
4
  description: "How to take notes in a meeting: listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository, correct and tag lines, see what changed in your notes, replay the meeting, link a note to its meeting and its initiative, and get a summary."
5
- timestamp: 2026-10-06
5
+ timestamp: 2026-10-07
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -95,7 +95,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
95
95
 
96
96
  When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
97
97
 
98
- To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. An event that already has a note shows its summary and **Open notes** instead. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
98
+ To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. The note opens inside the event's detail, in one column: type your notes, press **Listen**, and read the transcript there, without leaving the calendar. Press **Open in Notes** to see the same note on the full screen, and **Close** to fold it back to its summary. An event that already has a note shows its summary and **Open notes**, which opens the note in the detail. While one meeting is being listened to, another event's detail shows only its summary, so the meeting that is being heard is not stopped. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
99
99
 
100
100
  ## Dictate into your notes
101
101
 
@@ -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-06
5
+ timestamp: 2026-10-07
6
6
  ---
7
7
 
8
8
  # Today: what needs you
@@ -50,7 +50,7 @@ Each day of the month answers three questions:
50
50
 
51
51
  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).
52
52
 
53
- 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. When the meeting has a note, the detail shows its summary and **Open notes**, and the meeting's row shows **notes**. See [Notes](notes.md).
53
+ 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. The note runs in the detail: you type and listen there. **Open in Notes** shows it on the full screen. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the detail again, and the meeting's row shows **notes**. See [Notes](notes.md).
54
54
 
55
55
  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.
56
56
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.69.2",
3
+ "version": "0.70.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.69.2",
3
+ "version": "0.70.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",
@@ -6035,6 +6035,30 @@
6035
6035
  font-size: 12px;
6036
6036
  color: var(--muted-foreground);
6037
6037
  }
6038
+
6039
+ /* meeting-notes Run 5: the note runs inside a calendar event's detail, in one column */
6040
+ .nt-inhost__head {
6041
+ display: flex;
6042
+ align-items: center;
6043
+ justify-content: space-between;
6044
+ gap: 8px;
6045
+ }
6046
+ .nt-wrap--inline .nt-body2 {
6047
+ grid-template-columns: 1fr;
6048
+ }
6049
+ .nt-wrap--inline .nt-notecol {
6050
+ padding-right: 0;
6051
+ }
6052
+ .nt-wrap--inline .nt-trans {
6053
+ padding-left: 0;
6054
+ padding-top: 16px;
6055
+ border-left: 0;
6056
+ border-top: 1px solid var(--border);
6057
+ }
6058
+ .nt-wrap--inline .nt-lines {
6059
+ max-height: 320px;
6060
+ }
6061
+
6038
6062
  /* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
6039
6063
  /* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
6040
6064
  .lg-stack {
@@ -2,8 +2,9 @@
2
2
  *
3
3
  * notesFor(e) → the note rows linked to a calendar event (same id, or same start minute and title)
4
4
  * notesOfInitiative(id) → the note rows linked to an initiative, newest first
5
- * notesBlock(e, { ws, repo, initiative }) → the "Meeting notes" block of an event's detail: the summary and Open notes,
6
- * or Take notes; Take notes on an event that already has a note opens that note
5
+ * notesBlock(e, { ws, repo, initiative }) → the "Meeting notes" block of an event's detail. Take notes makes a note linked
6
+ * to the event and runs it right there (typing and listening, pm/view-notes.js renderInline);
7
+ * an event that has a note shows its summary and Open notes, which opens it there too
7
8
  * noteRows(rows, { onOpen }) → the rows of an initiative's Notes tab; onOpen runs before the note opens
8
9
  * openNote(id) → opens the Notes tab on that note
9
10
  * onNotes(fn) · refreshNotes() → redraw when the list is read
@@ -14,10 +15,14 @@
14
15
  import { el } from './dom.js';
15
16
  import { button } from './primitives.js';
16
17
  import { pmGet, pmPost } from './http.js';
18
+ import { state, setState, FIXTURE } from './state.js';
17
19
 
18
20
  const READ_MS = 15000; // the list is read again at most this often while a calendar or an initiative is drawn
19
21
  let rows = null; let readAt = 0; let reading = null; let failed = false;
20
22
  const subs = new Set();
23
+ let shown = null; // the note open in an event's detail (Take notes or Open notes); the listening note is shown too
24
+ let V = null; // pm/view-notes.js, read the first time a note is shown in a detail
25
+ const viewCtx = { state, setState, FIXTURE };
21
26
 
22
27
  export function onNotes(fn) { subs.add(fn); return () => subs.delete(fn); }
23
28
  export function refreshNotes() {
@@ -64,8 +69,14 @@ async function takeNotes(e, { ws, repo, initiative } = {}, btn) {
64
69
  const r = await pmPost('/api/notes/new', { meeting: meetingOf(e), ...(ini ? { initiative: ini } : {}) });
65
70
  if (btn) btn.disabled = false;
66
71
  if (!r || !r.ok) { const { toast } = await import('./primitives.js'); toast({ text: (r && r.error) || 'The note was not made.', tone: 'destructive' }); return; }
67
- refreshNotes();
68
- openNote(r.note.id);
72
+ shown = r.note.id;
73
+ await refreshNotes();
74
+ setState({});
75
+ }
76
+ function show(id) { shown = id; setState({}); }
77
+ function view() {
78
+ if (!V) import('./view-notes.js').then((m) => { V = m; setState({}); });
79
+ return V;
69
80
  }
70
81
 
71
82
  const dayOf = (iso) => { try { return new Date(iso).toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; } };
@@ -77,13 +88,24 @@ export function notesBlock(e, opts = {}) {
77
88
  const ended = !!e.ended;
78
89
  if (mine.length) {
79
90
  const n = mine[0];
91
+ const v = view();
92
+ const live = v && v.listeningId() === n.id;
93
+ if (v && (shown === n.id || live)) {
94
+ const host = el('div', { class: 'td-day__notes nt-inhost', 'data-notes-block': 'inline', 'data-notes-id': n.id });
95
+ const head = el('div', { class: 'nt-inhost__head' }, el('h4', null, 'Meeting notes'),
96
+ live ? null : button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { 'data-notes-close': n.id }, onClick: () => show(null) }));
97
+ host.appendChild(head);
98
+ if (v.renderInline(host, viewCtx, n.id)) return host;
99
+ }
80
100
  const lines = n.summary_lines || [];
101
+ const busy = v && v.listeningId() && v.listeningId() !== n.id;
81
102
  return el('div', { class: 'td-day__notes', 'data-notes-block': 'linked', 'data-notes-id': n.id },
82
103
  el('h4', null, 'Meeting notes'),
83
104
  n.private ? el('p', { class: 'td-day__note' }, 'This note is private.')
84
105
  : lines.length ? el('ul', { class: 'nt-lsum' }, ...lines.map((l) => el('li', null, l)))
85
106
  : el('p', { class: 'td-day__note' }, n.segments ? 'No summary yet. Open the note and press Summarize.' : 'No summary yet.'),
86
- el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openNote(n.id) })));
107
+ busy ? el('p', { class: 'td-day__note', 'data-notes-busy': '' }, 'Another meeting is being listened to. Stop it first to open this note here.') : null,
108
+ busy ? null : el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => show(n.id) })));
87
109
  }
88
110
  if (!notesKnown()) return null;
89
111
  const take = button({ label: 'Take notes', variant: ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
@@ -1,4 +1,4 @@
1
- /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 4).
1
+ /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 5).
2
2
  *
3
3
  * The screen follows the simplified mockup the PM reviewed (prototype/meeting-notes):
4
4
  * header the title and the date, and one action: Listen; while listening a red dot, the time, the sources and Stop;
@@ -26,7 +26,10 @@
26
26
  *
27
27
  * MEETING AND INITIATIVE (Run 4). Under the title, the note says which meeting it is (a calendar event, or not on the
28
28
  * calendar) and which initiative it belongs to, and how that was set: from the calendar event, suggested from what was said,
29
- * or chosen by the PM. Change opens a sheet for each. A note taken from an event's detail (pm/notes-links.js) opens here.
29
+ * or chosen by the PM. Change opens a sheet for each.
30
+ *
31
+ * IN THE EVENT'S DETAIL (Run 5). renderInline draws the same note inside a calendar event's detail (pm/notes-links.js), in
32
+ * one column: the PM types and listens there without leaving the calendar. Open in Notes shows it on the full screen.
30
33
  *
31
34
  * Reads and writes (lib/routes-notes.js). Notes live in the per-user runtime directory, never in a repository:
32
35
  * GET /api/notes · /api/notes/one?id=… · /api/notes/engine?ws=… · /api/notes/recording?id=… · /api/people?ws=…
@@ -64,6 +67,7 @@ let busy = '';
64
67
  let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
65
68
  let people = null; // { ws, names } — the repository's people, offered in the line editor
66
69
  let pendingOpen = null; // a note another page asked to open (openNote), selected at the next draw
70
+ let inlineSwitch = null; // the note an event's detail is switching to (renderInline), so a redraw does not start it twice
67
71
  const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
68
72
 
69
73
  const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
@@ -873,7 +877,7 @@ function listening(n) {
873
877
  if (speech && speech.id === n.id && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking')) return { kind: 'browser', state: speech.state, sources: ['mic'], started: speech.started };
874
878
  return null;
875
879
  }
876
- function header(ctx, n, ed) {
880
+ function header(ctx, n, ed, opts = {}) {
877
881
  const live = listening(n);
878
882
  const when = el('span', { class: 'nt-when' }, dayOf(n.created_at) + ', ' + nowClock(n.created_at));
879
883
  const nv = (n.versions || []).length;
@@ -890,11 +894,12 @@ function header(ctx, n, ed) {
890
894
  } else if ((n.transcript || []).length) {
891
895
  acts.push(button({ label: 'Copy as Markdown', variant: 'ghost', onClick: () => copyMarkdown(ctx, n), attrs: { 'data-nt-copy': '' } }));
892
896
  if (!n.private) acts.push(button({ label: 'Listen again', variant: 'ghost', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': 'again' } }));
893
- acts.push(button({ label: busy === 'new' ? 'Creating …' : 'New meeting', disabled: busy === 'new', onClick: () => newNote(ctx), attrs: { 'data-nt-new': '' } }));
897
+ if (!opts.inline) acts.push(button({ label: busy === 'new' ? 'Creating …' : 'New meeting', disabled: busy === 'new', onClick: () => newNote(ctx), attrs: { 'data-nt-new': '' } }));
894
898
  } else if (!n.private) {
895
899
  acts.push(button({ label: 'Listen', variant: 'primary', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': '' } }));
896
900
  }
897
- acts.push(button({ label: 'All notes', variant: 'ghost', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
901
+ if (opts.inline) acts.push(button({ label: 'Open in Notes', variant: 'ghost', onClick: () => openFull(ctx, n.id), attrs: { 'data-nt-full': '' } }));
902
+ else acts.push(button({ label: 'All notes', variant: 'ghost', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
898
903
  acts.push(iconButton({ icon: 'settings', ariaLabel: 'Settings', onClick: () => openSettings(ctx, n), attrs: { 'data-nt-settings': '' } }));
899
904
  return el('header', { class: 'nt-header', 'data-nt-part': 'header' }, el('div', { class: 'nt-headwrap' }, left, linksLine(ctx, n)), el('div', { class: 'nt-acts' }, acts));
900
905
  }
@@ -1009,8 +1014,7 @@ export function renderBoard(inner, ctx) {
1009
1014
  if (pendingOpen) { const id = pendingOpen; pendingOpen = null; if (id !== currentId) { selectNote(ctx, id); loadList(ctx); } }
1010
1015
  if (!engine || engine.ws !== wsOf(ctx)) { if (!engine || !engine.loading || engine.ws !== wsOf(ctx)) loadEngine(ctx); }
1011
1016
 
1012
- const focus = typeof document !== 'undefined' && editor && (document.activeElement === editor.body ? 'body' : document.activeElement === editor.title ? 'title' : null);
1013
- const caret = focus ? [editor[focus].selectionStart, editor[focus].selectionEnd] : null;
1017
+ const keep = keepFocus();
1014
1018
 
1015
1019
  const wrap = el('div', { class: 'nt-wrap', 'data-nt-part': 'view' });
1016
1020
  if (list && list.state === 'error') wrap.appendChild(el('div', { class: 'nt-banner nt-banner--bad', role: 'alert' }, list.error));
@@ -1037,7 +1041,53 @@ export function renderBoard(inner, ctx) {
1037
1041
  requestAnimationFrame(fitBody);
1038
1042
  }
1039
1043
  inner.appendChild(wrap);
1040
- // the redraw moves the fields, which blurs them; focus comes back in a microtask, after the frame is attached and before the
1041
- // next key event, so no keystroke lands while the field is detached
1042
- if (caret) queueMicrotask(() => { try { const t = editor[focus]; if (t.isConnected) { t.focus(); t.setSelectionRange(caret[0], caret[1]); } } catch { /* the field is gone */ } });
1044
+ keep();
1045
+ }
1046
+
1047
+ /** Which field has the focus, and a function that gives it back after the redraw. The redraw moves the fields, which blurs
1048
+ * them; focus comes back in a microtask, after the frame is attached and before the next key event, so no keystroke lands
1049
+ * while the field is detached. */
1050
+ function keepFocus() {
1051
+ const focus = typeof document !== 'undefined' && editor && (document.activeElement === editor.body ? 'body' : document.activeElement === editor.title ? 'title' : null);
1052
+ if (!focus) return () => {};
1053
+ const caret = [editor[focus].selectionStart, editor[focus].selectionEnd];
1054
+ return () => queueMicrotask(() => { try { const t = editor[focus]; if (t.isConnected) { t.focus(); t.setSelectionRange(caret[0], caret[1]); } } catch { /* the field is gone */ } });
1055
+ }
1056
+ async function openFull(ctx, id) {
1057
+ await flushSave(ctx);
1058
+ const c = await import('./panel-cockpit.js');
1059
+ pendingOpen = id;
1060
+ c.openView('notes');
1061
+ }
1062
+
1063
+ /** The note that is listening now, or null. */
1064
+ export const listeningId = () => (audio && audio.id) || (speech && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking') ? speech.id : null);
1065
+
1066
+ /** One note drawn inside a calendar event's detail (meeting-notes Run 5): the same note, editor and listening as the Notes
1067
+ * tab, in one column, with Open in Notes for the full screen. It never stops a meeting that is being heard: when another note
1068
+ * is listening it draws nothing and returns false. */
1069
+ export function renderInline(host, ctx, id) {
1070
+ if (ctx.FIXTURE) return false;
1071
+ const other = listeningId();
1072
+ if (other && other !== id) return false;
1073
+ ensureComponentsCss();
1074
+ if (!engine || engine.ws !== wsOf(ctx)) { if (!engine || !engine.loading || engine.ws !== wsOf(ctx)) loadEngine(ctx); }
1075
+ if (id !== currentId) { if (!inlineSwitch) { inlineSwitch = selectNote(ctx, id).finally(() => { inlineSwitch = null; }); } host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1076
+ const n = cache.get(id);
1077
+ if (!n) { loadNote(ctx, id); host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1078
+ const keep = keepFocus();
1079
+ const ed = editorFor(ctx, n);
1080
+ const wrap = el('div', { class: 'nt-wrap nt-wrap--inline', 'data-nt-part': 'inline', 'data-nt-id': n.id });
1081
+ if (banner) wrap.appendChild(el('div', { class: 'nt-banner' + (banner.tone === 'bad' ? ' nt-banner--bad' : ''), 'data-nt-banner': banner.tone, role: banner.tone === 'bad' ? 'alert' : 'status' }, banner.text));
1082
+ wrap.appendChild(header(ctx, n, ed, { inline: true }));
1083
+ if (n.private) wrap.appendChild(el('p', { class: 'nt-hint', 'data-nt-private': '' }, 'This note is private: it is not recorded, and nothing from it goes to Jev or to any agent.'));
1084
+ const pl = !listening(n) ? playerFor(ctx, n) : null;
1085
+ if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el));
1086
+ wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
1087
+ if (pl) queueMicrotask(paintNow);
1088
+ if (n.agent && n.agent.started && !n.agent.markdown && !agentTimer) pollAgent(ctx, n.id);
1089
+ requestAnimationFrame(fitBody);
1090
+ host.appendChild(wrap);
1091
+ keep();
1092
+ return true;
1043
1093
  }