create-pmos 0.106.0 → 0.107.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.106.0",
3
+ "version": "0.107.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@ed9ef84
1
+ kernel-source: pmos@5953acb
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-08T01:32:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
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-08T01:32:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
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-08T01:32:06Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T02:26:00Z"}
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: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, and Wispr Flow's notes beside them; 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 for conflicts, capture highlights during the meeting, create tasks from next steps, 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-07
5
+ timestamp: 2026-10-08
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -165,7 +165,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
165
165
 
166
166
  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.
167
167
 
168
- To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The note stays small at the bottom right of the workspace, with a red dot, its title and its time, on every page. Press it to open the note as a pop-up: add your notes, read the key points, and press **Stop** when the meeting ends. Press **Minimize** or Esc to make it small again. **Open in Notes** in the **⋯** menu shows it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays at the bottom right after a reload. When it no longer listens, press the x beside it to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
168
+ To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. PMOS starts to listen at once, to your microphone and to the meeting when local transcription is set up. Pick the meeting's tab or window when the browser asks. Let people know that you are taking notes. The note is linked to the event, and to the event's initiative when it has one. The note stays small at the bottom right of the workspace, with a red dot, its title and its time, on every page. Press it to open the note as a pop-up: add your notes, read the key points, and press **Stop** when the meeting ends. The pop-up stays steady while it listens: new lines are added at the end, and your place in the pop-up, the line you are typing and its cursor do not move. If the transcript is scrolled to its end, it follows the new lines. Press **Minimize** or Esc to make it small again. **Open in Notes** in the **⋯** menu shows it on the full screen. One note is taken at a time. If you press **Take notes** on another event while a note listens, PMOS asks first: **Keep it**, or **Stop it and start this one**. The note stays at the bottom right after a reload. When it no longer listens, press the x beside it to put it away. An event that already has a note shows its summary and **Open notes**, which opens the note in the pop-up. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
169
169
 
170
170
  ## Dictate into your notes
171
171
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.106.0",
3
+ "version": "0.107.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.106.0",
9
+ "version": "0.107.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.106.0",
3
+ "version": "0.107.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",
@@ -53,9 +53,8 @@ export function el(tag, attrs, ...children) {
53
53
  else if (k === 'ref') { if (typeof v === 'function') v(node); }
54
54
  else if (k === 'value' || k === 'checked' || k === 'disabled' || k === 'selected') node[k] = v;
55
55
  else if (k === 'html') node.innerHTML = String(v);
56
- else if (k.length > 2 && k[0] === 'o' && k[1] === 'n' && typeof v === 'function') {
57
- node.addEventListener(k.slice(2).toLowerCase(), v);
58
- } else if (v === true) node.setAttribute(k, '');
56
+ else if (k.length > 2 && k[0] === 'o' && k[1] === 'n' && typeof v === 'function') listen(node, k.slice(2).toLowerCase(), v);
57
+ else if (v === true) node.setAttribute(k, '');
59
58
  else node.setAttribute(k, String(v));
60
59
  }
61
60
  }
@@ -63,6 +62,14 @@ export function el(tag, attrs, ...children) {
63
62
  return node;
64
63
  }
65
64
 
65
+ /** An `onX` handler of el(). The listener reads the handler from the node, so patch() can give a node it keeps the handler of
66
+ * the node drawn in its place: a kept button then runs what the newest draw says, not what the first draw said. */
67
+ function listen(node, type, fn) {
68
+ const h = node.__pmOn || (node.__pmOn = Object.create(null));
69
+ if (!(type in h)) node.addEventListener(type, function (e) { const f = node.__pmOn[type]; return f ? f.call(this, e) : undefined; });
70
+ h[type] = fn;
71
+ }
72
+
66
73
  /** {backgroundColor:'red', minHeight:'30px'} -> 'background-color:red;min-height:30px'. */
67
74
  export function style(obj) {
68
75
  if (typeof obj === 'string') return obj;
@@ -174,3 +181,63 @@ export function keepScroll(scroller) {
174
181
  scroller.removeEventListener('scroll', onScroll);
175
182
  };
176
183
  }
