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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.149.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@b71e523
1
+ kernel-source: pmos@3f66dcc
2
2
  assembled: 2026-10-12
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-12T03:20:05Z"}
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:20:05Z"}
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:20:05Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T03:30:13Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -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 started and have no note, with the next one first. 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.
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.149.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.149.0",
9
+ "version": "0.150.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.149.0",
3
+ "version": "0.150.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",
@@ -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 started.
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 that has not started: a note made for its event, with its context kept. */
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 started and have no note yet, soonest first (the Record control's "upcoming" menu).
65
- * Read from the Agenda: GET /api/proactive/agenda?month=… The answer is kept for UPCOMING_MS. */
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
- upcoming = merged.filter((e) => !e.ended && Date.parse(e.start) > Date.now()).sort((a, b) => String(a.start).localeCompare(String(b.start)));
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 started (a pick makes a note linked to that
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 started. Nothing is opened or saved. */
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 started and have no note, the next one marked. */
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 fmt = (e, i) => {
191
- const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
192
- const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
193
- return { label: e.title, hint: e.time || '', detail: [i === 0 ? 'Next' : null, inText, e.with || null].filter(Boolean).join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
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 started; both are unavailable while a meeting is recording. The highlight's pop-up hangs
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 started and have no note, the next one marked. */
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 fmt = (e, i) => {
147
+ const next = evs.find((x) => !started(x));
148
+ const fmt = (e) => {
148
149
  const c = contextOf(e) || {};
149
- const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
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 = [i === 0 ? '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);
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
  };