create-pmos 0.130.0 → 0.132.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 (51) 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 +4 -2
  7. package/workspace/c3-work-test.js +200 -0
  8. package/workspace/fixtures/one-home-baseline.json +0 -91
  9. package/workspace/guide/console-calendar.md +65 -0
  10. package/workspace/guide/data-freshness.md +40 -0
  11. package/workspace/guide/index.md +2 -0
  12. package/workspace/guide/my-work.md +76 -1
  13. package/workspace/lib/calendar-cli.js +84 -0
  14. package/workspace/lib/calendar-layers.js +372 -0
  15. package/workspace/lib/facts-async.js +144 -35
  16. package/workspace/lib/freshness.js +167 -0
  17. package/workspace/lib/offloop.js +94 -0
  18. package/workspace/lib/pm-items.js +71 -6
  19. package/workspace/lib/pmos-verbs.js +3 -0
  20. package/workspace/lib/report-needs.js +1 -1
  21. package/workspace/lib/route-modules.js +1 -0
  22. package/workspace/lib/routes-board.js +3 -1
  23. package/workspace/lib/routes-calendar-views.js +112 -3
  24. package/workspace/lib/routes-my-work.js +27 -9
  25. package/workspace/lib/routes-pm-items.js +123 -13
  26. package/workspace/lib/routes-sources.js +64 -0
  27. package/workspace/lib/work-focus.js +106 -0
  28. package/workspace/package-lock.json +2 -2
  29. package/workspace/package.json +1 -1
  30. package/workspace/public/pm/c3-calendar.js +392 -0
  31. package/workspace/public/pm/c3-demo-cal4.js +114 -0
  32. package/workspace/public/pm/c3-demo-data.js +79 -77
  33. package/workspace/public/pm/c3-demo-misc2.js +4 -16
  34. package/workspace/public/pm/c3-demo-work-ix.js +19 -67
  35. package/workspace/public/pm/c3-demo.js +10 -3
  36. package/workspace/public/pm/c3-shell.js +5 -2
  37. package/workspace/public/pm/calendar-views.js +10 -3
  38. package/workspace/public/pm/components.css +34 -0
  39. package/workspace/public/pm/console3.css +155 -0
  40. package/workspace/public/pm/dest-settings.js +10 -1
  41. package/workspace/public/pm/freshness-line.js +123 -0
  42. package/workspace/public/pm/pm-work-view.js +364 -172
  43. package/workspace/public/pm/work-place.js +58 -0
  44. package/workspace/server.js +39 -2
  45. package/workspace/skills/calendar-sync.skill +30 -0
  46. package/workspace/test-files.json +2 -0
  47. package/workspace/test-lists/calendar-layers.json +5 -0
  48. package/workspace/test-lists/calendar-time-page.json +5 -0
  49. package/workspace/test-lists/console3-work-page.json +9 -0
  50. package/workspace/test-lists/workspace-data-freshness.json +6 -0
  51. 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.130.0",
3
+ "version": "0.132.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@21cec91
1
+ kernel-source: pmos@b177828
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:27:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T04:03:34Z"}
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:27:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T04:03:34Z"}
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:27:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T04:03:34Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -57,7 +57,9 @@ function collect(tab) {
57
57
  if (title.length < 4) continue;
58
58
  const id = item.getAttribute('data-id') || item.getAttribute('data-needs-row') || item.getAttribute('data-due-row') || item.getAttribute('data-draft-row') || '';
59
59
  const label = ctl === item ? 'row' : norm(ctl.getAttribute('aria-label') || ctl.textContent).slice(0, 40);
60
- 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;
61
63
  if (!out.get(title).controls.includes(label)) out.get(title).controls.push(label);
62
64
  }
63
65
  return [...out.values()];
