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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/notes.md +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/view-notes.js +25 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T09:05:35Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -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.
|
|
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.
|
|
9
|
+
"version": "0.120.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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 */ }
|