create-pmos 0.118.0 → 0.120.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.118.0",
3
+ "version": "0.120.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@5737e5d
1
+ kernel-source: pmos@aed5805
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-08T07:37:35Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T09:05:35Z"}
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-08T07:37:35Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T09:05:35Z"}
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-08T07:37:35Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T09:05:35Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -63,7 +63,7 @@ The Notes view shows one meeting, top to bottom. A bar at the top shows whether
63
63
  1. Press **Listen**. Select what to listen to:
64
64
  - **My microphone**: what you say.
65
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.
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**.
67
67
  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
68
  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
69
  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.118.0",
3
+ "version": "0.120.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.118.0",
9
+ "version": "0.120.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.118.0",
3
+ "version": "0.120.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",
@@ -55,6 +55,9 @@ const AGENT_POLL_MS = 3000;
55
55
  const CHUNK_SAMPLES = 16000; // one second at 16 kHz
56
56
  const RATE = 16000;
57
57
  const HI_RATE = 24000; // the replay is kept at this rate when the device has it (D92 as amended 2026-10-08)
58
+ const SILENCE_MS = 20000; // a meeting source that stays at exactly zero this long, while the microphone works, is reported
59
+ const SILENCE_PEAK = 1e-4; // the loudest sample of a source below this counts as silence
60
+ const HEARD_PEAK = 0.005; // the microphone has been heard once it reaches this
58
61
  const SEND_TRIES = 3; // a chunk that the server did not take is sent again this many times in all …
59
62
  const SEND_BACKOFF_MS = 400; // … after 400 ms, then 1200 ms
60
63
  const SECTIONS = [['decision', 'Decisions'], ['action', 'Next steps'], ['question', 'Open questions']];
@@ -628,8 +631,22 @@ async function openSource(kind) {
628
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 });
629
632
  return new MediaStream(tracks);
630
633
  }
634
+ /** The meeting source reaches PMOS as exact silence while the microphone is heard: the other people are not being recorded. Most often the
635
+ * sharing window had the wrong tab (PMOS's own tab has no sound). Say so once, and take the message back if sound arrives. */
636
+ function checkSilence(ctx, a) {
637
+ if (audio !== a || !a.sources.includes('meeting')) return;
638
+ if ((a.peak.meeting || 0) > SILENCE_PEAK) {
639
+ if (banner && banner.silent) { banner = null; ctx.setState({}); }
640
+ return;
641
+ }
642
+ const limit = (typeof window !== 'undefined' && window.__ntSilenceMs) || SILENCE_MS; // the suites shorten it
643
+ if (Date.now() - a.began < limit || (a.peak.mic || 0) < HEARD_PEAK || banner) return;
644
+ 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
+ ctx.setState({});
647
+ }
631
648
  async function startAudio(ctx, n, want) {
632
- const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, lost: 0, ended: 0 };
649
+ const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0 };
633
650
  const opened = [];
634
651
  // the meeting first, straight from the press: the browser lets a page share a screen only right after a click
635
652
  for (const kind of want.slice().sort((x, y) => (x === 'meeting' ? -1 : y === 'meeting' ? 1 : 0))) {
@@ -637,7 +654,7 @@ async function startAudio(ctx, n, want) {
637
654
  catch (e) { a.msg = (a.msg ? a.msg + ' ' : '') + (kind === 'mic' ? 'The microphone was not opened' : 'The meeting audio was not opened') + ': ' + ((e && e.noAudio) ? e.message : ((e && e.message) || 'permission was refused') + '.'); }
638
655
  }
639
656
  await stopAll(ctx); await flushSave(ctx);
640
- audio = a; ctx.setState({});
657
+ audio = a; if (banner && banner.audioMsg) banner = null; ctx.setState({});
641
658
  if (!opened.length) { audio = null; banner = { tone: 'bad', text: a.msg || 'Nothing to listen to.' }; ctx.setState({}); return; }
642
659
  const r = await pmPost('/api/notes/record', wsOf(ctx) ? { id: n.id, ws: wsOf(ctx) } : { id: n.id });
643
660
  if (!r || !r.ok) { for (const [, s] of opened) for (const t of s.getTracks()) t.stop(); audio = null; banner = { tone: 'bad', text: (r && r.error) || 'listening did not start' }; ctx.setState({}); return; }
@@ -655,9 +672,11 @@ async function startAudio(ctx, n, want) {
655
672
  const rs = a.rs[kind] = makeResampler(ac.sampleRate);
656
673
  // a device slower than 24 kHz has nothing above 12 kHz to keep, so it sends 16 kHz audio only and the replay stays 16 kHz
657
674
  const rs24 = ac.sampleRate >= HI_RATE ? (a.rs24[kind] = makeResampler(ac.sampleRate, HI_RATE)) : null;
658
- a.buf24[kind] = [];
675
+ a.buf24[kind] = []; a.peak[kind] = 0;
659
676
  await openCapture(ac, src, sink, (block) => {
660
677
  if (audio !== a) return;
678
+ let pk = a.peak[kind]; for (let i = 0; i < block.length; i++) { const v = block[i] < 0 ? -block[i] : block[i]; if (v > pk) pk = v; }
679
+ a.peak[kind] = pk;
661
680
  const s = rs.push(block);
662
681
  const b = a.buf[kind]; for (let i = 0; i < s.length; i++) b.push(s[i]);
663
682
  if (rs24) { const h = rs24.push(block); const b24 = a.buf24[kind]; for (let i = 0; i < h.length; i++) b24.push(h[i]); }
@@ -666,7 +685,7 @@ async function startAudio(ctx, n, want) {
666
685
  for (const t of stream.getTracks()) t.addEventListener('ended', () => { if (audio !== a) return; a.ended++; if (a.ended >= a.streams.length) stopAudio(ctx); });
667
686
  }
668
687
  a.state = 'on';
669
- a.clock = setInterval(() => tickClocks(a.started), 1000);
688
+ a.clock = setInterval(() => { tickClocks(a.started); checkSilence(ctx, a); }, 1000);
670
689
  ctx.setState({});
671
690
  poll(ctx, n.id);
672
691
  import('./notes-links.js').then((m) => m.refreshNotes()).catch(() => {});
@@ -690,7 +709,7 @@ function sendChunk(ctx, a, kind, final) {
690
709
  return;
691
710
  }
692
711
  a.lost += seconds; // not silent: the recording has a gap, and the PM is told how long
693
- banner = { tone: 'bad', text: 'About ' + Math.max(1, Math.round(a.lost)) + ' s of the recording was not saved, because the workspace server did not take it'
712
+ banner = { tone: 'bad', audioMsg: true, text: 'About ' + Math.max(1, Math.round(a.lost)) + ' s of the recording was not saved, because the workspace server did not take it'
694
713
  + (r && r.error ? ' (' + r.error + ')' : '') + '.' + (audio === a ? ' Listening goes on.' : '') };
695
714
  ctx.setState({});
696
715
  }).catch(() => {});
@@ -700,6 +719,7 @@ async function stopAudio(ctx, { discard = false, resumable = true } = {}) {
700
719
  const a = audio;
701
720
  if (!a) return;
702
721
  audio = null;
722
+ if (banner && banner.silent) banner = null;
703
723
  clearInterval(a.clock);
704
724
  for (const s of a.streams) for (const t of s.getTracks()) { try { t.stop(); } catch { /* stopped */ } }
705
725
  try { if (a.ctx) await a.ctx.close(); } catch { /* closed */ }