create-pmos 0.129.0 → 0.131.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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-one-home-test.js +8 -3
  7. package/workspace/c3-work-test.js +200 -0
  8. package/workspace/fixtures/one-home-baseline.json +0 -91
  9. package/workspace/guide/briefing.md +60 -0
  10. package/workspace/guide/calendar-popup.md +46 -0
  11. package/workspace/guide/console-calendar.md +65 -0
  12. package/workspace/guide/index.md +4 -0
  13. package/workspace/guide/layout.md +1 -0
  14. package/workspace/guide/my-work.md +76 -1
  15. package/workspace/lib/calendar-layers.js +372 -0
  16. package/workspace/lib/pm-items.js +71 -6
  17. package/workspace/lib/report-needs.js +1 -1
  18. package/workspace/lib/route-modules.js +1 -0
  19. package/workspace/lib/routes-brief-page.js +187 -0
  20. package/workspace/lib/routes-calendar-views.js +112 -3
  21. package/workspace/lib/routes-pm-items.js +123 -13
  22. package/workspace/lib/work-focus.js +106 -0
  23. package/workspace/package-lock.json +2 -2
  24. package/workspace/package.json +1 -1
  25. package/workspace/public/pm/c3-brief-data.js +11 -0
  26. package/workspace/public/pm/c3-brief-needs.js +145 -0
  27. package/workspace/public/pm/c3-brief-pulse.js +70 -0
  28. package/workspace/public/pm/c3-brief-today.js +44 -0
  29. package/workspace/public/pm/c3-calendar.js +392 -0
  30. package/workspace/public/pm/c3-calpop.js +103 -0
  31. package/workspace/public/pm/c3-demo-brief-page.js +54 -0
  32. package/workspace/public/pm/c3-demo-cal4.js +114 -0
  33. package/workspace/public/pm/c3-demo-data.js +79 -77
  34. package/workspace/public/pm/c3-demo-misc2.js +4 -16
  35. package/workspace/public/pm/c3-demo-work-ix.js +19 -67
  36. package/workspace/public/pm/c3-demo.js +11 -4
  37. package/workspace/public/pm/c3-shell.js +27 -149
  38. package/workspace/public/pm/calendar-views.js +11 -3
  39. package/workspace/public/pm/components.css +1 -0
  40. package/workspace/public/pm/console3.css +198 -0
  41. package/workspace/public/pm/pm-work-view.js +364 -172
  42. package/workspace/public/pm/work-place.js +58 -0
  43. package/workspace/test-files.json +7 -1
  44. package/workspace/test-lists/calendar-layers.json +5 -0
  45. package/workspace/test-lists/calendar-time-page.json +5 -0
  46. package/workspace/test-lists/calpop.json +4 -0
  47. package/workspace/test-lists/console3-work-page.json +9 -0
  48. package/workspace/public/pm/c3-demo-work2.js +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.129.0",