184
+
185
+ /**
186
+ * Patch `live`'s children to look like `fresh`'s, keeping every node that is still there (note pop-up flicker, 2026-10-08).
187
+ * A view that draws itself again on every state change can draw into a detached `fresh` and patch the screen from it: what
188
+ * did not change stays the same element, so its scroll, focus, caret, open state and CSS animation stay; what changed is
189
+ * updated in place; what is new is inserted. Children are matched by tag and by their data-* attributes (so a list keyed
190
+ * by `data-x="3"` keeps its rows), else by position. A kept node takes the attributes, the el() handlers and the form
191
+ * values of the node drawn in its place. A `kept(node)` placeholder in `fresh` stands for a node the view owns and keeps
192
+ * itself (an editor field): that node is left where it is, or moved there, and never patched.
193
+ */
194
+ export function patchChildren(live, fresh) {
195
+ let cur = live.firstChild;
196
+ for (const n of [...fresh.childNodes]) {
197
+ if (n.__pmKeep) {
198
+ const t = n.__pmKeep;
199
+ if (cur === t) cur = cur.nextSibling;
200
+ else live.insertBefore(t, cur);
201
+ continue;
202
+ }
203
+ if (cur && sameNode(cur, n)) { patchNode(cur, n); cur = cur.nextSibling; continue; }
204
+ let found = null;
205
+ if (n.nodeType === 1 && keyOf(n)) for (let c = cur; c; c = c.nextSibling) if (sameNode(c, n)) { found = c; break; }
206
+ if (found) { live.insertBefore(found, cur); patchNode(found, n); continue; }
207
+ live.insertBefore(n, cur);
208
+ }
209
+ while (cur) { const x = cur.nextSibling; live.removeChild(cur); cur = x; }
210
+ return live;
211
+ }
212
+ /** A placeholder for patchChildren(): `node` is the view's own, and stays as it is. Off a page, the node itself. */
213
+ export function kept(node) {
214
+ if (!node || !node.isConnected) return node;
215
+ const p = document.createElement('pm-kept');
216
+ p.__pmKeep = node;
217
+ node.__pmOwned = true; // never matched to, and patched into, another node drawn beside it
218
+ return p;
219
+ }
220
+ function keyOf(n) {
221
+ let k = '';
222
+ for (const a of n.attributes) if (a.name.startsWith('data-')) k += a.name + '=' + a.value + ';';
223
+ return k;
224
+ }
225
+ function sameNode(a, b) {
226
+ if (a.nodeType !== b.nodeType) return false;
227
+ if (a.nodeType !== 1) return true;
228
+ return a.tagName === b.tagName && !a.__pmOwned && keyOf(a) === keyOf(b);
229
+ }
230
+ function patchNode(a, b) {
231
+ if (a.nodeType !== 1) { if (a.nodeValue !== b.nodeValue) a.nodeValue = b.nodeValue; return; }
232
+ for (const x of [...a.attributes]) if (!b.hasAttribute(x.name)) a.removeAttribute(x.name);
233
+ for (const x of b.attributes) if (a.getAttribute(x.name) !== x.value) a.setAttribute(x.name, x.value);
234
+ const ha = a.__pmOn; const hb = b.__pmOn;
235
+ if (ha) for (const t in ha) if (!hb || !hb[t]) ha[t] = null;
236
+ if (hb) for (const t in hb) listen(a, t, hb[t]);
237
+ const tag = a.tagName;
238
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
239
+ if (a.value !== b.value && a !== document.activeElement) a.value = b.value;
240
+ if (a.checked !== b.checked) a.checked = b.checked;
241
+ }
242
+ patchChildren(a, b);
243
+ }
@@ -13,7 +13,7 @@
13
13
  * openDock(id) → shows a note in the pop-up
14
14
  * minimize() · activeId() · forget(id)
15
15
  */
16
- import { el } from './dom.js';
16
+ import { el, patchChildren } from './dom.js';
17
17
  import { button, iconButton, layer, toast, ensureComponentsCss } from './primitives.js';
18
18
  import { askConfirm } from './dialogs.js';
19
19
  import { pmPost } from './http.js';
