create-pmos 0.224.0 → 0.225.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.224.0",
3
+ "version": "0.225.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@f8c3934
1
+ kernel-source: pmos@6a10a1d
2
2
  assembled: 2026-10-14
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-14T05:57:56Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
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-14T05:57:56Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
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-14T05:57:56Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T06:35:37Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -10,6 +10,7 @@ const T = require('./test-run').configure({ name: 'c3-light-load' });
10
10
 
11
11
  const BASE = T.BASE;
12
12
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ // Settings and the dock tree get 30 s: a loaded CI runner took longer than 15 s to draw them (twice on one run, 33/33 locally)
13
14
  const until = async (fn, ms = 15000, step = 150) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
14
15
  let pass = 0; let fail = 0;
15
16
  const ok = (name, cond, extra = '') => { if (cond) pass++; else fail++; console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
@@ -74,18 +75,18 @@ process.on('unhandledRejection', (e) => { console.error(e); fail++; finish(); })
74
75
  /* each place still opens */
75
76
  await page.click('[data-c3-tab="cal"]');
76
77
  ok('Calendar: its code loads on the first press', !!(await until(() => scripts.has('calendar-views.js'))));
77
- ok('Calendar: the page draws', !!(await until(() => page.evaluate(() => { const p = document.querySelector('[data-c3-pane="cal"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
78
+ ok('Calendar: the page draws', !!(await until(() => page.evaluate(() => { const p = document.querySelector('[data-c3-pane="cal"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
78
79
  await page.click('[data-c3-tab="init"]');
79
- ok('Initiatives: its code loads and the page draws', !!(await until(() => scripts.has('c3-initiatives.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="init"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
80
+ ok('Initiatives: its code loads and the page draws', !!(await until(() => scripts.has('c3-initiatives.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="init"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
80
81
  await page.click('[data-c3-tab="rep"]');
81
82
  ok('Reports: its code loads and the page draws', !!(await until(() => scripts.has('c3-reports.js') && page.evaluate(() => !!document.querySelector('[data-c3-pane="rep"] [data-rp3]')))));
82
83
  await page.click('[data-c3pg="settings"]');
83
- ok('Settings: its code loads and the page draws', !!(await until(() => scripts.has('c3-settings.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="settings"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
84
+ ok('Settings: its code loads and the page draws', !!(await until(() => scripts.has('c3-settings.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="settings"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }), 30000)));
84
85
  await page.click('[data-c3-tab="brief"]');
85
86
  const before = gets.length;
86
87
  await page.keyboard.press('Control+j');
87
88
  ok('Dock: opening it reads the sessions and the working trees', !!(await until(() => gets.slice(before).some((g) => g.path === '/api/sessions') && gets.slice(before).some((g) => g.path === '/api/git/worktrees'))));
88
- ok('Dock: the tree draws', !!(await until(() => page.evaluate(() => { const t = document.querySelector('[data-c3pg="dock-tree"]'); return !!t && !document.querySelector('[data-c3pg="dock"]').hidden && !/Reading the repository/.test(t.textContent); }))));
89
+ ok('Dock: the tree draws', !!(await until(() => page.evaluate(() => { const t = document.querySelector('[data-c3pg="dock-tree"]'); return !!t && !document.querySelector('[data-c3pg="dock"]').hidden && !/Reading the repository/.test(t.textContent); }), 30000)));
89
90
  await page.keyboard.press('Control+j');
90
91
  await page.evaluate(() => { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); });
91
92
  await page.keyboard.press('c'); // the key C opens the calendar popup (the header button may be in the quiet layout's overflow)
@@ -141,7 +141,7 @@ When PMOS holds nothing for a past meeting, the detail says so. For a coming mee
141
141
 
142
142
  ### Meeting focus
143
143
 
144
- **Focus** opens one calm page for a meeting that is today. It shows on the detail of a meeting that has not ended, and next to the recording on the header while a meeting is recorded. The page covers the console. **Leave focus** gives the console back. Leaving does not stop the recording. The page stays open when you reload it. From the recording, the page finds the meeting by its start time and title, so it also finds a meeting that has no calendar id, or that moved on the same day after you started to record.
144
+ **Focus** opens one calm page for a meeting that is today. It shows on the detail of a meeting that has not ended, and next to the recording on the header while a meeting is recorded. An untitled note that records has **Focus** too: the page opens on the note, with the conversation and the captured items, and no topics or material. The page covers the console. **Leave focus** gives the console back. Leaving does not stop the recording. The page stays open when you reload it. From the recording, the page finds the meeting by its start time and title, so it also finds a meeting that has no calendar id, or that moved on the same day after you started to record.
145
145
 
146
146
  The page has these parts:
147
147
 
@@ -16,7 +16,7 @@ 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 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.
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. These are the meetings the Calendar shows, including the ones you imported in Settings. 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
 
@@ -7,8 +7,9 @@
7
7
  * 1. <runtime>/calendar-import/events.jsonl the import ledger (Settings › Import now, `pmos calendar import`);
8
8
  * 2. <runtime>/calendar-memory.json the calendar the morning brief submits (lib/calendar-memory.js), 90 days back and 31 ahead;
9
9
  * 3. <runtime>/briefs/<today>.json today's brief, whose calendar is today's meetings with their plan.
10
- * The Agenda and the Record menu read 2 and 3 (GET /api/proactive/agenda). Calendar and the Briefing's Today read 1 only, so a PM whose
11
- * meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three. An imported event wins
10
+ * The Agenda reads 2 and 3 (GET /api/proactive/agenda). Calendar and the Briefing's Today read 1 only, so a PM whose
11
+ * meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three. The Record menu reads it
12
+ * too (todayOf, GET /api/calendar-today): it read the Agenda once, so an imported meeting was on the Calendar and missing under Record. An imported event wins
12
13
  * over a remembered one with the same start minute and title, because it carries the PM's notes; today's brief replaces the remembered
13
14
  * events of today, as agendaMonth does.
14
15
  *
@@ -71,4 +72,22 @@ function readAll(runtimeDir, { now = Date.now() } = {}) {
71
72
  return out.sort((a, b) => (Date.parse(a.at) || 0) - (Date.parse(b.at) || 0));
72
73
  }
73
74
 
74
- module.exports = { readAll, asEvent, localIso };
75
+ /** Today's meetings as the Record menu reads them (GET /api/calendar-today): every event readAll joins, kept when its start falls on
76
+ * the local day of `now`, in the shape the menu and a note's meeting link use: { id, start, end, title, time, minutes, with, src, ... }. Pure. */
77
+ function todayOf(events, now = Date.now()) {
78
+ const day = localDate(now);
79
+ const out = [];
80
+ for (const e of events || []) {
81
+ const s = Date.parse(e && e.at);
82
+ if (!e || !e.title || Number.isNaN(s) || localDate(s) !== day) continue;
83
+ const f = e.end && !Number.isNaN(Date.parse(e.end)) ? Date.parse(e.end) : s + 30 * 60000;
84
+ const people = [].concat(e.attendees || []).map((p) => (p && typeof p === 'object' ? p.name || '' : String(p || ''))).filter(Boolean);
85
+ const d = new Date(s);
86
+ out.push({ id: String(e.id || ''), start: localIso(s), end: localIso(f), title: String(e.title), time: `${pad(d.getHours())}:${pad(d.getMinutes())}`,
87
+ minutes: Math.round((f - s) / 60000), ...(people.length ? { with: people.slice(0, 6).join(', ') } : {}),
88
+ src: e.src || 'import', ...(e.initiative ? { initiative: e.initiative } : {}) });
89
+ }
90
+ return out.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
91
+ }
92
+
93
+ module.exports = { readAll, asEvent, localIso, todayOf };
@@ -5,6 +5,7 @@
5
5
  * layers, layers_total, layers_note, free, context, context_total, (lib/calendar-layers.js)
6
6
  * link_boards } every board a meeting can be linked to: [{ ws, id, name }]
7
7
  * &only=gantt → { ok, view, range, gantt, no_plan, strategy, today } the rows with a plan date or a Jira read, and how many have neither
8
+ * GET /api/calendar-today → { ok, date, events } today's meetings for the Record menu (calendar-events.js todayOf)
8
9
  * POST /api/calendar-links → { ok, link } what a meeting is about (lib/calendar-links.js, below)
9
10
  * tidy, tidy_total, tidy_note } (lib/calendar-tidy.js: suggestions in words, no write)
10
11
  *
@@ -221,13 +222,22 @@ async function postLink(req, res, ctx) {
221
222
 
222
223
  module.exports = {
223
224
  readers,
224
- prefixes: ['/api/calendar-views', '/api/calendar-links'],
225
+ prefixes: ['/api/calendar-views', '/api/calendar-links', '/api/calendar-today'],
225
226
  handlePure: handle,
226
227
  handle: async (req, res, url, ctx) => {
227
228
  if (url.pathname === '/api/calendar-links') {
228
229
  if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
229
230
  return postLink(req, res, ctx);
230
231
  }
232
+ if (url.pathname === '/api/calendar-today') {
233
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
234
+ const cal = require('./calendar-events.js');
235
+ const now = Date.now();
236
+ let events = [];
237
+ try { events = cal.todayOf(cal.readAll(ctx.runtimeDir, { now }), now); } catch { events = []; }
238
+ ctx.json(res, { ok: true, date: today(), events });
239
+ return true;
240
+ }
231
241
  if (url.pathname !== '/api/calendar-views') return false;
232
242
  if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
233
243
  const w = ctx.getWs(url.searchParams.get('ws'));
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.224.0",
3
+ "version": "0.225.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.224.0",
9
+ "version": "0.225.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.224.0",
3
+ "version": "0.225.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",
@@ -63,7 +63,7 @@ export const notesRows = () => rows || [];
63
63
 
64
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
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
+ * Read from the calendar: GET /api/calendar-today (the meetings the Calendar shows). The answer is kept for UPCOMING_MS; the time filter runs on every call. */
67
67
  const UPCOMING_MS = 60000;
68
68
  export const RECORD_GRACE_MS = 10 * 60000;
69
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; };
@@ -84,12 +84,11 @@ let upcoming = null; let upcomingAt = 0; let upcomingReading = null;
84
84
  export function upcomingToday({ force = false } = {}) {
85
85
  if (!force && upcoming && Date.now() - upcomingAt < UPCOMING_MS) return Promise.resolve(live(upcoming));
86
86
  if (upcomingReading) return upcomingReading.then(live);
87
- const d = new Date(); const pad = (n) => String(n).padStart(2, '0');
88
- const day = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
89
- // the Agenda gives the event; today's brief gives the plan for it (purpose, how to be ready, steps, links, related items)
90
- upcomingReading = Promise.all([pmGet('/api/proactive/agenda?month=' + day.slice(0, 7)), pmGet('/api/proactive/today')]).then(([r, t]) => {
87
+ // the calendar gives the event (every meeting the Calendar shows: imported, remembered and today's brief); today's brief gives the plan for it
88
+ // (purpose, how to be ready, steps, links, related items). The Agenda alone missed the imported meetings.
89
+ upcomingReading = Promise.all([pmGet('/api/calendar-today'), pmGet('/api/proactive/today')]).then(([r, t]) => {
91
90
  upcomingReading = null; upcomingAt = Date.now();
92
- const evs = r && r.days && r.days[day] ? r.days[day].events || [] : [];
91
+ const evs = r && r.ok && Array.isArray(r.events) ? r.events : [];
93
92
  const cal = t && t.ok && t.brief && Array.isArray(t.brief.calendar) ? t.brief.calendar : [];
94
93
  const planOf = (e) => cal.find((c) => minuteOf(c.start) === minuteOf(e.start) && normTitle(c.title) === normTitle(e.title));
95
94
  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; });
@@ -178,6 +178,12 @@ function openFocusFor(m) { // no date: the page takes the local day of the sta
178
178
  .catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
179
179
  }
180
180
 
181
+ /** Meeting focus for a recording that is not on the calendar (an untitled note): the page opens on the note, with no topics or brief. */
182
+ function openFocusNote(id, title) {
183
+ import('./c3-focus.js').then((f) => f.openFocus({ note: id, event: { title: title || 'Untitled note' } }))
184
+ .catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
185
+ }
186
+
181
187
  function mountReal(host) {
182
188
  ensureC3Css();
183
189
  let ready = false;
@@ -232,7 +238,7 @@ function mountReal(host) {
232
238
  group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
233
239
  btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
234
240
  } else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
235
- if (mt) group.push(btn('Open the focus page for this meeting', 'focus', () => openFocusFor(mt), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
241
+ group.push(btn('Open the focus page for this meeting', 'focus', () => (mt ? openFocusFor(mt) : openFocusNote(st.id, st.title)), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
236
242
  group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
237
243
  kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
238
244
  }