@@ -103,7 +105,7 @@ function collect(tab) {
103
105
 
104
106
  // Every title that has an action control on two or more tabs.
105
107
  const where = new Map();
106
- 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)) {
107
109
  if (!where.has(it.title)) where.set(it.title, []);
108
110
  where.get(it.title).push({ tab, controls: it.controls });
109
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,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.
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: Guide
3
+ title: "Data freshness: how the workspace keeps up to date, and how old each source is"
4
+ timestamp: 2026-10-11
5
+ ---
6
+
7
+ # Data freshness
8
+
9
+ The workspace reads your repositories, and your agents read Jira, your calendar and My work for it. This page says when each one is read again and where you see how old it is.
10
+
11
+ ## Your repositories
12
+
13
+ The workspace checks each registered repository every 10 seconds for a change: a commit, a checkout, a pull, or a file saved by you or an agent. The check is two read-only git commands. When something changed and then stays the same for 3 seconds, the workspace reads the repository again. It reads nothing when nothing changed.
14
+
15
+ - **Initiatives** (Briefing, Calendar and the reports) are read in a separate process, so the workspace keeps answering while it reads. While a read runs, the pages show the last result. The result is kept on this machine, so after a restart the pages open at once with the last result and a new read starts only if the repository changed.
16
+ - **Board and My work** are folded in a separate thread for the same reason.
17
+
18
+ To stop the checks for one repository, press **Stop watching this repository** in Settings › Data freshness. The pages then show what was read last, and **Refresh** reads it again. Press **Watch this repository for changes** to start the checks again.
19
+
20
+ ## Jira, calendar, My work and tracker items
21
+
22
+ The workspace holds no password or token, and it never calls these services. An agent session reads each one with your own connection, on a schedule or when you press **Refresh**:
23
+
24
+ | Source | Skill | Default schedule | Shown as old after |
25
+ |---|---|---|---|
26
+ | Calendar | calendar-sync | 06:30 and 12:30 every day (set `calendar`) | 8 hours |
27
+ | Jira status | jira-sync | 06:30 every day (set `jira`) | 26 hours |
28
+ | My work | my-work-read | when you press Read again | 26 hours |
29
+ | Tracker items | tracker-refresh | Friday 15:30 (set `tracker`) | 8 days |
30
+
31
+ The calendar-sync skill reads today and the next 14 days, read-only, and saves the events with this command:
32
+
33
+ - `pmos calendar import --from <file|-> --sensitivity <label>` saves the events the agent read. The label is `public`, `internal`, `confidential` or `private`. `--dry-run` checks the file and saves nothing.
34
+ - `pmos calendar status` says how many events are saved and when they were read.
35
+
36
+ The events are kept in your own runtime folder on this machine, never in a repository.
37
+
38
+ ## Where you see it
39
+
40
+ Each page shows a short **as of** line for the sources it reads. The line turns amber when a source is older than its limit, and says **never read** when nothing was read yet. Settings › Data freshness shows every source in one table: when it was read, its state, its schedule, and **Refresh**.
@@ -43,10 +43,12 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
43
43
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
44
44
  - [Jira tickets](jira.md): Jira tickets drafted from the PRD, pushed when you select them, and their status.
45
45
  - [My work](my-work.md): the work assigned to you, its order, the brief to an agent, and `pmos my-work`.
46
+ - [Data freshness](data-freshness.md): when each source is read again, how old it is, and `pmos calendar`.
46
47
  - [People](people.md): the people you work with in a repository, how they get there, and `pmos people`.
47
48
  - [Docs](docs.md): whether each linked document is the same as, ahead of or behind its Google Docs and Confluence copy.
48
49
  - [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.
49
50
  - [Layout](layout.md): the frame, the keys and the rule for where a list or a detail opens.
51
+ - [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.
50
52
  - [Statuses](statuses.md): what each status means, where it comes from, and what you can change.
51
53
  - [Initiatives and next steps](initiatives-and-next-steps.md): the detail, the next steps, and the artifacts.
52
54
  - [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`.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "My work: every Jira ticket and WBS item assigned to you, ranked, briefed to an agent, approved in Agenda"
4
- timestamp: 2026-10-07
4
+ timestamp: 2026-10-11
5
5
  ---
6
6
 
7
7
  # My work
@@ -68,3 +68,78 @@ Agenda shows one card, **N items need your brief**, when items wait in To brief.
68
68
  | `pmos my-work submit --file <read.json>` | The my-work-read skill submits its read. |
69
69
 
70
70
  None of these briefs an agent or takes an item. Do that in My work.
71
+
72
+ ## The Work page: your own items in six views
73
+
74
+ The Work page holds the items you add for yourself. These are your own to-dos, such as "send Dana the new date". The server decides where each item shows, so one item is never in two views.
75
+
76
+ The six views follow the OmniFocus style. The **Work** tab badge counts Inbox plus Today.
77
+
78
+ | View | What it holds |
79
+ |---|---|
80
+ | Inbox | Items with no date, not yet decided. Give each one a date, a defer date or a job, mark it waiting, move it to Someday, or drop it. |
81
+ | Today | Items due today or overdue, and every flagged item. Overdue items come first, then flagged items, then the rest by due date. |
82
+ | Upcoming | Items that arrive in the future. A later due date or a defer date puts an item here. The next 7 days are grouped by day, then **Later**. |
83
+ | Waiting | Items where you wait for someone. The item that has waited longest comes first. |
84
+ | Someday | Items you parked. They do not appear anywhere else. |
85
+ | Review | Items in Inbox, Someday or Waiting that nobody touched for 7 days. The most stale item comes first. |
86
+
87
+ An item goes to the first view that fits these rules, in this order. Parked is Someday. Waiting on someone is Waiting. A defer date after today is Upcoming. Flagged is Today. Due today or earlier is Today. A later due date is Upcoming. An undated item whose defer date has arrived is Today. No date is Inbox.
88
+
89
+ ### Defer and flag
90
+
91
+ A defer date means "not before this day". The item stays in Upcoming and shows **Not before** with the date. On that day it moves to Today by itself. A due date is when the work must be done. A defer date is when you start.
92
+
93
+ Flag an item to put it in Today now, whatever its date. Remove the flag to send it back.
94
+
95
+ ### Focus
96
+
97
+ Focus sits at the top of Today. PMOS suggests up to three jobs for the day and gives the reason for each, for example "2 items overdue, 1 flagged".
98
+
99
+ PMOS groups the Today items by their job. An item with no job is its own group. It ranks a group by how late its items are, and by whether they are due today, flagged, P1 or work outside PMOS. It uses rules only and calls no model.
100
+
101
+ For each suggestion, you accept it, change it or reject it. An accepted job stays at the top of Today for the day. A rejected job does not come back that day. Focus starts empty each new day.
102
+
103
+ ### Work outside PMOS
104
+
105
+ Some items are not done in PMOS, such as "talk to Ana" or "set up a meeting with Raj". Mark these as offline work and name the person. An offline item offers three actions:
106
+
107
+ - **Done.** Add a one-line outcome if you want to keep it.
108
+ - **Not yet.** Choose later today, tomorrow or next week. Tomorrow and next week set a defer date, and the item returns to Today on that day. Next week means the next Monday. Later today reminds you in 3 hours.
109
+ - **Drop.**
110
+
111
+ An offline item can carry evidence that it is done, for example a meeting that took place. The row then says **Looks done?** and shows a **Confirm** button. PMOS never marks an item done by itself. You press Confirm.
112
+
113
+ ### Review
114
+
115
+ Review is a weekly pass. It shows one item at a time. Press **Keep** to leave the item as it is and clear it from the queue for 7 days. Press **Change** to edit it. Press **Drop** to remove it. Any change to an item also clears it from the queue.
116
+
117
+ ### Link to a team task
118
+
119
+ A Work item can link to the plan item it advances. The link shows on the item with the title of the plan item. Closing the plan item is a separate step, in the initiative.
120
+
121
+ ### Where decisions go
122
+
123
+ A decision or an approval only you can make is not a Work item. It shows in Briefing, under **Needs you**.
124
+
125
+ ## Using the Work page
126
+
127
+ The Work page in the console has six views: **Inbox**, **Today**, **Upcoming**, **Waiting**, **Someday** and **Review**. Each view button shows how many items it holds.
128
+
129
+ Each item is in one view only. The rule, in order: an item in Someday stays in Someday. An item that waits on a person is in Waiting. An item with a Not before date after today is in Upcoming. A flagged item is in Today. An item due today or earlier is in Today. An item due later is in Upcoming. An item with no date is in Inbox.
130
+
131
+ Decisions are not on the Work page. They have their own place in the console.
132
+
133
+ **Today** starts with a Focus band. It suggests up to 3 items. Select **Accept** to pin one, **Not today** to remove the suggestion, or **Unpin** to remove a pinned item. You can edit the title of a pinned item.
134
+
135
+ **Upcoming** shows one block for each of the next 7 days, then **Later**.
136
+
137
+ **Review** shows the items in Inbox, Waiting and Someday that nobody changed for 7 days or more, one at a time, the oldest first. For each item, select **Keep**, **Change** or **Drop**. When no items remain, the view shows **Review done**.
138
+
139
+ Open an item to use these actions: **Flag**, **Not before** (a date), **Someday** or **Bring back**, **Waiting** (a name) or **Stop waiting**, **Done** and **Drop**.
140
+
141
+ An item marked **outside PMOS** is work you do yourself, for example a talk with a person. It shows the person's name. Write the result in one line and select **Done**, or select **Not yet** and then **Later today**, **Tomorrow** or **Next week**. The item shows how many times you moved it. When PMOS finds evidence that the work is done, the item shows **Looks done?** and the evidence. Select **Confirm** to close it.
142
+
143
+ Each action shows **Undo** for a short time.
144
+
145
+ When you type a new line, the chip below the box tells you the view where the line will go.