@@ -123,6 +123,10 @@ function draw() {
123
123
  const live = !!active && !!v && liveId() === active.id;
124
124
  clearInterval(tick); tick = null;
125
125
  if (!active || !v) { host.replaceChildren(); return; }
126
+ // The dock draws on every state change, and a transcript line comes in every few seconds while it listens. It draws into
127
+ // a detached node and patches the screen from it, so only what changed is touched: the lines already shown, the field
128
+ // being typed in, the scroll and the red dot's pulse stay as they are, and a new line is appended (2026-10-08 flicker).
129
+ const fresh = el('div');
126
130
  if (live) tick = setInterval(() => { const c = host.querySelector('[data-nt-dock-clock]'); if (c) c.textContent = clockText(); }, 1000);
127
131
  if (!open) {
128
132
  const pill = button({ label: el('span', { class: 'nt-pill__txt' },
@@ -131,14 +135,20 @@ function draw() {
131
135
  live ? el('span', { class: 'nt-pill__clock', 'data-nt-dock-clock': '' }, clockText()) : null),
132
136
  variant: 'outline', attrs: { class: 'nt-pill', 'data-nt-pill': live ? 'live' : 'idle', 'aria-label': (live ? 'Notes are being taken: ' : 'Meeting note: ') + titleOf() + '. Open the note.' },
133
137
  onClick: () => openDock(active.id) });
134
- host.replaceChildren(el('div', { class: 'nt-pillrow' }, pill,
138
+ fresh.appendChild(el('div', { class: 'nt-pillrow' }, pill,
135
139
  live ? null : iconButton({ icon: 'x', ariaLabel: 'Put the note away', onClick: close, attrs: { 'data-nt-dock-close': '' } })));
140
+ patchChildren(host, fresh);
136
141
  return;
137
142
  }
138
143
  // the note draws its own bar (status, Stop, Minimize and the ⋯ menu): the pop-up adds no second head (Run 7)
139
144
  const body = el('div', { class: 'nt-pop__body' });
140
145
  const pop = el('section', { class: 'nt-pop', role: 'dialog', 'aria-label': 'Meeting note', 'data-nt-pop': active.id }, body);
141
- host.replaceChildren(pop);
146
+ fresh.appendChild(pop);
142
147
  if (!v.renderInline(body, viewCtx, active.id, { onMinimize: minimize })) body.appendChild(el('p', { class: 'nt-hint' }, 'Another note is being taken.'),
143
148
  button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: minimize, attrs: { 'data-nt-minimize': '' } }));
149
+ // a transcript read to its end follows the new lines; one the PM scrolled up in stays where they left it
150
+ const box = host.querySelector('[data-nt-pop] [data-nt-transcript]');
151
+ const atEnd = !!box && box.scrollTop + box.clientHeight >= box.scrollHeight - 8;
152
+ patchChildren(host, fresh);
153
+ if (atEnd && box.isConnected) box.scrollTop = box.scrollHeight;
144
154
  }
@@ -39,7 +39,7 @@
39
39
  * POST /api/notes/new · save · dictate · record · transcript · audio · label · speaker · switch · whisper-setup · private
40
40
  * · summarize · summarize-agent · delete · line · recording-delete · link
41
41
  */
42
- import { el } from './dom.js';
42
+ import { el, kept } from './dom.js';
43
43
  import { askConfirm, askText } from './dialogs.js';
44
44
  import { pmGet, pmPost } from './http.js';
45
45
  import { mdRender } from './markdown.js';
@@ -66,6 +66,10 @@ let speech = null; // the browser recogniser: { mode: 'dictate'|'reco
66
66
  let audio = null; // local Whisper capture: { id, state, sources, ctx, streams, started, clock, msg }
67
67
  let engine = null; // { ws, whisper, jev, at }
68
68
  let showList = false;
69
+ let inlineDraw = false; // the pop-up is drawing: the fields and the player it shows are left in place (pm/note-dock.js)
70
+ /** A node this view keeps across redraws (an editor field, the player). The pop-up patches itself (dom.js patchChildren), so
71
+ * there the node is a placeholder and stays where it is: a field that is typed in is never moved, and keeps its focus. */
72
+ const own = (node) => (inlineDraw ? kept(node) : node);
69
73
  let pollTimer = null; let agentTimer = null;
70
74
  let busy = '';
71
75
  let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
@@ -1204,11 +1208,11 @@ function header(ctx, n, ed, opts = {}) {
1204
1208
  }
1205
1209
  if (opts.onMinimize) acts.push(button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: opts.onMinimize, attrs: { 'data-nt-minimize': '' } }));
