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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.224.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@f8c3934
1
+ kernel-source: pmos@2164aa0
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: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-14T05:57:56Z"}
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-14T05:57:56Z"}
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) && /You write it/.test(t) && /new/.test(t) && /2 key results do not trace to it/.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 no OKR files exist, it also proposes the OKRs. 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. It never drafts a document that you write (the product strategy).
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
@@ -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 };
@@ -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: 'you', kind: 'doc', days: 91, isNew: true }),
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 it as a core document while that source is absent. */
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.224.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.224.0",
9
+ "version": "0.226.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.226.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",
@@ -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: 'you', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
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 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
  }