create-pmos 0.192.0 → 0.193.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/console-calendar.md +1 -1
- package/workspace/lib/routes-meeting-focus.js +23 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-focus.js +6 -4
- package/workspace/public/pm/c3-shell.js +1 -1
- package/workspace/public/pm/shell-recording.js +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.193.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-13T08:02:12Z"}
|
|
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-13T08:02:12Z"}
|
|
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-13T08:02:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -137,7 +137,7 @@ When PMOS holds nothing for a past meeting, the detail says so. For a coming mee
|
|
|
137
137
|
|
|
138
138
|
### Meeting focus
|
|
139
139
|
|
|
140
|
-
**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.
|
|
140
|
+
**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.
|
|
141
141
|
|
|
142
142
|
The page has these parts:
|
|
143
143
|
|
|
@@ -55,14 +55,32 @@ function linesOf(runtimeDir, keys) {
|
|
|
55
55
|
|
|
56
56
|
/* ── the meeting, its note and its brief ──────────────────────────────────────────────────────────────────────────── */
|
|
57
57
|
|
|
58
|
+
const minuteOf = (t) => { const ms = Date.parse(t); return Number.isFinite(ms) ? Math.floor(ms / 60000) : null; };
|
|
59
|
+
const normTitle = (t) => require('./calendar-memory.js').normTitle(t);
|
|
60
|
+
/** A meeting named by its start and title (a recording whose meeting has no stored id). The start is compared as an instant, not as
|
|
61
|
+
* text: a note keeps its meeting's start in UTC ("…T07:00:00.000Z") while the calendar writes it with the local offset ("…T15:00:00+08:00").
|
|
62
|
+
* Same minute and same title first, then the only meeting of that title on that local day (a meeting moved after the note linked it). */
|
|
63
|
+
function findByStart(events, by) {
|
|
64
|
+
const m = minuteOf(by.at); if (m === null) return null;
|
|
65
|
+
const rows = (events || []).filter((e) => e && e.id && minuteOf(e.at) !== null);
|
|
66
|
+
const title = by.title ? normTitle(by.title) : '';
|
|
67
|
+
const atMin = rows.filter((e) => minuteOf(e.at) === m);
|
|
68
|
+
const both = title ? atMin.filter((e) => normTitle(e.title) === title) : atMin;
|
|
69
|
+
if (both.length) return both[0];
|
|
70
|
+
if (!title) return null;
|
|
71
|
+
const day = localDay(m * 60000);
|
|
72
|
+
const named = rows.filter((e) => normTitle(e.title) === title && localDay(Date.parse(e.at)) === day);
|
|
73
|
+
return named.length === 1 ? named[0] : null;
|
|
74
|
+
}
|
|
58
75
|
function findEvent(events, id, date, by) {
|
|
59
|
-
if (!id && by && by.at)
|
|
60
|
-
const hit = (events || []).filter((e) => e && e.id && e.at === String(by.at) && (!by.title || e.title === String(by.title)));
|
|
61
|
-
return hit[0] || null;
|
|
62
|
-
}
|
|
76
|
+
if (!id && by && by.at) return findByStart(events, by);
|
|
63
77
|
const rows = (events || []).filter((e) => e && e.id && (String(e.id) === id || baseId(e.id) === baseId(id)));
|
|
64
78
|
const onDay = rows.filter((e) => !date || String(e.at || '').slice(0, 10) === date);
|
|
65
|
-
|
|
79
|
+
const hit = onDay.find((e) => String(e.id) === id) || onDay[0] || null;
|
|
80
|
+
if (hit) return hit;
|
|
81
|
+
// a date that is the UTC day of the start, not its local day (an early-morning meeting east of UTC): the start names it
|
|
82
|
+
if (by && by.at) return findByStart(events, by);
|
|
83
|
+
return rows.find((e) => String(e.id) === id) || null; // an occurrence id names one meeting whatever the day says
|
|
66
84
|
}
|
|
67
85
|
/** A recording that is not on the calendar (a talk in the corridor): the note stands in for the meeting. It has no end, no people
|
|
68
86
|
* and no brief, so focus shows what is said, PMOS's notes and Captured, and no topics from an invite. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.193.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.193.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -32,6 +32,8 @@ import { toast } from './c3-toast.js';
|
|
|
32
32
|
const POLL_MS = 3000;
|
|
33
33
|
const LAST_MINUTES = 5; // the last minutes of the meeting: the captured items show under the stage
|
|
34
34
|
const pad = (n) => String(n).padStart(2, '0');
|
|
35
|
+
/** The local day of a start: a note keeps its meeting's start in UTC, and its first ten characters can be the day before. */
|
|
36
|
+
const localDayOf = (t) => { const ms = Date.parse(t); if (!Number.isFinite(ms)) return ''; const d = new Date(ms); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
|
35
37
|
const hhmm = (ms) => { const d = new Date(ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()); };
|
|
36
38
|
export const baseId = (id) => String(id || '').split('@')[0];
|
|
37
39
|
/** 3:07, or 1:03:07 from an hour on; a minus sign before a negative time. Pure. */
|
|
@@ -84,7 +86,7 @@ export function closeFocus() {
|
|
|
84
86
|
const grid = host && host.querySelector('.c3grid'); if (grid) grid.inert = false;
|
|
85
87
|
const comp = host && host.querySelector('[data-c3ph]'); if (comp) comp.inert = false;
|
|
86
88
|
if (f.phone) { const st = f.phone && document.querySelector('[data-c3ph-focus-bar]'); if (st) st.focus({ preventScroll: true }); return; }
|
|
87
|
-
try { const u = new URL(location.href); if (u.searchParams.has('focus')) { u.searchParams.delete('focus'); u.searchParams.delete('fdate'); history.replaceState(history.state, '', u); } } catch { /* no history */ }
|
|
89
|
+
try { const u = new URL(location.href); if (u.searchParams.has('focus')) { u.searchParams.delete('focus'); u.searchParams.delete('fdate'); u.searchParams.delete('fat'); u.searchParams.delete('ftitle'); history.replaceState(history.state, '', u); } } catch { /* no history */ }
|
|
88
90
|
if (f.opener && document.contains(f.opener) && typeof f.opener.focus === 'function') { try { f.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -95,7 +97,7 @@ export function openFocus({ event, date, pop, back, phone, note, meet, mtitle }
|
|
|
95
97
|
if (phone) pop = false; // the phone has the whole page, in one column
|
|
96
98
|
const resolve = !!pop && want === 'now'; // the extension names no meeting: which one this call is comes first
|
|
97
99
|
const id = resolve ? '' : want;
|
|
98
|
-
const day = String(date ||
|
|
100
|
+
const day = String(date || localDayOf(ev.at || ev.start) || '');
|
|
99
101
|
ensureCss();
|
|
100
102
|
const host = document.getElementById('c3-root') || document.body;
|
|
101
103
|
const root = el('div', { class: 'c3focus' + (pop ? ' fc-pop' : '') + (phone ? ' fc-phone' : ''), role: 'region', 'aria-label': pop ? 'PMOS meeting helper' : 'Meeting focus', 'data-c3focus': '', ...(pop ? { 'data-fc-pop': '' } : {}), ...(phone ? { 'data-fc-phone': '' } : {}) });
|
|
@@ -142,7 +144,7 @@ export function openFocus({ event, date, pop, back, phone, note, meet, mtitle }
|
|
|
142
144
|
host.append(root);
|
|
143
145
|
const grid = host.querySelector && host.querySelector('.c3grid'); if (grid) grid.inert = true;
|
|
144
146
|
const comp = host.querySelector && host.querySelector('[data-c3ph]'); if (comp) comp.inert = true; // the phone companion under the page
|
|
145
|
-
if (!pop && !phone) { try { const u = new URL(location.href); u.searchParams.set('focus', id); if (day) u.searchParams.set('fdate', day); history.replaceState(history.state, '', u); } catch { /* no history */ } } // the compact page keeps its address: a reload asks again which meeting it is
|
|
147
|
+
if (!pop && !phone) { try { const u = new URL(location.href); u.searchParams.set('focus', id); if (day) u.searchParams.set('fdate', day); if (!id && ev.at) { u.searchParams.set('fat', ev.at); if (ev.title) u.searchParams.set('ftitle', ev.title); } history.replaceState(history.state, '', u); } catch { /* no history */ } } // the compact page keeps its address: a reload asks again which meeting it is
|
|
146
148
|
draw();
|
|
147
149
|
F.timers.clock = setInterval(tick, 1000);
|
|
148
150
|
if (resolve) resolveNow(); else load();
|
|
@@ -185,7 +187,7 @@ async function load() {
|
|
|
185
187
|
if (f.id) p.set('event', f.id);
|
|
186
188
|
else if (f.noteId) p.set('note', f.noteId); // a recording with no meeting
|
|
187
189
|
if (f.date && !f.noteId) p.set('date', f.date);
|
|
188
|
-
if (!f.
|
|
190
|
+
if (!f.noteId && (e.at || e.start)) { p.set('at', e.at || e.start); if (e.title) p.set('title', e.title); } // the start and title still find the meeting when the id or the day does not
|
|
189
191
|
const j = await pmGet('/api/meeting-focus?' + p.toString());
|
|
190
192
|
if (F !== f) return;
|
|
191
193
|
if (j && j.ok) {
|
|
@@ -623,7 +623,7 @@ async function boot() {
|
|
|
623
623
|
let start = q.get('tab');
|
|
624
624
|
const saved = DEMO ? null : await pmGet('/api/settings' + (S.ws ? '?ws=' + encodeURIComponent(S.ws) : '')).catch(() => null); // demo mode's workspace is not registered
|
|
625
625
|
if (saved && saved.ok) { applyLook(root, saved.settings); if (!start) { const s = (saved.settings || []).find((x) => x.key === 'look.start'); if (s && BUILD[s.value]) start = s.value; } }
|
|
626
|
-
if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '' }, date: q.get('fdate') || '', pop: q.get('pop') === '1', meet: q.get('meet') || '', mtitle: q.get('mtitle') || '' })).catch(() => { /* the focus page is not available */ }); // a reload keeps the Meeting focus page open; ?pop=1 is its compact form for the Chrome extension (focus=now asks which meeting the Meet call is)
|
|
626
|
+
if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '', ...(q.get('fat') ? { at: q.get('fat'), title: q.get('ftitle') || '' } : {}) }, date: q.get('fdate') || '', pop: q.get('pop') === '1', meet: q.get('meet') || '', mtitle: q.get('mtitle') || '' })).catch(() => { /* the focus page is not available */ }); // a reload keeps the Meeting focus page open; ?pop=1 is its compact form for the Chrome extension (focus=now asks which meeting the Meet call is)
|
|
627
627
|
if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
|
|
628
628
|
new ResizeObserver(moveInd).observe(S.tabNav);
|
|
629
629
|
workCount(); briefCount(); askCount(); S.dock.start();
|
|
@@ -173,9 +173,8 @@ function meetingOfNote(id) {
|
|
|
173
173
|
return m && m.kind === 'event' && m.start ? m : null;
|
|
174
174
|
}
|
|
175
175
|
/** Meeting focus (initiative c3-meeting-focus): the page for the meeting being recorded. The event id is empty for a meeting without a stored id; the page then finds it by start and title. */
|
|
176
|
-
function openFocusFor(m) {
|
|
177
|
-
|
|
178
|
-
import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.event_id || '', at: m.start, end: m.end || null, title: m.title }, date: day }))
|
|
176
|
+
function openFocusFor(m) { // no date: the page takes the local day of the start (the note keeps it in UTC)
|
|
177
|
+
import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.event_id || '', at: m.start, end: m.end || null, title: m.title } }))
|
|
179
178
|
.catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
|
|
180
179
|
}
|
|
181
180
|
|