create-pmos 0.149.0 → 0.150.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 +2 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/note-dock.js +2 -2
- package/workspace/public/pm/notes-links.js +23 -6
- package/workspace/public/pm/shell-recording.js +7 -7
- package/workspace/public/pm/topbar.js +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.150.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-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
|
|
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-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
|
|
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-12T03:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -16,13 +16,13 @@ Every button in the top bar and in the note is an icon. Hold the pointer on an i
|
|
|
16
16
|
The Record control is at the right of the top bar. It has two groups, always in the same place.
|
|
17
17
|
|
|
18
18
|
- **The meeting group** shows only while a meeting is recording or paused. It starts with the meeting's name and its clock (paused: **paused · 48 min left**). Press the name to open the note. While recording, the group has a highlighter icon, **Pause** and **Stop**. While paused, it has **Resume** and **Stop**. When there is nothing to resume, the group is not there.
|
|
19
|
-
- **The new group** is the red microphone with a plus, and an arrow. The microphone starts an untitled note and listens at once. The arrow lists the meetings of today that have not
|
|
19
|
+
- **The new group** is the red microphone with a plus, and an arrow. The microphone starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended and have no note, soonest first. A meeting that has started stays in the list until 10 minutes after it ends, so you can still record it. It shows "Now · started 5 min ago", or "Ended 3 min ago" in those last 10 minutes. **Next** marks the first meeting that has not started. Each shows who is coming and how many agenda items it has. Press one to start a note for that meeting, with its context. Both are unavailable while a meeting is recording. Pause or stop it first.
|
|
20
20
|
|
|
21
21
|
Starting a note, from the Record control or from **Take notes** on a calendar event, shows the Notes page with that note.
|
|
22
22
|
|
|
23
23
|
### The Record control in the console
|
|
24
24
|
|
|
25
|
-
The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not started. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
|
|
25
|
+
The console (the page at `/c3.html`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended, including one that has started, until 10 minutes after its end. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
|
|
26
26
|
|
|
27
27
|
When Whisper or Qwen3-ASR transcribes the meeting, the red group also names the engine and the language, for example **Whisper · English**. Check it when lines come out in the wrong language: the language is set in the Notes settings.
|
|
28
28
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.150.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.150.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
*
|
|
3
3
|
* The Record control lives in the top bar (pm/topbar.js). It has two groups. The left one exists while a meeting is recording
|
|
4
4
|
* or paused: a chip that names the meeting, then Highlight, Pause and Stop (recording) or Resume and Stop (paused). The right
|
|
5
|
-
* one is always there: New, which starts an untitled note, and an arrow that lists today's meetings that have not
|
|
5
|
+
* one is always there: New, which starts an untitled note, and an arrow that lists today's meetings that have not ended, including one under way.
|
|
6
6
|
* There is no pop-up note any more: the chip, and every start of a recording, show the Notes place with that note selected,
|
|
7
7
|
* and the note there is the full note (pm/dest-notes.js).
|
|
8
8
|
*
|
|
@@ -117,7 +117,7 @@ async function begin(id) {
|
|
|
117
117
|
}
|
|
118
118
|
/** New: an untitled note, linked to nothing, that listens at once. */
|
|
119
119
|
export function recordNew(btn) { return startNote({}, btn); }
|
|
120
|
-
/** A meeting
|
|
120
|
+
/** A meeting from the Record menu (coming up, or under way): a note made for its event, with its context kept. */
|
|
121
121
|
export function recordEvent(e, btn) { return startNote({ meeting: meetingOf(e) }, btn); }
|
|
122
122
|
/** Take notes on a calendar event (the calendar's own button): the note is linked to the event, and to its initiative when it has one. */
|
|
123
123
|
export function takeNotes(e, { ws, repo, initiative } = {}, btn) {
|
|
@@ -61,13 +61,29 @@ export function meetingOf(e) {
|
|
|
61
61
|
}
|
|
62
62
|
export const notesRows = () => rows || [];
|
|
63
63
|
|
|
64
|
-
/** Today's meetings that have not
|
|
65
|
-
*
|
|
64
|
+
/** Today's meetings that have not ended and have no note yet, soonest first (the Record control's "upcoming" menu). A meeting that has
|
|
65
|
+
* started stays in the list until its end plus RECORD_GRACE_MS, so it can still be recorded once it is under way.
|
|
66
|
+
* Read from the Agenda: GET /api/proactive/agenda?month=… The answer is kept for UPCOMING_MS; the time filter runs on every call. */
|
|
66
67
|
const UPCOMING_MS = 60000;
|
|
68
|
+
export const RECORD_GRACE_MS = 10 * 60000;
|
|
69
|
+
const endOf = (e) => { const f = Date.parse(e.end); if (Number.isFinite(f)) return f; const s = Date.parse(e.start); return Number.isFinite(s) ? s + (Number(e.minutes) || 30) * 60000 : NaN; };
|
|
70
|
+
/** Still recordable at `now`: not yet past its end plus the grace period. */
|
|
71
|
+
export const recordable = (e, now = Date.now()) => endOf(e) + RECORD_GRACE_MS > now;
|
|
72
|
+
/** Has the meeting started at `now`? */
|
|
73
|
+
export const started = (e, now = Date.now()) => Date.parse(e.start) <= now;
|
|
74
|
+
/** When the meeting is, for the Record menu: 'in 25 min', 'in 1 h 10 min', 'Now · started 5 min ago', or 'Ended 3 min ago'. */
|
|
75
|
+
export function whenText(e, now = Date.now()) {
|
|
76
|
+
const span = (m) => (m < 60 ? m + ' min' : Math.floor(m / 60) + ' h' + (m % 60 ? ' ' + (m % 60) + ' min' : ''));
|
|
77
|
+
const end = endOf(e);
|
|
78
|
+
if (Number.isFinite(end) && end <= now) return 'Ended ' + span(Math.max(1, Math.round((now - end) / 60000))) + ' ago';
|
|
79
|
+
if (started(e, now)) { const m = Math.round((now - Date.parse(e.start)) / 60000); return m < 1 ? 'Now · just started' : 'Now · started ' + span(m) + ' ago'; }
|
|
80
|
+
return 'in ' + span(Math.max(1, Math.round((Date.parse(e.start) - now) / 60000)));
|
|
81
|
+
}
|
|
82
|
+
const live = (evs) => (evs || []).filter((e) => recordable(e));
|
|
67
83
|
let upcoming = null; let upcomingAt = 0; let upcomingReading = null;
|
|
68
84
|
export function upcomingToday({ force = false } = {}) {
|
|
69
|
-
if (!force && upcoming && Date.now() - upcomingAt < UPCOMING_MS) return Promise.resolve(upcoming);
|
|
70
|
-
if (upcomingReading) return upcomingReading;
|
|
85
|
+
if (!force && upcoming && Date.now() - upcomingAt < UPCOMING_MS) return Promise.resolve(live(upcoming));
|
|
86
|
+
if (upcomingReading) return upcomingReading.then(live);
|
|
71
87
|
const d = new Date(); const pad = (n) => String(n).padStart(2, '0');
|
|
72
88
|
const day = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
|
73
89
|
// the Agenda gives the event; today's brief gives the plan for it (purpose, how to be ready, steps, links, related items)
|
|
@@ -77,10 +93,11 @@ export function upcomingToday({ force = false } = {}) {
|
|
|
77
93
|
const cal = t && t.ok && t.brief && Array.isArray(t.brief.calendar) ? t.brief.calendar : [];
|
|
78
94
|
const planOf = (e) => cal.find((c) => minuteOf(c.start) === minuteOf(e.start) && normTitle(c.title) === normTitle(e.title));
|
|
79
95
|
const merged = evs.map((e) => { const p = planOf(e); return p ? { ...e, with: e.with || p.with, purpose: p.purpose, prep: typeof p.prep === 'string' ? p.prep : e.prep, steps: p.steps, links: p.links, refs: p.refs, initiative: p.initiative } : e; });
|
|
80
|
-
|
|
96
|
+
// the server's `ended` flag is not read here: it is true from the end itself, and the list keeps a meeting for the grace period after it
|
|
97
|
+
upcoming = merged.filter((e) => recordable(e)).sort((a, b) => String(a.start).localeCompare(String(b.start)));
|
|
81
98
|
return upcoming;
|
|
82
99
|
}).catch(() => { upcomingReading = null; return upcoming || []; });
|
|
83
|
-
return upcomingReading;
|
|
100
|
+
return upcomingReading.then(live);
|
|
84
101
|
}
|
|
85
102
|
export const upcomingWithoutNote = (evs) => (evs || []).filter((e) => notesFor(e).length === 0);
|
|
86
103
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* mountRecording(host) / disposeRecording(host)
|
|
4
4
|
*
|
|
5
5
|
* The control is always there, idle included, with the same capabilities as the main app's Record control (pm/topbar.js, pm/note-dock.js):
|
|
6
|
-
* New (an untitled note that records at once), an arrow that lists today's meetings that have not
|
|
6
|
+
* New (an untitled note that records at once), an arrow that lists today's meetings that have not ended, including one under way (a pick makes a note linked to that
|
|
7
7
|
* calendar event and records it), and, while a meeting records or is paused, a chip that names it with Highlight, Pause or Resume, and Stop
|
|
8
8
|
* (Stop asks first and offers "Pause instead"). All of it goes through pm/note-dock.js, which owns the real recorder, so the audio capture of
|
|
9
9
|
* pm/view-notes.js runs here without the main app's Notes place on screen. The chip opens the note in the main app, because console3 has no
|
|
@@ -51,7 +51,7 @@ export const simDock = {
|
|
|
51
51
|
const live = new WeakMap();
|
|
52
52
|
|
|
53
53
|
/* Demo mode, idle: the mockup's Record button and the arrow beside it (the same two buttons the real control draws). Record starts the simulated recorder with an
|
|
54
|
-
* untitled note; the arrow lists the same choice and today's meetings that have not
|
|
54
|
+
* untitled note; the arrow lists the same choice and today's meetings that have not ended. Nothing is opened or saved. */
|
|
55
55
|
function simIdle(d) {
|
|
56
56
|
const glyph = (dd) => { const sv = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); sv.setAttribute('class', 'ic sm'); sv.setAttribute('viewBox', '0 0 24 24'); sv.setAttribute('aria-hidden', 'true'); const pth = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pth.setAttribute('d', dd); sv.appendChild(pth); return sv; };
|
|
57
57
|
const start = (title) => { if (d === simDock) simStart(title); };
|
|
@@ -182,15 +182,15 @@ function mountReal(host) {
|
|
|
182
182
|
const off = (b, why) => { b.setAttribute('aria-disabled', 'true'); b.classList.add('c3rec__off'); b.title = why; b.setAttribute('aria-label', why); return b; };
|
|
183
183
|
const chipText = (st) => (st.mode === 'recording' ? (st.started ? clockOf(Date.now() - st.started) : '') : st.until ? 'paused · ' + Math.max(1, Math.ceil((st.until - Date.now()) / 60000)) + ' min left' : 'paused');
|
|
184
184
|
|
|
185
|
-
/** The arrow beside New: Untitled note, then today's meetings that have not
|
|
185
|
+
/** The arrow beside New: Untitled note, then today's meetings that have not ended (or ended within the grace period) and have no note, the next one marked. */
|
|
186
186
|
async function openUpcoming(anchor) {
|
|
187
187
|
const { menu } = await import('./primitives.js');
|
|
188
188
|
const links = await import('./notes-links.js');
|
|
189
189
|
const evs = links.upcomingWithoutNote(await links.upcomingToday());
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const inText =
|
|
193
|
-
return { label: e.title, hint: e.time || '', detail: [
|
|
190
|
+
const next = evs.find((x) => !links.started(x));
|
|
191
|
+
const fmt = (e) => {
|
|
192
|
+
const inText = links.whenText(e);
|
|
193
|
+
return { label: e.title, hint: e.time || '', detail: [e === next ? 'Next' : null, inText, e.with || null].filter(Boolean).join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
|
|
194
194
|
};
|
|
195
195
|
menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => dock.recordNew() }, { sep: true },
|
|
196
196
|
...(evs.length ? evs.map(fmt) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
|
|
@@ -23,7 +23,7 @@ import { button, iconButton, menu, needsDot, needsCount } from './primitives.js'
|
|
|
23
23
|
import { COPY } from './frame-copy.js';
|
|
24
24
|
import * as dock from './note-dock.js';
|
|
25
25
|
import { el as h } from './dom.js';
|
|
26
|
-
import { upcomingToday, upcomingWithoutNote } from './notes-links.js';
|
|
26
|
+
import { upcomingToday, upcomingWithoutNote, whenText, started } from './notes-links.js';
|
|
27
27
|
import { contextOf, peopleOf } from './notes-context.js';
|
|
28
28
|
|
|
29
29
|
/** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
|
|
@@ -91,7 +91,7 @@ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {})
|
|
|
91
91
|
/* The Record control (meeting-notes Run 13, prototype/meeting-notes/record-control-mockup.html). Two groups, always in the same
|
|
92
92
|
* place. Left, only while a meeting is recording or paused: the chip that names it, then Highlight, Pause and Stop (recording)
|
|
93
93
|
* or Resume and Stop (paused). Nothing to resume: no left group. Right, always: New (an untitled note) and an arrow that lists
|
|
94
|
-
* today's meetings that have not
|
|
94
|
+
* today's meetings that have not ended (one under way included); both are unavailable while a meeting is recording. The highlight's pop-up hangs
|
|
95
95
|
* under the control. */
|
|
96
96
|
const recSlot = el('div', { class: 'fr-rec', 'data-fr': 'record' });
|
|
97
97
|
let recKey = '';
|
|
@@ -141,15 +141,15 @@ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {})
|
|
|
141
141
|
if (panel) kids.push(panel);
|
|
142
142
|
setChildren(recSlot, kids);
|
|
143
143
|
}
|
|
144
|
-
/** The arrow beside New: Untitled note, then today's meetings that have not
|
|
144
|
+
/** The arrow beside New: Untitled note, then today's meetings that have not ended (or ended within the grace period) and have no note, the next one marked. */
|
|
145
145
|
async function openUpcoming(anchor) {
|
|
146
146
|
const evs = upcomingWithoutNote(await upcomingToday());
|
|
147
|
-
const
|
|
147
|
+
const next = evs.find((x) => !started(x));
|
|
148
|
+
const fmt = (e) => {
|
|
148
149
|
const c = contextOf(e) || {};
|
|
149
|
-
const
|
|
150
|
-
const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
|
|
150
|
+
const inText = whenText(e); const first = e === next;
|
|
151
151
|
const who = peopleOf(c); const extra = c.attendees ? ' (' + c.attendees + ' attendees)' : '';
|
|
152
|
-
const bits = [
|
|
152
|
+
const bits = [first ? 'Next' : null, inText, who.length ? who.slice(0, 2).join(', ') + (who.length > 2 ? ' +' + (who.length - 2) : '') : c.attendees ? c.attendees + ' attendees' : null, c.agenda && c.agenda.length ? c.agenda.length + ' agenda item' + (c.agenda.length === 1 ? '' : 's') : null].filter(Boolean);
|
|
153
153
|
void extra;
|
|
154
154
|
return { label: e.title, hint: e.time || '', detail: bits.join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
|
|
155
155
|
};
|