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 +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/c3-light-load-test.js +5 -4
- package/workspace/guide/console-calendar.md +1 -1
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/calendar-events.js +22 -3
- package/workspace/lib/routes-calendar-views.js +11 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/notes-links.js +5 -6
- package/workspace/public/pm/shell-recording.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
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-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-
|
|
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-
|
|
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
|
|
package/workspace/guide/notes.md
CHANGED
|
@@ -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
|
|
11
|
-
* meetings came in with the morning brief saw them under Record and nowhere else. This reader joins the three.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.225.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|