1206
1210
  if (!opts.inline) acts.push(button({ label: 'All notes', variant: 'ghost', size: 'sm', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
1207
- const more = iconButton({ icon: 'ellipsis', ariaLabel: 'More', onClick: () => moreMenu(ctx, n, opts, more), attrs: { 'data-nt-more': '' } });
1211
+ const more = iconButton({ icon: 'ellipsis', ariaLabel: 'More', onClick: (e) => moreMenu(ctx, n, opts, (e && e.currentTarget) || more), attrs: { 'data-nt-more': '' } });
1208
1212
  acts.push(more);
1209
1213
  return el('header', { class: 'nt-header', 'data-nt-part': 'header' },
1210
1214
  el('div', { class: 'nt-bar' }, status, el('div', { class: 'nt-acts' }, acts)),
1211
- el('div', { class: 'nt-headl' }, ed.titleWrap, whenLine(ctx, n), peopleRow(n)));
1215
+ el('div', { class: 'nt-headl' }, own(ed.titleWrap), whenLine(ctx, n), peopleRow(n)));
1212
1216
  }
1213
1217
  function listPane(ctx) {
1214
1218
  const rows = (list && list.notes) || [];
@@ -1421,7 +1425,7 @@ function myNotes(ctx, n, ed) {
1421
1425
  const t = liveMs(n);
1422
1426
  const parts = [el('h3', { class: 'nt-sec__h' }, 'Your notes')];
1423
1427
  if (items.length) parts.push(el('ul', { class: 'nt-items', 'data-nt-notelines': '' }, items));
1424
- if (!replaying) parts.push(el('div', { class: 'nt-addrow' }, el('span', { class: 'nt-at', 'data-nt-clock-now': '' }, t !== null ? fmtTime(t) : ''), ed.addWrap));
1428
+ if (!replaying) parts.push(el('div', { class: 'nt-addrow' }, el('span', { class: 'nt-at', 'data-nt-clock-now': '' }, t !== null ? fmtTime(t) : ''), own(ed.addWrap)));
1425
1429
  else parts.push(el('p', { class: 'nt-hint' }, 'Your notes, as the meeting plays. Pause to add more.'));
1426
1430
  const sp = speech && speech.id === n.id ? speech : null;
1427
1431
  const bar = [];
@@ -1618,16 +1622,20 @@ export function renderInline(host, ctx, id, opts = {}) {
1618
1622
  if (!n) { loadNote(ctx, id); host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1619
1623
  const keep = keepFocus();
1620
1624
  const ed = editorFor(ctx, n);
1625
+ inlineDraw = true;
1626
+ try { drawInline(ctx, n, ed, host, opts); } finally { inlineDraw = false; }
1627
+ keep();
1628
+ return true;
1629
+ }
1630
+ function drawInline(ctx, n, ed, host, opts) {
1621
1631
  const wrap = el('div', { class: 'nt-wrap nt-wrap--inline', 'data-nt-part': 'inline', 'data-nt-id': n.id });
1622
1632
  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));
1623
1633
  wrap.appendChild(header(ctx, n, ed, { inline: true, onMinimize: opts.onMinimize }));
1624
1634
  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.'));
1625
1635
  const pl = !listening(n) ? playerFor(ctx, n) : null;
1626
- if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el));
1636
+ if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), own(pl.el)));
1627
1637
  wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n, { inline: true })));
1628
1638
  if (pl) queueMicrotask(paintNow);
1629
1639
  if (n.agent && n.agent.started && !n.agent.markdown && !agentTimer) pollAgent(ctx, n.id);
1630
1640
  host.appendChild(wrap);
1631
- keep();
1632
- return true;
1633
1641
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js).",
2
+ "_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), plus the note pop-up patched in place while it listens (pm-note-dock-test.js).",
3
3
  "modules": [
4
4
  "notes.test.js",
5
5
  "notes-listen.test.js",
@@ -7,13 +7,15 @@
7
7
  "notes-qwen.test.js"
8
8
  ],
9
9
  "panels": [
10
- "pm-notes-test.js"
10
+ "pm-notes-test.js",
11
+ "pm-note-dock-test.js"
11
12
  ],
12
13
  "seconds": {
13
14
  "notes.test.js": 12,
14
15
  "pm-notes-test.js": 40,
15
16
  "notes-listen.test.js": 10,
16
17
  "notes-final.test.js": 6,
17
- "notes-qwen.test.js": 4
18
+ "notes-qwen.test.js": 4,
19
+ "pm-note-dock-test.js": 20
18
20
  }
19
21
  }