3
+ "version": "0.131.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@64a9920
1
+ kernel-source: pmos@1d6493f
2
2
  assembled: 2026-10-11
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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:54:27Z"}
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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:54:27Z"}
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-11T03:14:46Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T03:54:27Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -49,6 +49,7 @@ function collect(tab) {
49
49
  // row (Work's rows have no buttons; a swipe or an arrow key is the action).
50
50
  for (const ctl of pane.querySelectorAll('button, [data-a], [role="button"], [data-swipe], [data-row][tabindex]')) {
51
51
  if (!visible(ctl) || ctl.disabled) continue;
52
+ if (ctl.closest('[data-ref-row]')) continue; // a reference row (Briefing Today, Portfolio pulse) opens or explains; it holds no action on a record (pm-brief-page-test.js checks that)
52
53
  const item = ctl.closest(ITEM);
53
54
  if (!item || !pane.contains(item)) continue;
54
55
  const t = item.querySelector('[data-pm-title], .tt, h2, b, strong, .t, [data-title], h3, h4');
@@ -56,7 +57,9 @@ function collect(tab) {
56
57
  if (title.length < 4) continue;
57
58
  const id = item.getAttribute('data-id') || item.getAttribute('data-needs-row') || item.getAttribute('data-due-row') || item.getAttribute('data-draft-row') || '';
58
59
  const label = ctl === item ? 'row' : norm(ctl.getAttribute('aria-label') || ctl.textContent).slice(0, 40);
59
- if (!out.has(title)) out.set(title, { title, id, controls: [] });
60
+ const ref = !!ctl.closest('[data-ref-row]'); // a reference row (Needs you rows for the PM's own items, Reports references) acts on the item's home, so it is not a second home
61
+ if (!out.has(title)) out.set(title, { title, id, controls: [], ref: true });
62
+ if (!ref) out.get(title).ref = false;
60
63
  if (!out.get(title).controls.includes(label)) out.get(title).controls.push(label);
61
64
  }
62
65
  return [...out.values()];
@@ -95,12 +98,14 @@ function collect(tab) {
95
98
  last = n;
96
99
  }
97
100
  byTab[tab] = items || [];
98
- ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
101
+ // Reports (since the dashboards) draws widgets and charts, not items: it may have none, and then it cannot hold a second home.
102
+ if (tab === 'rep') ok('the rep tab holds no item with an action control, or only widgets', true);
103
+ else ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
99
104
  }
100
105
 
101
106
  // Every title that has an action control on two or more tabs.
102
107
  const where = new Map();
103
- for (const tab of TABS) for (const it of byTab[tab]) {
108
+ for (const tab of TABS) for (const it of byTab[tab].filter((x) => !x.ref)) {
104
109
  if (!where.has(it.title)) where.set(it.title, []);
105
110
  where.get(it.title).push({ tab, controls: it.controls });
106
111
  }
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-work-test.js — the console3 Work page (initiative console3-work-page; planning/prd/console3-work-page-prd.md R1 to R10, rubric
4
+ * dimensions 1 to 5 and 7). Loads /c3.html?demo=1 at 1440 wide and then at 390 wide. Demo mode answers every /api call in the page,
5
+ * so nothing reaches the server and the fixture (public/pm/c3-demo-data.js pmItems) is placed by the same rule the server uses.
6
+ *
7
+ * views six views in order (Inbox, Today, Upcoming, Waiting, Someday, Review), each with its count; no Triage, Week, Later
8
+ * lens and no Flow today card
9
+ * one home none of the decisions and approvals whose home is Briefing · Needs you is a Work row or Work text
10
+ * Focus Today opens with up to three suggestions, each with the reason; Accept pins, Not today hides, Edit title saves, Unpin
11
+ * offline an item done outside PMOS shows "outside PMOS" and the person; Looks done? has Confirm (with Undo); Not yet moves it
12
+ * to a later day; Done takes a one-line outcome
13
+ * Upcoming the next 7 days by day, then Later; a deferred item says "Not before …"
14
+ * Review one at a time with progress: Keep, Change (the edit fields), Drop, then "Review done"
15
+ * capture the preview chip names the view the line lands in, by the server's rule, and never Triage
16
+ * phone at 390 wide every view draws with no horizontal scroll
17
+ * The page must raise no script error. Every wait polls the page; no fixed sleep stands in for a state.
18
+ */
19
+ const path = require('path');
20
+ const { spawn } = require('child_process');
21
+ const { findBrowser } = require('./lib/find-browser');
22
+ const T = require('./test-run').configure({ name: 'c3-work' });
23
+
24
+ const BASE = T.BASE;
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
+ const until = async (fn, ms = 10000, step = 120) => {
27
+ const t0 = Date.now();
28
+ for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); }
29
+ };
30
+ const results = [];
31
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
32
+ let srv = null; let browser = null;
33
+ const finish = async (code) => {
34
+ if (browser) await browser.close().catch(() => {});
35
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
36
+ T.killTmux();
37
+ const bad = results.filter((x) => !x.cond).length;
38
+ console.log(`\n${results.length - bad}/${results.length} c3-work assertions passed`);
39
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
40
+ };
41
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
42
+
43
+ const VIEWS = ['inbox', 'today', 'upcoming', 'waiting', 'someday', 'review'];
44
+ const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review'];
45
+ /* The decisions and approvals the demo used to list in Work (PRD R9): their home is Briefing · Needs you. */
46
+ const DECISIONS = ['Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast', 'Say yes to Sam: ship the logging change',
47
+ 'Self-serve onboarding: continue or park?', 'Pick interview slots with 3 customers', 'Assign owners to 2 unowned workshop actions',
48
+ 'Confirm the 1 Oct numbers before the 10:30 update', 'Accept the 15 Oct interview invite', 'Review the 13:00 interview guide',
49
+ 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', 'Reply to Marta: SSO 14 Oct, SCIM in Nov', 'Weekly status for leadership', 'Prep note for 10:30 Checkout sync'];
50
+ const W = '[data-pm-work]';
51
+
52
+ (async () => {
53
+ const exe = findBrowser();
54
+ if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-work suite cannot run'); return finish(1); }
55
+ let chromium;
56
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-work suite cannot run'); return finish(1); }
57
+
58
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
59
+ await T.waitForServer(BASE);
60
+ browser = await chromium.launch({ executablePath: exe });
61
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
62
+ const page = await ctx.newPage();
63
+ const errors = [];
64
+ page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
65
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
66
+ const st = (f, a) => page.evaluate(f, a);
67
+ const click = (sel) => st((s) => { const b = document.querySelector(s); if (b) b.click(); return !!b; }, sel);
68
+ const view = async (id) => { await click(W + ' [data-lens="' + id + '"]'); return until(() => st(([w, v]) => { const h = document.querySelector(w + ' [data-pm-lens-head]'); return !!h && h.getAttribute('data-pm-lens-head') === v; }, [W, id]), 8000); };
69
+ const titles = () => st((w) => [...document.querySelectorAll(w + ' [data-pm-item] [data-pm-title]')].map((n) => n.textContent), W);
70
+ const counts = () => st((w) => Object.fromEntries([...document.querySelectorAll(w + ' [data-pm-count]')].map((n) => [n.getAttribute('data-pm-count'), Number(n.textContent)])), W);
71
+ const rowSel = (title) => st(([w, t]) => { const n = [...document.querySelectorAll(w + ' [data-pm-item]')].find((r) => (r.querySelector('[data-pm-title]') || {}).textContent === t); return n ? '[data-pm-item="' + n.getAttribute('data-pm-item') + '"]' : null; }, [W, title]);
72
+ const open = async (title) => {
73
+ const s = await rowSel(title); if (!s) return null;
74
+ await click(W + ' ' + s + ' [data-pm-title]');
75
+ return (await until(() => st((q) => { const c = document.querySelector(q + ' [data-pm-case]'); return !!c && !!c.closest('.xp.open'); }, W + ' ' + s), 4000)) ? s : null;
76
+ };
77
+ const toastUndo = () => until(() => click('[data-c3-toast-action]'), 4000);
78
+
79
+ await page.goto(BASE + '/c3.html?demo=1&tab=work');
80
+ const up = await until(() => st((w) => !!document.querySelector('[data-c3-demo="1"]') && !!document.querySelector(w + ' [data-pm-lenses]') && !!document.querySelector(w + ' [data-pm-lens-head]'), W), 45000, 250);
81
+ if (!up) { await click('[data-c3-tab="work"]'); }
82
+ const ready = up || await until(() => st((w) => !!document.querySelector(w + ' [data-pm-lens-head]'), W), 20000, 250);
83
+ ok('the console3 page comes up in demo mode with the Work tab', !!ready);
84
+ if (!ready) return finish(1);
85
+
86
+ /* the six views and their counts (R1) */
87
+ const bar = await st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].map((b) => [b.getAttribute('data-lens'), b.textContent.replace(/\d+$/, '').trim()]), W);
88
+ ok('the bar has the six views in order: Inbox, Today, Upcoming, Waiting, Someday, Review', JSON.stringify(bar.map((b) => b[0])) === JSON.stringify(VIEWS) && JSON.stringify(bar.map((b) => b[1])) === JSON.stringify(LABELS), JSON.stringify(bar));
89
+ const c0 = await counts();
90
+ ok('each view shows its count (3, 5, 6, 2, 2, 3)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3]', JSON.stringify(c0));
91
+ ok('the page opens on Today', await st((w) => (document.querySelector(w + ' [data-pm-lens-head]') || {}).getAttribute && document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head') === 'today', W));
92
+ const text0 = await st((w) => document.querySelector(w).textContent, W);
93
+ ok('no Triage, Week or Later view and no Flow today card', !/Triage|Flow today/.test(text0) && !(await st((w) => !!document.querySelector(w + ' [data-pm-flow], ' + w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)), (text0.match(/.{0,40}(Triage|Flow today).{0,40}/) || [''])[0]);
94
+
95
+ /* one home (R9): no decision is a Work row in any view, nor Work text */
96
+ const seen = new Set(); let allText = '';
97
+ for (const v of VIEWS) { await view(v); (await titles()).forEach((t) => seen.add(t)); allText += '\n' + await st((w) => document.querySelector(w).textContent, W); }
98
+ const leaked = DECISIONS.filter((t) => seen.has(t) || allText.includes(t));
99
+ ok('no decision or approval whose home is Briefing · Needs you shows on the Work page', leaked.length === 0, leaked.join(' | '));
100
+ ok('the Work page lists the PM\'s own work in every view', seen.size >= 14, String(seen.size));
101
+
102
+ /* Today and Focus (R4) */
103
+ await view('today');
104
+ const today0 = await titles();
105
+ ok('Today lists overdue first, then flagged, then due today', today0[0] === 'Call Priya about the Q4 budget variance' && today0[1] === 'Send the revised Checkout v2 timeline to Dara' && today0[today0.length - 1] === 'Update the Checkout v2 risk log', JSON.stringify(today0));
106
+ const foc = await st((w) => [...document.querySelectorAll(w + ' [data-pm-focus] [data-pm-focus-card]')].map((c) => ({ key: c.getAttribute('data-pm-focus-card'), state: c.getAttribute('data-pm-focus-state'), why: (c.querySelector('.wkfc-why') || {}).textContent || '' })), W);
107
+ ok('Today opens with Focus: three suggested jobs, each with the reason', foc.length === 3 && foc.every((f) => f.state === 'suggested' && /^Why: \S/.test(f.why)), JSON.stringify(foc));
108
+ const focBefore = await st((w) => { const f = document.querySelector(w + ' [data-pm-focus]'); const l = document.querySelector(w + ' [data-pm-list="today"]'); return !!f && !!l && !!(f.compareDocumentPosition(l) & Node.DOCUMENT_POSITION_FOLLOWING); }, W);
109
+ ok('…at the top of Today, above the list', focBefore);
110
+ const [k1, k2, k3] = foc.map((f) => f.key);
111
+ await click(W + ' [data-pm-focus-accept="' + k1 + '"]');
112
+ ok('Accept pins the job to Today', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'accepted'; }, [W, k1]), 5000));
113
+ await click(W + ' [data-pm-focus-dismiss="' + k2 + '"]');
114
+ ok('Not today takes a suggestion away', !!await until(() => st(([w, k]) => !document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'), [W, k2]), 5000));
115
+ await click(W + ' [data-pm-focus-edit="' + k3 + '"]');
116
+ const inp = await until(() => st(([w, k]) => !!document.querySelector(w + ' [data-pm-focus-input="' + k + '"]'), [W, k3]), 4000);
117
+ if (inp) await page.fill(W + ' [data-pm-focus-input="' + k3 + '"]', 'Prep the search one-pager before lunch');
118
+ await click(W + ' [data-pm-focus-save="' + k3 + '"]');
119
+ const edited = await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'accepted' ? (c.querySelector('[data-pm-focus-title]') || {}).textContent : null; }, [W, k3]), 5000);
120
+ ok('Edit title saves the new title and pins the job', edited === 'Prep the search one-pager before lunch', String(edited));
121
+ await click(W + ' [data-pm-focus-clear="' + k1 + '"]');
122
+ ok('Unpin takes a pinned job back to a suggestion', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'suggested'; }, [W, k1]), 5000));
123
+ const focText = await st((w) => document.querySelector(w + ' [data-pm-focus]').textContent, W);
124
+ ok('Focus copy has no em-dash', !/—/.test(focText));
125
+
126
+ /* offline work (R6) */
127
+ const qa = await open('Talk to Dara about QA capacity for the beta');
128
+ ok('an offline row says "outside PMOS" and names the person', !!qa && await st((s) => { const r = document.querySelector(s); return !!r.querySelector('[data-pm-offline-tag]') && /outside PMOS/.test(r.textContent) && /Dara/.test(r.querySelector('.chips').textContent); }, W + ' ' + qa));
129
+ ok('…its case file offers Done with an outcome, Not yet (later today, tomorrow, next week) and Drop', !!qa && await st((s) => { const b = document.querySelector(s + ' [data-pm-offline]'); return !!b && !!b.querySelector('[data-pm-outcome]') && !!b.querySelector('[data-pm-offline-done]') && ['later-today', 'tomorrow', 'next-week'].every((w) => b.querySelector('[data-pm-notyet="' + w + '"]')) && !!b.querySelector('[data-pm-drop]'); }, W + ' ' + qa));
130
+ ok('…and "Looks done?" with a Confirm button; it is not done until the PM confirms', !!qa && await st((s) => { const e = document.querySelector(s + ' [data-pm-evidence]'); return !!e && /Looks done\?/.test(e.textContent) && !!e.querySelector('[data-pm-confirm]'); }, W + ' ' + qa) && (await titles()).includes('Talk to Dara about QA capacity for the beta'));
131
+ ok('…with the shared actions Flag, Not before, Someday and Waiting', !!qa && await st((s) => ['[data-pm-flag]', '[data-pm-defer]', '[data-pm-someday]', '[data-pm-wait-on]'].every((q) => document.querySelector(s + ' ' + q)), W + ' ' + qa));
132
+ await click(W + ' ' + qa + ' [data-pm-confirm]');
133
+ ok('Confirm closes it: the row leaves Today and the count follows', !!await until(async () => !(await titles()).includes('Talk to Dara about QA capacity for the beta') && (await counts()).today === 4, 5000), JSON.stringify(await counts()));
134
+ await toastUndo();
135
+ ok('…and Undo brings it back', !!await until(async () => (await titles()).includes('Talk to Dara about QA capacity for the beta') && (await counts()).today === 5, 5000));
136
+ const call = await open('Call Priya about the Q4 budget variance');
137
+ ok('an item moved before says how often', !!call && /Moved once/.test(await st((s) => document.querySelector(s).textContent, W + ' ' + call)));
138
+ if (call) await click(W + ' ' + call + ' [data-pm-notyet="tomorrow"]');
139
+ ok('Not yet, tomorrow: it leaves Today and Upcoming gains it', !!await until(async () => { const c = await counts(); return !(await titles()).includes('Call Priya about the Q4 budget variance') && c.today === 4 && c.upcoming === 7; }, 5000), JSON.stringify(await counts()));
140
+
141
+ /* Upcoming (R5) */
142
+ await view('upcoming');
143
+ const days = await st((w) => [...document.querySelectorAll(w + ' [data-pm-day]')].map((d) => ({ key: d.getAttribute('data-pm-day'), head: d.querySelector('h3').textContent, rows: [...d.querySelectorAll('[data-pm-title]')].map((t) => t.textContent) })), W);
144
+ ok('Upcoming groups the next 7 days by day, then Later', days.length === 8 && days[0].key === '2026-10-09' && /^Tomorrow/.test(days[0].head) && days[7].key === 'later' && /^Later/.test(days[7].head), JSON.stringify(days.map((d) => d.key + ':' + d.head)));
145
+ ok('…each item under the day it arrives', days.length === 8 && days[0].rows.includes('Set up a meeting with Sam about the logging rollout') && days[0].rows.includes('Call Priya about the Q4 budget variance')
146
+ && days.find((d) => d.key === '2026-10-12').rows.includes('Write release notes for SSO 1.0') && days[7].rows.includes('Write the Q1 discovery plan'), JSON.stringify(days.map((d) => d.rows)));
147
+ const nb = await st((w) => { const s = [...document.querySelectorAll(w + ' [data-pm-item]')].find((r) => /Write release notes for SSO 1\.0/.test(r.textContent)); const c = s && s.querySelector('[data-pm-notbefore]'); return c ? c.textContent : null; }, W);
148
+ ok('a deferred item says "Not before" its date', nb === 'Not before Mon 12 Oct', String(nb));
149
+ const adv = await st((w) => [...document.querySelectorAll(w + ' [data-pm-resolves]')].map((n) => n.textContent), W);
150
+ ok('an item that advances a team task names it ("Advances: <title>", or the id when the title is unknown)', adv.includes('Advances: SSO general release checklist') && adv.includes('Advances: returns-interview-5'), JSON.stringify(adv));
151
+ const sam = await open('Set up a meeting with Sam about the logging rollout');
152
+ if (sam) { await page.fill(W + ' ' + sam + ' [data-pm-outcome]', 'Booked for Tuesday 10:00'); await click(W + ' ' + sam + ' [data-pm-offline-done]'); }
153
+ ok('Done with a one-line outcome closes an offline item', !!sam && !!await until(async () => !(await titles()).includes('Set up a meeting with Sam about the logging rollout') && (await counts()).upcoming === 6, 5000));
154
+
155
+ /* the shared actions: Flag from the Inbox (R2) */
156
+ await view('inbox');
157
+ const tear = await open('Read the competitor pricing teardown');
158
+ if (tear) await click(W + ' ' + tear + ' [data-pm-flag]');
159
+ ok('Flag moves an Inbox item to Today', !!tear && !!await until(async () => { const c = await counts(); return c.inbox === 2 && c.today === 5; }, 5000), JSON.stringify(await counts()));
160
+
161
+ /* Review (R7) */
162
+ await view('review');
163
+ const prog = () => st((w) => { const r = document.querySelector(w + ' [data-pm-review]'); return r ? { id: r.getAttribute('data-pm-review'), p: r.querySelector('[data-pm-review-progress]').textContent, t: r.querySelector('[data-pm-title]').textContent } : null; }, W);
164
+ const r1 = await until(prog, 5000);
165
+ ok('Review shows one item at a time, the most stale first, with progress', !!r1 && r1.p === '1 of 3' && r1.t === 'Clean up the Checkout Jira labels', JSON.stringify(r1));
166
+ if (r1) await click(W + ' [data-pm-review-keep="' + r1.id + '"]');
167
+ const r2 = await until(async () => { const p = await prog(); return p && p.p === '2 of 3' ? p : null; }, 5000);
168
+ ok('Keep moves on to the next item', !!r2 && r2.t === 'Idea: weekly digest for partner admins', JSON.stringify(r2));
169
+ if (r2) await click(W + ' [data-pm-review-change="' + r2.id + '"]');
170
+ ok('Change opens the item\'s edit fields', !!r2 && !!await until(() => st((id) => !!document.querySelector('[data-pm-review] [data-pm-edit-form="' + id + '"]') && !!document.querySelector('[data-pm-review] [data-pm-edit="' + id + ':defer"]'), r2.id), 4000));
171
+ if (r2) await click(W + ' [data-pm-review-next="' + r2.id + '"]');
172
+ const r3 = await until(async () => { const p = await prog(); return p && p.p === '3 of 3' ? p : null; }, 5000);
173
+ ok('…and Done changing moves on', !!r3 && r3.t === 'Annual-plan label', JSON.stringify(r3));
174
+ if (r3) await click(W + ' [data-pm-review-drop="' + r3.id + '"]');
175
+ ok('Drop ends the pass with "Review done"', !!await until(() => st((w) => { const d = document.querySelector(w + ' [data-pm-review-done]'); return !!d && /Review done/.test(d.textContent); }, W), 5000));
176
+
177
+ /* capture (R2): the chip names the view the server will place the line in */
178
+ const chip = async (line) => { await page.fill(W + ' [data-pm-add]', line); return until(() => st((w) => { const t = document.querySelector(w + ' [data-pm-chips] [data-pm-lands]'); return t ? t.textContent : null; }, W), 3000); };
179
+ const lands = [await chip('Read the security brief'), await chip('Send Dana the new date by tomorrow'), await chip('Ship the fix due today'), await chip('Get the contract back waiting on Lee')];
180
+ ok('the capture chip says where a line lands: Inbox, Upcoming, Today, Waiting', JSON.stringify(lands) === '["Inbox","Upcoming","Today","Waiting"]', JSON.stringify(lands));
181
+ ok('…and never says Triage', !/Triage/.test(await st((w) => document.querySelector(w + ' .c3add').textContent, W)));
182
+ await page.fill(W + ' [data-pm-add]', 'Read the security brief');
183
+ await page.press(W + ' [data-pm-add]', 'Enter');
184
+ ok('a captured line lands in Inbox and the page shows it there', !!await until(async () => (await st((w) => document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head'), W)) === 'inbox' && (await titles()).includes('Read the security brief'), 6000));
185
+
186
+ /* phone width: every view, no horizontal scroll */
187
+ await page.setViewportSize({ width: 390, height: 844 });
188
+ const wide = [];
189
+ for (const v of VIEWS) {
190
+ await view(v);
191
+ await sleep(150); // a layout settle before a measurement that must NOT find overflow
192
+ const w = await st(() => Math.max(document.documentElement.scrollWidth, document.body.scrollWidth));
193
+ if (w > 391) wide.push(v + ':' + w);
194
+ }
195
+ ok('at 390 wide no view scrolls sideways', wide.length === 0, wide.join(' '));
196
+ const tgt = await st((w) => [...document.querySelectorAll(w + ' [data-pm-lenses] [data-lens]')].every((b) => b.getBoundingClientRect().height >= 44), W);
197
+ ok('…and the view buttons are at least 44px tall', tgt);
198
+ ok('the page raised no script error', errors.length === 0, errors.slice(0, 3).join(' | '));
199
+ return finish();
200
+ })();
@@ -1,97 +1,6 @@
1
1
  {
2
2
  "_comment": "Items the console3 page makes actionable on more than one tab today (c3-one-home-test.js). Known-failing until the Phase 2 restructure of the console3 feedback plan; the suite fails only on a duplicate that is not listed here. Remove an entry when its duplicate is gone.",
3
3
  "duplicates": [
4
- {
5
- "title": "accept the 15 oct interview invite",
6
- "tabs": [
7
- "brief",
8
- "work"
9
- ]
10
- },
11
- {
12
- "title": "approve pricing-page copy before handoff",
13
- "tabs": [
14
- "brief",
15
- "work"
16
- ]
17
- },
18
- {
19
- "title": "assign owners to 2 unowned workshop actions",
20
- "tabs": [
21
- "brief",
22
- "work"
23
- ]
24
- },
25
- {
26
- "title": "confirm the 1 oct numbers before the 10:30 update",
27
- "tabs": [
28
- "brief",
29
- "work"
30
- ]
31
- },
32
- {
33
- "title": "confirm the growth assumption in the q4 forecast",
34
- "tabs": [
35
- "brief",
36
- "work"
37
- ]
38
- },
39
- {
40
- "title": "cut to 3 flows and hold 24 oct",
41
- "tabs": [
42
- "init",
43
- "rep"
44
- ]
45
- },
46
- {
47
- "title": "freeze scope after today's decision",
48
- "tabs": [
49
- "init",
50
- "rep"
51
- ]
52
- },
53
- {
54
- "title": "pick interview slots with 3 customers",
55
- "tabs": [
56
- "brief",
57
- "work"
58
- ]
59
- },
60
- {
61
- "title": "pull returns forward a quarter if interview 5 agrees",
62
- "tabs": [
63
- "init",
64
- "rep"
65
- ]
66
- },
67
- {
68
- "title": "review the 13:00 interview guide",
69
- "tabs": [
70
- "brief",
71
- "work"
72
- ]
73
- },
74
- {
75
- "title": "run the address-step experiment, freeze scope",
76
- "tabs": [
77
- "init",
78
- "rep"
79
- ]
80
- },
81
- {
82
- "title": "say yes to sam: ship the logging change",
83
- "tabs": [
84
- "brief",
85
- "work"
86
- ]
87
- },
88
- {
89
- "title": "self-serve onboarding: continue or park?",
90
- "tabs": [
91
- "brief",
92
- "work"
93
- ]
94
- },
95
4
  {
96
5
  "title": "sso is live with 3 customers",
97
6
  "tabs": [
@@ -0,0 +1,60 @@
1
+ ---
2
+ type: Guide
3
+ title: "The Briefing: today's brief"
4
+ description: "The Briefing page of the console (/c3.html): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
5
+ timestamp: 2026-10-11
6
+ ---
7
+
8
+ # The Briefing: today's brief
9
+
10
+ The Briefing is the first page of the console at `/c3.html`. It is one column. It reads from top to bottom: the headline cards, Today, Needs you and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
11
+
12
+ The Briefing shows only what PMOS already holds. When PMOS holds nothing for a section, the section says so.
13
+
14
+ ## Headline cards
15
+
16
+ The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case. The day-part buttons (Plan, Unblock, Close, Reflect) re-rank the same signals.
17
+
18
+ ## Today
19
+
20
+ Today lists the meetings of the day, in time order. Each row shows:
21
+
22
+ - The start time and the length.
23
+ - The people and the initiative of the meeting.
24
+ - **Prepared**: what PMOS holds for the meeting. This is the agent's prep note, your notes on the event, the notes you started for it, and the last notes of the same meeting. When PMOS holds none, the row says "Nothing prepared yet".
25
+ - **Before the meeting**: your open items that name the meeting.
26
+
27
+ A meeting that is over is dimmed. The meeting under way is marked **Now**. The next one is marked **Next**. When the calendar holds no meeting for today, Today says so.
28
+
29
+ A Today row is a reference. It has no button that changes anything.
30
+
31
+ ## Needs you
32
+
33
+ Needs you lists what only you can do. The rows come in this order:
34
+
35
+ 1. Your own items that are overdue. The most late is first. The row says how late.
36
+ 2. Your own items due today.
37
+ 3. Items outside PMOS that wait for you to confirm: talk to someone, meet, call, ask.
38
+ 4. Decisions that only you can make.
39
+
40
+ Press a row to open it in place. A row of your own item has three actions. Each one writes to the item itself, the same record the Work page shows.
41
+
42
+ - **Done** marks the item done. For an item outside PMOS you can first type one line about what happened. The line is optional.
43
+ - **Not yet** asks again later. Choose **Later today**, **Tomorrow** or **Next week**. The item leaves Needs you and comes back on that day.
44
+ - **Drop** removes the item. Press **Undo** in the notice to bring it back.
45
+
46
+ PMOS can see evidence that an outside item is done. For example, a meeting with the person is on the calendar. Then the row says "Looks done: a meeting with Ana is booked Tue 10:00. Confirm?" PMOS never marks the item done itself. The item changes only when you press **Done** or **Yes, it is done**.
47
+
48
+ ## Portfolio pulse
49
+
50
+ Portfolio pulse shows one bar for the status of the initiatives and one row for each initiative. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
51
+
52
+ To go to the initiative, press **Open initiative** inside the opened row.
53
+
54
+ ## Where each action lives
55
+
56
+ An item has its actions in one place only. On the Briefing, Today and Portfolio pulse show items, and do not change them. Needs you is where you decide and where you confirm your own overdue items.
57
+
58
+ ## On a phone
59
+
60
+ At 400 pixels wide the Briefing is one column with no sideways scroll. Each button in an opened row is 44 pixels high.
@@ -0,0 +1,46 @@
1
+ ---
2
+ type: Guide
3
+ title: "Calendar popup"
4
+ description: "How to open the full calendar over any page of the console with the calendar button or the C key, how to close it, and how it remembers the zoom."
5
+ timestamp: 2026-10-11
6
+ ---
7
+
8
+ # Calendar popup
9
+
10
+ The calendar popup shows the full calendar over any page of the console (`/c3.html`). You do not leave the page you are on.
11
+
12
+ ## Open it
13
+
14
+ Press the **calendar button** in the top bar. It is next to the search button. Or press **C**.
15
+
16
+ The popup is never open when the page loads.
17
+
18
+ The key **C** does nothing in these cases:
19
+
20
+ - You are typing in a field, for example the search box.
21
+ - You hold Ctrl, ⌘ or Alt.
22
+ - A sheet or the search palette is open.
23
+
24
+ ## Use it
25
+
26
+ The popup has the same five zoom levels as the Calendar tab: Day, Week, Month, Quarter and Year. It has no ribbon. Press **+** or **-** to zoom. The popup opens at the zoom you used last. The console keeps that zoom until you change it.
27
+
28
+ The popup and the Calendar tab are two separate views. A zoom or a date you choose in one view does not change the other.
29
+
30
+ ## Close it
31
+
32
+ Close the popup in one of three ways:
33
+
34
+ 1. Press **Esc**.
35
+ 2. Press the dark area around the popup.
36
+ 3. Press the close button (×) at the top right.
37
+
38
+ The focus goes back to the control that was in use before the popup opened. When you used the button, the focus goes back to the calendar button.
39
+
40
+ ## On a phone
41
+
42
+ On a phone the popup fills the screen. The page does not scroll sideways. Every button is at least 44 pixels high and wide.
43
+
44
+ ## Related
45
+
46
+ - [Layout](layout.md) lists all the keys.
@@ -0,0 +1,65 @@
1
+ ---
2
+ type: Guide
3
+ title: "Guide: the Calendar tab of the console page"
4
+ description: "The Calendar tab of the console page (/c3.html): the five zooms, the layer chips, the markers, the context of a meeting, free time and the empty states."
5
+ timestamp: 2026-10-11
6
+ ---
7
+
8
+ # Guide: the Calendar tab of the console page
9
+
10
+ The Calendar tab of the console page (`/c3.html`) shows time. It shows what happens and what falls due, by date. It shows no card deck, no task list and no strategy panel. Your due items live in Work. The strategy and progress panels live in Reports and Initiatives.
11
+
12
+ ## Zoom and move
13
+
14
+ Use **Day**, **Week**, **Month**, **Quarter** or **Year** to change the zoom. Use the arrows to go to the previous or the next period. Press **Today** to come back to today.
15
+
16
+ The keys work too. Press `+` to zoom in and `-` to zoom out. Press the left or the right arrow to move. Press `t` for today. On a touch screen, swipe sideways to move and pinch to zoom.
17
+
18
+ ## Layers
19
+
20
+ The chips under the zoom bar are the layers. Each chip shows the number of items in the range. Press a chip to hide its items, and press it again to show them.
21
+
22
+ | Chip | What it shows |
23
+ |---|---|
24
+ | Meetings | The meetings of your calendar. |
25
+ | Milestones and due dates | The promised dates of the initiatives, and the milestones and due dates of their plan items. |
26
+ | Releases | The release dates. |
27
+ | Report dates | The dates on which a report goes out. |
28
+ | My due items | The due dates of your own items, as markers. |
29
+ | Free time | The free blocks of your working day. This chip is off at the start. |
30
+
31
+ All layers are on at the start, except Free time.
32
+
33
+ ## The grid
34
+
35
+ - **Day** shows the timeline of the day. The markers of the day are in a strip above it.
36
+ - **Week** shows a timeline for each day. The markers of each day are in a strip under the day name. Press a day name to open that day.
37
+ - **Month** shows a cell for each day. A cell shows the number of meetings and up to three markers. "+2" means two more markers. Press the day number to open the day.
38
+ - **Quarter** shows a row for each week. **Year** shows a card for each month. Each shows the number of meetings and dated items, and the most important markers.
39
+
40
+ A marker has a symbol and a short title. A red edge means late or at risk of missing. An amber edge means at risk.
41
+
42
+ ## Select a marker
43
+
44
+ Press a marker to show its detail in the side column. The detail shows the title, the kind and the date. The link under it opens the item where it lives: an initiative opens in Initiatives, your item opens in Work, and a report opens in Reports. A marker has no action of its own. You act on the item in its home.
45
+
46
+ ## Select a meeting
47
+
48
+ Press a meeting to show its context in the side column. Each line has a link to its source.
49
+
50
+ - For a past meeting, **What came out of it** lists the decisions and actions of its note. **What moved after it** lists the items that changed within 48 hours and refer to the meeting or its initiative.
51
+ - For a coming meeting, **Prepared** lists what is ready, and **To prepare** lists what is still open.
52
+
53
+ When PMOS holds nothing for a meeting, the detail says so. It does not guess.
54
+
55
+ ## Free time
56
+
57
+ Press **Free time** to show the free blocks. A free block is 90 minutes or more inside your working hours. On Day and Week, each block is a hatched window on the timeline. On Month, each day shows the number of free blocks. The side column lists the blocks.
58
+
59
+ Each block can have a suggested use, for example a prep step that is at risk or the top item due that week. When nothing fits, the block says "Free, nothing specific to suggest". A suggestion is only a proposal. PMOS writes nothing to your calendar.
60
+
61
+ ## Empty states
62
+
63
+ The **In this range** list in the side column names each layer with its count. When a layer is empty, it tells you why. For example, no calendar is cached yet, or the server sent no dated items. When the range has nothing at all, the grid says so.
64
+
65
+ On a narrow screen the side column is under the grid. From 1100 pixels wide it is at the right.
@@ -22,6 +22,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
22
22
  | You want | Go to |
23
23
  |---|---|
24
24
  | What is waiting for you now | Agenda |
25
+ | Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (`/c3.html`) |
25
26
  | Every Jira ticket and WBS item assigned to you, ranked, to brief an agent | My work, or press `g w` |
26
27
  | The initiatives of a repository, or of every repository | Initiatives. Choose **All repositories** in Repository ▾ for every repository. |
27
28
  | The sessions of a repository, by initiative | The agent dock. ⌘K → **Branches and sessions** lists the branches. |
@@ -38,12 +39,15 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
38
39
 
39
40
  ## Where to read next
40
41
 
42
+ - [The Briefing](briefing.md): the console's first page: Today's meetings, Needs you (Done, Not yet, Drop) and the Portfolio pulse.
41
43
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
42
44
  - [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
43
45
  - [My work](my-work.md): the work assigned to you, its order, the brief to an agent, and `pmos my-work`.
44
46
  - [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
45
47
  - [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
48
+ - [Calendar popup](calendar-popup.md): open the full calendar over any page of the console with the calendar button or the C key, and close it.
46
49
  - [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
50
+ - [The Calendar tab of the console page](console-calendar.md): the zooms, the layer chips, the markers, the context of a meeting and free time.
47
51
  - [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
48
52
  - [Initiatives and next steps](initiatives-and-next-steps.md): the detail, the next steps, and the artifacts.
49
53
  - [Breakdown](breakdown.md): an initiative's items in the Tree and on the Board, the item sheet, Undo, Keep, Hand to agent and `pmos item`.
@@ -111,6 +111,7 @@ On the console page (`/c3.html`), text on the blue accent and on a red badge is
111
111
  | g t, g i, g b, g r, g d, g p | Show Agenda, Initiatives, Board, Reports, Docs or People. |
112
112
  | ⌘K (Ctrl-K) | Open or close the command palette. |
113
113
  | ⌘J (Ctrl-J) | Open or fold the dock. |
114
+ | C | On the console page (`/c3.html`): open the [calendar popup](calendar-popup.md). Ignored while you type or while a sheet is open. |
114
115
  | ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
115
116
  | j, k | Move down or up the rows of the place. |
116
117
  | Enter | Open the row that has the focus. |