create-pmos 0.224.0 → 0.226.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-core-docs-test.js +1 -1
- package/workspace/c3-light-load-test.js +5 -4
- package/workspace/guide/console-calendar.md +1 -1
- package/workspace/guide/core-documents.md +3 -2
- package/workspace/guide/notes.md +1 -1
- package/workspace/lib/calendar-events.js +22 -3
- package/workspace/lib/core-docs.js +15 -8
- 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/c3-demo-coredocs.js +5 -5
- 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.226.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:56:55Z"}
|
|
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:56:55Z"}
|
|
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:56:55Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ const { harness } = require('./c3-ini-harness');
|
|
|
30
30
|
const pf = await rows();
|
|
31
31
|
ok('P3 the core set lists 11 documents, strategy first', pf.split(',').length === 11 && pf.startsWith('strategy,okrs,roadmap,invest'), pf);
|
|
32
32
|
ok('P4 states: up to date, draft ready, due, comes later where it applies', (await rowState('strategy')) === 'ok' && (await rowState('roadmap')) === 'draft' && (await rowState('status')) === 'due');
|
|
33
|
-
ok('P5 a row shows its question, cadence, reader and who writes it', await st(() => { const t = document.querySelector('[data-cd-row="strategy"]').textContent; return /Yearly, reviewed each quarter/.test(t) && /Leadership, every PM/.test(t) && /
|
|
33
|
+
ok('P5 a row shows its question, cadence, reader and who writes it', await st(() => { const t = document.querySelector('[data-cd-row="strategy"]').textContent; return /Yearly, reviewed each quarter/.test(t) && /Leadership, every PM/.test(t) && /PMOS proposes/.test(t) && /new/.test(t) && /2 key results do not trace to it/.test(t); }));
|
|
34
34
|
ok('P6 the right column says what the level is for, lists the drafts and how the levels connect', await st(() => { const l = document.querySelector('[data-cd="level"]'); return !!l && /Are we betting on the right things\?/.test(l.textContent) && document.querySelectorAll('[data-cd="drafts"] [data-cd-wait]').length === 2 && !!document.querySelector('[data-cd="connect"]'); }));
|
|
35
35
|
ok('P7 the address holds the Documents view', await st(() => /init=docs/.test(location.search)));
|
|
36
36
|
ok('P8 the summary line counts the drafts and what is due', await st(() => /2 drafts to review, 1 due/.test(document.querySelector('[data-cd="core"]').textContent)));
|
|
@@ -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
|
|
|
@@ -29,7 +29,6 @@ Risks are written once, on the initiative. Board risks and cross-board risks col
|
|
|
29
29
|
| Due | The cadence passed, or the current stage needs the document and it is not there. |
|
|
30
30
|
| Draft ready | PMOS drafted it. It waits for you. |
|
|
31
31
|
| Not started | Nothing is there, and the level needs it now. |
|
|
32
|
-
| None yet | Nothing is there yet, and this fills as the work goes: the decision log, kill conditions, market and user context. |
|
|
33
32
|
| Comes later | The initiative has not reached the stage that needs it. |
|
|
34
33
|
| Stale | It was issued before a change it depends on: a stage move (initiative risks, launch checklist, status update), a newer risk list or outcome review (rolled-up documents), or a change to the board settings (board charter). |
|
|
35
34
|
|
|
@@ -39,13 +38,15 @@ A row that is not started always has a next step:
|
|
|
39
38
|
|
|
40
39
|
- **Draft it**: PMOS drafts a document it keeps itself.
|
|
41
40
|
- **Propose them** (OKRs and health budgets): with no OKR files, PMOS drafts proposed key results and health budgets for you to review and issue. Once OKR files exist, they are the source.
|
|
41
|
+
- **Propose it** (decision log, kill conditions, market and user context): until the real source exists, PMOS drafts one from what it has read. The decision log comes from meeting notes and initiative records. Kill conditions come from each bet and its key result. Market and user context comes from research notes and Horizon scans. You review and issue it. Once the real source exists (a decisions file, kill conditions on the initiatives, research under `planning/research/` or `planning/radar/`), that source is used instead.
|
|
42
|
+
- **Draft it** on the product strategy: PMOS proposes a strategy from your key results, board charters and initiatives. It is a proposal only. You change it, then issue it or drop it.
|
|
42
43
|
- **Run a scan** (Horizon scan reports): opens the Portfolio Overview at Horizon scan, where Scan now is.
|
|
43
44
|
- **Set it up** (status reports): opens Reports, where you set up the report.
|
|
44
45
|
|
|
45
46
|
## How PMOS drafts a document
|
|
46
47
|
|
|
47
48
|
- **When you ask.** Press **Draft it** on a row, or **Ask PMOS to change it** on a draft. PMOS starts one background run. The run reads the sources of that document and writes one draft. It has no tool that sends, posts or edits.
|
|
48
|
-
- **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While
|
|
49
|
+
- **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While their sources do not exist, it also proposes the OKRs, the decision log, kill conditions and market and user context. There is no limit on how many drafts run at once. If a run writes nothing, PMOS tries again on the next tick. If you drop a draft, PMOS does not draft that document again until the next day. The product strategy is proposed when it is not started or its quarterly review is due.
|
|
49
50
|
- **The Initiative Driver.** When the current stage of an initiative needs a document that is not there or is stale, the driver drafts it and files a proposal. **Keep** issues the draft. The driver does not propose a stage move while a document of the current stage is due.
|
|
50
51
|
|
|
51
52
|
## Issue a draft
|
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 };
|
|
@@ -49,17 +49,17 @@ const LEVEL = Object.freeze({
|
|
|
49
49
|
const e = (level, o) => Object.freeze({ level, reader: null, cadence: null, build: false, cat: null, isNew: false, days: null, rollup: null, tool: null, store: false, ...o });
|
|
50
50
|
const SETS = Object.freeze({
|
|
51
51
|
portfolio: Object.freeze([
|
|
52
|
-
e('portfolio', { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: '
|
|
52
|
+
e('portfolio', { key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'proposes', kind: 'doc', days: 91, isNew: true }),
|
|
53
53
|
e('portfolio', { key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', kind: 'derived', store: true, days: 91 }),
|
|
54
54
|
e('portfolio', { key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', kind: 'doc', days: 30 }),
|
|
55
55
|
e('portfolio', { key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', kind: 'doc', days: 91, isNew: true }),
|
|
56
56
|
e('portfolio', { key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', kind: 'derived' }),
|
|
57
57
|
e('portfolio', { key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', kind: 'report' }),
|
|
58
|
-
e('portfolio', { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', kind: 'derived' }),
|
|
59
|
-
e('portfolio', { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', kind: 'derived' }),
|
|
58
|
+
e('portfolio', { key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', kind: 'derived', store: true }),
|
|
59
|
+
e('portfolio', { key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', kind: 'derived', store: true }),
|
|
60
60
|
e('portfolio', { key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', kind: 'doc', days: 30, rollup: 'risks', isNew: true }),
|
|
61
61
|
e('portfolio', { key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', kind: 'doc', days: 91, rollup: 'outcome', isNew: true }),
|
|
62
|
-
e('portfolio', { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', kind: 'derived' }),
|
|
62
|
+
e('portfolio', { key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', kind: 'derived', store: true }),
|
|
63
63
|
]),
|
|
64
64
|
board: Object.freeze([
|
|
65
65
|
e('board', { key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', kind: 'doc', isNew: true }),
|
|
@@ -120,7 +120,8 @@ const levelOfScope = (scope) => { const p = parseScope(scope); return p ? p.leve
|
|
|
120
120
|
const scopeDir = (scope) => String(scope).replace(':', '-');
|
|
121
121
|
/** A doc entry PMOS may draft: kind doc, and not one the PM writes. */
|
|
122
122
|
const draftable = (entry) => !!entry && entry.kind === 'doc' && entry.by !== 'you';
|
|
123
|
-
/** A derived entry with `store` (the OKRs) is read from its own source, and PMOS may propose
|
|
123
|
+
/** A derived entry with `store` (the OKRs, the decision log, kill conditions, market context) is read from its own source, and PMOS may propose
|
|
124
|
+
* it as a core document while that source is absent. */
|
|
124
125
|
const storable = (entry) => !!entry && (entry.kind === 'doc' || !!entry.store);
|
|
125
126
|
/** A derived entry that fills as the work goes: with no source yet it is "none", not missing (portfolio and board only). */
|
|
126
127
|
const FILLS = Object.freeze(['files', 'watches', 'keeps']);
|
|
@@ -573,7 +574,7 @@ function actOf(entry, st) {
|
|
|
573
574
|
if (st === 'stale' && draftable(entry)) return { label: 'Draft it', do: 'draft' };
|
|
574
575
|
if (st !== 'missing') return null;
|
|
575
576
|
if (draftable(entry)) return { label: 'Draft it', do: 'draft' };
|
|
576
|
-
if (entry.store && entry.by !== 'you') return { label: 'Propose them', do: 'draft' };
|
|
577
|
+
if (entry.store && entry.by !== 'you') return { label: entry.key === 'okrs' ? 'Propose them' : 'Propose it', do: 'draft' };
|
|
577
578
|
if (entry.key === 'scan') return { label: 'Run a scan', go: 'scan' };
|
|
578
579
|
if (entry.kind === 'report' && entry.level !== 'initiative') return { label: 'Set it up', go: 'reports' };
|
|
579
580
|
return null;
|
|
@@ -623,8 +624,8 @@ function driverLists(set) {
|
|
|
623
624
|
// ── the core-doc run's prompt ────────────────────────────────────────────────────────────────────────
|
|
624
625
|
const S = '<state>'; // the state folder, named in the prompt as the run sees it
|
|
625
626
|
const GUIDE = Object.freeze({
|
|
626
|
-
strategy: { sources: ['The issued product strategy, if any, under <state>/core-docs/portfolio/strategy/', 'planning/okrs/ (the key results and health budgets)', 'The board charters under <state>/core-docs/board-*/charter/'],
|
|
627
|
-
sections: ['Where we are going', 'Where we play (one line per pillar, with its boards)', 'What we will not do', 'How we will know (a table: Pillar, Key results, Boards)'] },
|
|
627
|
+
strategy: { sources: ['The issued product strategy, if any, under <state>/core-docs/portfolio/strategy/', 'planning/okrs/ (the key results and health budgets)', 'The board charters under <state>/core-docs/board-*/charter/', 'The boards and their initiatives with their anchors (`pmos board`, `pmos initiative list`)', 'The Horizon scan reports and the research under planning/radar/ and planning/research/, if any'],
|
|
628
|
+
sections: ['A short line: this is a proposal for the PM to change, keep or drop; the PM issues the strategy', 'Where we are going', 'Where we play (one line per pillar, with its boards)', 'What we will not do', 'How we will know (a table: Pillar, Key results, Boards)'] },
|
|
628
629
|
roadmap: { sources: ['`pmos initiative list` (every initiative with its horizon, stage and anchor)', 'planning/okrs/ (the key results the bets serve)', 'The issued portfolio roadmap before this one, if any'],
|
|
629
630
|
sections: ['Now (each bet, its board and the key result it serves)', 'Next', 'Later', 'What changed since the last roadmap'] },
|
|
630
631
|
invest: { sources: ['`pmos initiative list` (anchors: key result or health budget)', 'The plan items and their points on every board (`pmos board`)', 'The people and their points a week (`pmos people list`)', 'The Jira snapshots under the state folder, if any (read-only)'],
|
|
@@ -647,6 +648,12 @@ const GUIDE = Object.freeze({
|
|
|
647
648
|
sections: ['Patterns (each with the initiatives it comes from)', 'Keep, change, try (a table)'] },
|
|
648
649
|
okrs: { sources: ['The product strategy, if one is issued: <state>/core-docs/portfolio/strategy/', 'The boards and their initiatives with their anchors (`pmos board`)', 'planning/okrs/health-budgets.md, if any', 'The KR metric records and their readings (planning/records/kr-metrics.jsonl, `pmos measure`), if any'],
|
|
649
650
|
sections: ['A short line: these are proposals for the PM to change, keep or drop', 'Objectives, each with two to four key results (a table: Key result, Metric, Now, Target, Initiatives that serve it); write "not known" where no reading exists', 'Health budgets for upkeep work (a table: Budget, What it covers, Limit)', 'Initiatives with no key result to anchor to, if any'] },
|
|
651
|
+
decisions: { sources: ['Meeting notes and their summaries in the state folder (decisions, agreed, chose, will not)', 'The initiative records and their stage moves, holds, cancels and merges (`pmos initiative list`)', 'The proposals the PM kept or dropped (the driver ledger), if any', 'The issued decision log before this one, if any: <state>/core-docs/portfolio/decisions/'],
|
|
652
|
+
sections: ['A short line: these are decisions found in the sources, for the PM to keep, correct or drop', 'A table: Date, Decision, Why, Boards, Who decided, Source', 'Open questions that look like decisions still to make'] },
|
|
653
|
+
kill: { sources: ['Every initiative with its bet, its anchor and its kill conditions, if any (`pmos initiative list`, the stage tools)', 'planning/okrs/ and the key result readings (`pmos measure` records), if any', 'The issued kill conditions before these, if any: <state>/core-docs/portfolio/kill/'],
|
|
654
|
+
sections: ['A short line: these are proposed kill conditions for the PM to change, keep or drop; the PM writes them onto each initiative', 'A table: Initiative, Bet, Key result, Kill condition (a number and a date), Reading now', 'Bets with no key result to measure, if any'] },
|
|
655
|
+
context: { sources: ['The research under planning/research/ and the Horizon scan reports under planning/radar/, if any', 'Meeting notes that name users, customers, competitors or the market', 'The problem statements and research notes of the initiatives (the stage tools)', 'The issued market and user context before this one, if any: <state>/core-docs/portfolio/context/'],
|
|
656
|
+
sections: ['A short line: this is a proposal built from what PMOS has read, for the PM to change, keep or drop', 'Jobs to be done (a table: Who, Job, Evidence)', 'The market map (a table: Player, What they offer, Where we differ)', 'What we do not know yet'] },
|
|
650
657
|
risks: { sources: ['The initiative record and every file it names (PRD, design, rubric)', 'The Jira snapshot of the initiative, if any (blocked and late tickets)', 'Meeting notes and requirement notes that name the initiative'],
|
|
651
658
|
sections: ['A table: Risk, Impact (High, Medium, Low), Likelihood, Next step, Owner, Since', 'Waits on other boards, if any'] },
|
|
652
659
|
outcome: { sources: ['The key result readings since the release (`pmos measure` records)', 'The release that shipped it (the releases ledger)', 'The initiative bet, its kill conditions and its learnings (the stage tools)', 'Support tickets and notes that name it, if any'],
|
|
@@ -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.226.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.226.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -19,17 +19,17 @@ const RHYTHM = { portfolio: 'each quarter, with a monthly pulse', board: 'every
|
|
|
19
19
|
|
|
20
20
|
/* the core sets: key, title, ic, for, reader, cadence, by, kind (doc | live | report), cat, isNew, state, when, due, flag, home */
|
|
21
21
|
const PF = [
|
|
22
|
-
{ key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: '
|
|
22
|
+
{ key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'proposes', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
|
|
23
23
|
{ key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12, kind: 'derived', store: true },
|
|
24
24
|
{ key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', st: 'draft', w: 0, due: 4 },
|
|
25
25
|
{ key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'draft', w: 0, due: 4, isNew: true },
|
|
26
26
|
{ key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', st: 'ok', w: -1, kind: 'report' },
|
|
27
27
|
{ key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', st: 'due', w: -10, due: 4, kind: 'report' },
|
|
28
|
-
{ key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', st: 'ok', w: -2 },
|
|
29
|
-
{ key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', st: 'ok', w: -30, flag: '1 tripped' },
|
|
28
|
+
{ key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', store: true, st: 'ok', w: -2 },
|
|
29
|
+
{ key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', store: true, st: 'ok', w: -30, flag: '1 tripped' },
|
|
30
30
|
{ key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', st: 'ok', w: -3, isNew: true },
|
|
31
31
|
{ key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
|
|
32
|
-
{ key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', st: 'ok', w: -20 },
|
|
32
|
+
{ key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', store: true, st: 'ok', w: -20 },
|
|
33
33
|
];
|
|
34
34
|
const BD = [
|
|
35
35
|
{ key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', st: 'ok', w: -15, isNew: true },
|
|
@@ -104,7 +104,7 @@ function actOf(e, stt) {
|
|
|
104
104
|
const kind = e.kind || 'doc';
|
|
105
105
|
if (kind === 'doc' && e.by !== 'you' && (stt === 'missing' || stt === 'stale')) return { label: 'Draft it', do: 'draft' };
|
|
106
106
|
if (stt !== 'missing') return null;
|
|
107
|
-
if (e.store && e.by !== 'you') return { label: 'Propose them', do: 'draft' };
|
|
107
|
+
if (e.store && e.by !== 'you') return { label: e.key === 'okrs' ? 'Propose them' : 'Propose it', do: 'draft' };
|
|
108
108
|
if (e.key === 'scan') return { label: 'Run a scan', go: 'scan' };
|
|
109
109
|
if (kind === 'report' && !e.cat) return { label: 'Set it up', go: 'reports' };
|
|
110
110
|
return null;
|
|
@@ -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
|
}
|