create-pmos 0.122.0 → 0.124.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.122.0",
3
+ "version": "0.124.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@7af87c4
1
+ kernel-source: pmos@eee5377
2
2
  assembled: 2026-10-09
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-09T01:31:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-09T03:19:19Z"}
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-09T01:31:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-09T03:19:19Z"}
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-09T01:31:23Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-09T03:19:19Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -62,8 +62,9 @@ The Notes view shows one meeting, top to bottom. A bar at the top shows whether
62
62
 
63
63
  1. Press **Listen**. Select what to listen to:
64
64
  - **My microphone**: what you say.
65
- - **The meeting**: the other people in an online meeting. Select the meeting's tab or window next, and tick **Share tab audio**.
66
- 2. Press **Start**. The header shows a red dot, the time, and the sources. Tell people that you are taking notes. If the meeting stays completely silent for 20 seconds while your microphone works, a red message says that the other people are not being recorded. Most often the sharing window had the wrong tab. Press **Stop**, press **Listen** again, choose the meeting's tab, and tick **Share tab audio**. The message goes away when sound arrives or when you press **Stop**.
65
+ - **The meeting**: the other people in an online meeting. The meeting can be in any app, such as Zoom, or in any browser tab. In the sharing window, choose **Entire screen** and tick **Also share system audio**, so that the sound of every app is recorded. To record one browser tab, choose that tab and tick **Share tab audio**. Chrome offers system audio on Windows and ChromeOS; on macOS use a tab.
66
+ - **Skipping the sharing window.** A browser always asks before a page records the screen, and a page cannot turn that off. To pick **Entire screen** without being asked, start Chrome with the switch `--auto-select-desktop-capture-source="Entire screen"` (a Chrome on your own machine, for example from a shortcut). System audio is then shared when the browser and the system offer it (see above). This is a Chrome setting on your machine, not a PMOS setting.
67
+ 2. Press **Start**. The header shows a red dot, the time, and the sources. Tell people that you are taking notes. If the meeting stays completely silent for 20 seconds while your microphone works, a red message says that the other people are not being recorded. Most often the sharing window had the wrong source. Press **Stop**, press **Listen** again, choose **Entire screen** (or the meeting's tab), and tick **Also share system audio** (or **Share tab audio**). The message goes away when sound arrives or when you press **Stop**.
67
68
  3. Each line shows in the transcript a few seconds after it is said, with who said it, the time and its label. Your lines are **You**. The meeting's lines are **Meeting**: click the name to change it. The transcript cannot tell the people in the meeting apart.
68
69
  4. Press **Stop** to end. The summary shows at the top of your notes. To carry on, press **Listen again**: the clock carries on from where it stopped, and the time between does not count. Line times and the kept audio use the same clock.
69
70
  5. If the workspace does not take a part of the audio, the page sends it again two times. If it still fails, a red message tells you how many seconds were not saved. Listening goes on. That part of the recording has a gap.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.122.0",
3
+ "version": "0.124.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.122.0",
9
+ "version": "0.124.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.122.0",
3
+ "version": "0.124.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",
@@ -207,7 +207,7 @@ async function refresh(inst) {
207
207
  if (seq !== inst.seq) return;
208
208
  if (r && r.ok) { inst.rows = r.notes || []; inst.error = null; } else { inst.rows = inst.rows || []; inst.error = (r && r.error) || COPY.readError; }
209
209
  if (!inst.open && inst.rows.length) inst.open = inst.rows[0].id;
210
- if (one && one.ok && one.note && one.note.id === inst.open) inst.note = one.note;
210
+ if (one && one.ok && one.note && one.note.id === inst.open) { inst.note = one.note; inst.rec = one.recording || null; }
211
211
  else if (inst.open && !one) loadOpen(inst);
212
212
  drawLeft(inst);
213
213
  drawRight(inst);
@@ -217,8 +217,25 @@ async function loadOpen(inst) {
217
217
  const r = await pmGet('/api/notes/one?id=' + encodeURIComponent(id));
218
218
  if (inst.open !== id) return;
219
219
  inst.note = r && r.ok ? r.note : null;
220
+ inst.rec = r && r.ok ? r.recording || null : null;
220
221
  drawRight(inst);
221
222
  }
223
+ /** The replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on. Null when no audio is kept. */
224
+ function replayFor(inst, n) {
225
+ const rec = inst.rec;
226
+ if (!rec || n.private) { inst.audio = null; return null; }
227
+ const key = n.id + ':' + rec.bytes + ':' + (rec.cleaned ? 1 : 0);
228
+ if (inst.audio && inst.audio.key === key) return inst.audio.el;
229
+ if (inst.audio && !inst.audio.el.paused) return inst.audio.el; // never cut a replay that is playing
230
+ const a = el('audio', { controls: true, preload: 'metadata', class: 'nt-audio', 'data-ntm-audio': '', src: '/api/notes/recording?id=' + encodeURIComponent(n.id) + '&b=' + rec.bytes + (rec.cleaned ? '&c=1' : '') });
231
+ inst.audio = { key, el: a };
232
+ return a;
233
+ }
234
+ function seekReplay(inst, ms) {
235
+ const a = inst.audio && inst.audio.el;
236
+ if (!a) return;
237
+ try { a.currentTime = Math.max(0, ms / 1000); a.play(); } catch { /* the audio is not ready */ }
238
+ }
222
239
  function choose(inst, id) {
223
240
  if (inst.open === id) return;
224
241
  inst.open = id; inst.note = null; inst.ctxOpen = null; inst.info = false;
@@ -259,7 +276,7 @@ function drawRight(inst, force) {
259
276
  const n = inst.note;
260
277
  const live = dock.recordState();
261
278
  const held = !!n && live.mode !== 'idle' && live.id === n.id;
262
- const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
279
+ const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
263
280
  // a held note draws again on every render: the clock, the lines and the paused minutes move. Nothing else draws without a change.
264
281
  if (!force && !held && sig === inst.rightSig) return;
265
282
  // a field being typed in is not redrawn under the cursor, except in the live block, whose fields are kept across redraws
@@ -387,6 +404,11 @@ function noteParts(inst, n, live) {
387
404
  if (V.renderLive(liveHost, { state: inst.ctx.state, setState: inst.ctx.setState, FIXTURE: inst.ctx.FIXTURE }, n.id)) out.push(liveHost);
388
405
  }
389
406
 
407
+ const replay = held ? null : replayFor(inst, n);
408
+ if (replay) out.push(el('section', { class: 'nt-pg-sec nt-pg-sec--first nt-pg-replay', 'data-ntm': 'replay' },
409
+ el('h3', { class: 'nt-pg-sec__h' }, el('span', null, 'Replay')), replay,
410
+ el('p', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the discussion to play from there.')));
411
+
390
412
  const cf = conflictsOf(n);
391
413
  if (cf.length) {
392
414
  out.push(el('section', { class: 'nt-pg-conflicts', 'data-ntm': 'conflicts' },
@@ -438,7 +460,7 @@ function noteParts(inst, n, live) {
438
460
  out.push(el('details', { class: 'nt-pg-sec nt-pg-fold', 'data-ntm': 'discussion' },
439
461
  el('summary', { class: 'nt-pg-fold__sum' }, COPY.discussion + ' · ' + bits),
440
462
  p.topics.map((tp) => el('div', { class: 'nt-pg-topic' }, el('h4', { class: 'nt-pg-topic__h' }, tp.title), (tp.points || []).map((pt) => el('p', { class: 'nt-pg-topic__p' }, pt)))),
441
- tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x) => el('div', { class: 'nt-pg-li' }, el('span', { class: 'nt-pg-li__at' }, V.fmtTime(x.t || 0)), el('span', { class: 'nt-pg-li__text' }, x.text)))) : null));
463
+ tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x) => el('div', { class: 'nt-pg-li' }, inst.audio && !held ? button({ label: V.fmtTime(x.t || 0), variant: 'ghost', size: 'sm', onClick: () => seekReplay(inst, x.t || 0), attrs: { 'data-ntm-seek': '', title: 'Play from here' } }) : el('span', { class: 'nt-pg-li__at' }, V.fmtTime(x.t || 0)), el('span', { class: 'nt-pg-li__text' }, x.text)))) : null));
442
464
  }
443
465
  return out.filter(Boolean);
444
466
  }
@@ -624,11 +624,12 @@ async function cleanNow(ctx, n) {
624
624
  async function openSource(kind) {
625
625
  const md = navigator.mediaDevices;
626
626
  if (kind === 'mic') return md.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
627
- // preferCurrentTab + selfBrowserSurface defaults to the current tab with tab audio, reducing friction for the common case
628
- const s = await md.getDisplayMedia({ video: true, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', preferCurrentTab: true, selfBrowserSurface: 'include' });
627
+ // The meeting is often in Zoom or another app, not in this tab: offer the whole screen first (its system audio carries every app),
628
+ // keep every window and tab selectable, and leave PMOS's own tab out, because it has no sound. Chrome ignores a hint it does not know.
629
+ const s = await md.getDisplayMedia({ video: { displaySurface: 'monitor' }, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', monitorTypeSurfaces: 'include', surfaceSwitching: 'include', selfBrowserSurface: 'exclude' });
629
630
  const tracks = s.getAudioTracks();
630
631
  for (const t of s.getVideoTracks()) t.stop(); // only the sound is used
631
- if (!tracks.length) throw Object.assign(new Error('The shared tab or window has no audio. Share it again and tick "Share tab audio".'), { noAudio: true });
632
+ if (!tracks.length) throw Object.assign(new Error('The shared screen, window or tab has no audio. Share it again, and tick "Also share system audio" (entire screen) or "Share tab audio" (a tab).'), { noAudio: true });
632
633
  return new MediaStream(tracks);
633
634
  }
634
635
  /** The meeting source reaches PMOS as exact silence while the microphone is heard: the other people are not being recorded. Most often the
@@ -642,7 +643,7 @@ function checkSilence(ctx, a) {
642
643
  const limit = (typeof window !== 'undefined' && window.__ntSilenceMs) || SILENCE_MS; // the suites shorten it
643
644
  if (Date.now() - a.began < limit || (a.peak.mic || 0) < HEARD_PEAK || banner) return;
644
645
  banner = { tone: 'bad', silent: true, audioMsg: true, text: 'The meeting\'s audio has been silent for ' + Math.round((Date.now() - a.began) / 1000) + ' seconds, while your microphone is working, so the other people are not being recorded. '
645
- + 'If the meeting is talking, press Stop, press Listen again, choose the meeting\'s tab in the sharing window and tick "Share tab audio".' };
646
+ + 'If the meeting is talking, press Stop, press Listen again, choose Entire screen in the sharing window (or the meeting\'s tab) and tick "Share tab audio" or "Also share system audio".' };
646
647
  ctx.setState({});
647
648
  }
648
649
  async function startAudio(ctx, n, want) {