create-pmos 0.167.0 → 0.169.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 (59) 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-driver-fixture.js +61 -0
  7. package/workspace/c3-driver-test.js +164 -0
  8. package/workspace/extension/README.md +33 -0
  9. package/workspace/extension/address.js +20 -0
  10. package/workspace/extension/content.js +76 -0
  11. package/workspace/extension/icons/icon-128.png +0 -0
  12. package/workspace/extension/icons/icon-16.png +0 -0
  13. package/workspace/extension/icons/icon-32.png +0 -0
  14. package/workspace/extension/icons/icon-48.png +0 -0
  15. package/workspace/extension/manifest.json +15 -0
  16. package/workspace/extension/options.css +33 -0
  17. package/workspace/extension/options.html +26 -0
  18. package/workspace/extension/options.js +24 -0
  19. package/workspace/extension/panel.css +21 -0
  20. package/workspace/extension/panel.html +14 -0
  21. package/workspace/extension/panel.js +67 -0
  22. package/workspace/guide/console-calendar.md +14 -0
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-driver.md +87 -0
  25. package/workspace/lib/driver-cli.js +207 -0
  26. package/workspace/lib/driver-decide.js +118 -0
  27. package/workspace/lib/driver-facts.js +113 -0
  28. package/workspace/lib/driver-needs.js +107 -0
  29. package/workspace/lib/driver-run.js +187 -0
  30. package/workspace/lib/driver.js +408 -0
  31. package/workspace/lib/extension-cli.js +39 -0
  32. package/workspace/lib/headless.js +13 -2
  33. package/workspace/lib/meeting-focus.js +48 -1
  34. package/workspace/lib/needs-providers.js +1 -0
  35. package/workspace/lib/pmos-verbs.js +6 -0
  36. package/workspace/lib/route-modules.js +1 -0
  37. package/workspace/lib/routes-boards.js +11 -1
  38. package/workspace/lib/routes-driver.js +167 -0
  39. package/workspace/lib/routes-meeting-focus.js +17 -1
  40. package/workspace/package-lock.json +2 -2
  41. package/workspace/package.json +1 -1
  42. package/workspace/public/pm/c3-brief-needs.js +19 -3
  43. package/workspace/public/pm/c3-demo-focus.js +12 -2
  44. package/workspace/public/pm/c3-demo.js +1 -1
  45. package/workspace/public/pm/c3-driver-work.js +110 -0
  46. package/workspace/public/pm/c3-driver.css +18 -0
  47. package/workspace/public/pm/c3-driver.js +298 -0
  48. package/workspace/public/pm/c3-focus.css +31 -0
  49. package/workspace/public/pm/c3-focus.js +122 -21
  50. package/workspace/public/pm/c3-ii-tabs.js +3 -2
  51. package/workspace/public/pm/c3-ini-ui.js +4 -2
  52. package/workspace/public/pm/c3-initiatives.js +14 -0
  53. package/workspace/public/pm/c3-shell.js +2 -2
  54. package/workspace/public/pm/pm-work-view.js +12 -2
  55. package/workspace/server.js +21 -1
  56. package/workspace/skills/initiative-drive.skill +50 -0
  57. package/workspace/skills/work-unblock.skill +42 -0
  58. package/workspace/test-files.json +6 -0
  59. package/workspace/test-lists/c3-meeting-focus.json +13 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.167.0",
3
+ "version": "0.169.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@23fe058
1
+ kernel-source: pmos@d87e438
2
2
  assembled: 2026-10-12
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-12T14:00:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T14:51:11Z"}
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-12T14:00:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T14:51:11Z"}
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-12T14:00:51Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-12T14:51:11Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ /* c3-driver-fixture.js — a real server, a scratch repository and a seeded driver ledger for the console3 Initiative Driver suite
3
+ * (c3-driver-test.js). Not a suite: it asserts nothing. The server runs with PMOS_DRIVER=off, so no headless run starts: every
4
+ * proposal, run and blocker the page shows is a line this file wrote with lib/driver.js, the single writer.
5
+ *
6
+ * const F = await start({ name }) -> { BASE, ws, board, team, ledger, ids, item, itemDue, post, get, stop }
7
+ * ids: { problem, evidence, sizing, move, follow, step } proposal ids; item: the Work task with a follow-up; itemDue: the one with a due date
8
+ */
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { spawn } = require('child_process');
12
+ const T = require('./test-run').configure({ name: 'c3-driver' });
13
+ const { makeRepos, codeRepo, write, git } = require('./plan-test-repos');
14
+ const D = require('./lib/driver.js');
15
+
16
+ const HOUR = 3600000;
17
+ async function call(base, method, p, body, headers) {
18
+ const r = await fetch(base + p, { method, headers: headers || {}, body: body === undefined ? undefined : JSON.stringify(body) });
19
+ const raw = await r.text(); let j = null; try { j = JSON.parse(raw); } catch { /* not JSON */ }
20
+ return { status: r.status, body: j, raw };
21
+ }
22
+
23
+ async function start({ name = 'c3-driver' } = {}) {
24
+ const R = makeRepos(name);
25
+ const team = codeRepo(path.join(R.base, 'team'));
26
+ fs.mkdirSync(path.join(team, 'pmos', 'state'), { recursive: true });
27
+ write(team, 'pmos/state/.keep', '');
28
+ git(team, ['add', '-A']); git(team, ['commit', '-q', '-m', 'state']);
29
+ const ledger = path.join(team, 'pmos', 'state', 'driver.jsonl');
30
+ const BASE = T.BASE;
31
+ const srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore',
32
+ env: T.env({ PMOS_WS_PORT: String(T.PORT), PMOS_SELF_ROOT: team, PMOS_NO_OPEN: '1', PMOS_DRIVER: 'off' }) });
33
+ await T.waitForServer(BASE);
34
+ const press = await T.pressHeaders(BASE);
35
+ const post = (p, body) => call(BASE, 'POST', p, body, press);
36
+ const get = (p) => call(BASE, 'GET', p);
37
+ const reg = await post('/api/workspaces', { root: team, focus: true });
38
+ const ws = reg.body.workspace.id;
39
+ const made = await post('/api/initiatives', { ws, title: 'Partner API onboarding', lane: 'feature', anchor: 'health:tech', id: 'drv-ui' });
40
+ if (made.status !== 200) throw new Error('could not create the initiative: ' + made.raw);
41
+ const a = (await post('/api/pm-items/add', { line: 'Get the sandbox access date from Dana waiting on Dana Hartono' })).body.item;
42
+ const b = (await post('/api/pm-items/add', { line: 'Send the partner onboarding list to Lina' })).body.item;
43
+ const tI = { kind: 'initiative', ws, id: 'drv-ui' };
44
+ const now = Date.now();
45
+ const w = (line, at) => D.append(ledger, line, { now: at });
46
+ const prop = (id, target, what, data, why, sources, at, run) => { w({ ev: 'proposal', id, target, what, data, why, sources: sources || [], run, by: 'agent:sess-4.0a1b2c3d' }, at); return id; };
47
+ const ids = { problem: 'p-aaaaaaaa01', evidence: 'p-aaaaaaaa02', sizing: 'p-aaaaaaaa03', move: 'p-aaaaaaaa04', follow: 'p-aaaaaaaa05', step: 'p-aaaaaaaa06' };
48
+ w({ ev: 'epoch' }, now - 3 * 24 * HOUR);
49
+ w({ ev: 'run-start', run: 'run-aaaaaaaa01', target: tI, board: ws, wakes: [], session: 'sess-4.0a1b2c3d' }, now - 5 * HOUR);
50
+ prop(ids.problem, tI, 'problem', { text: 'New partners wait 9 days on average from signed contract to first live API call. Support handles 23 onboarding tickets a month.' }, 'first draft from the Jira and Slack threads', [{ label: 'Jira', ref: 'IDPF-1204' }, { label: 'Slack', ref: '#partner-support, 6 Oct' }], now - 4 * HOUR, 'run-aaaaaaaa01');
51
+ prop(ids.evidence, tI, 'evidence', { text: 'Six sources back the problem statement. Four name sandbox access as the main delay.', items: [{ text: 'Partner X waited 9 days for sandbox access', ref: 'IDPF-1204' }, { text: 'Two partners paused go-live this quarter', ref: 'https://example.invalid/partner-council/1-oct' }] }, 'evidence for the problem', [{ label: 'Meeting note', ref: 'IDPF DSU, 8 Oct' }], now - 4 * HOUR + 600000, 'run-aaaaaaaa01');
52
+ prop(ids.sizing, tI, 'sizing', { text: '14 partners are in the pipeline this half. Cutting the wait from 9 days to 3 moves about 5 go-lives into Q4.' }, 'low confidence: two data points', [{ label: 'Slack', ref: '#sales-asks, 7 Oct' }], now - 3 * HOUR, 'run-aaaaaaaa01');
53
+ prop(ids.move, tI, 'move', { to: 'Plan' }, 'the problem, evidence and sizing are in', [], now - 50 * HOUR, 'run-aaaaaaaa01');
54
+ w({ ev: 'run-end', run: 'run-aaaaaaaa01', ok: true, agent_run: null }, now - 3 * HOUR + 60000);
55
+ w({ ev: 'blocker', key: `initiative:${ws}:drv-ui:pm-open`, day: D.localDay(now), target: tI, board: ws, code: 'pm-open', owner: 'pm', text: "The agent's drafts have waited on you since " + new Date(now - 50 * HOUR).toISOString().slice(0, 10) }, now - HOUR);
56
+ prop(ids.follow, { kind: 'work', ws, id: a.id }, 'follow-up', { text: 'Hi Dana,\n\nQuick follow-up on the sandbox access for Nusa Pay. Can you tell me which day your engineers can use the sandbox?\n\nThanks,\nWawan', to: 'Dana' }, 'waiting 4 days, no reply', [{ label: 'Slack', ref: '#partner-support' }], now - 2 * HOUR, null);
57
+ prop(ids.step, { kind: 'work', ws, id: b.id }, 'next-step', { text: 'Send the list to Lina', due: '2026-10-15' }, 'overdue with no next step', [], now - HOUR, null);
58
+ const stop = () => { try { srv.kill('SIGKILL'); } catch { /* gone */ } T.killTmux(); R.cleanup(); };
59
+ return { BASE, ws, board: ws, team, ledger, ids, item: a, itemDue: b, post, get, stop, T, D, tI };
60
+ }
61
+ module.exports = { start };
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-driver-test.js — the Initiative Driver on console3 (initiative initiative-driver; PRD section 5 and done-criterion 9).
4
+ * LIVE mode: a real server and a scratch repository whose driver.jsonl this suite seeds (c3-driver-fixture.js; PMOS_DRIVER=off, so no
5
+ * run starts and every line the page shows is one the fixture wrote). A real Chromium drives /c3.html.
6
+ *
7
+ * strip Initiative · Overview shows the Agent strip: the count, Pause, "1 of 3 runs used today", the status, the last action, the
8
+ * blocker line, one row per open proposal; a row opens to its text and sources (a URL source is a link)
9
+ * runs the runs chip opens a sheet that lists today's run
10
+ * decide Keep on a problem proposal posts /api/driver/keep and the row turns "Kept by you"; Send back asks for a note and posts it;
11
+ * Drop posts and the row leaves
12
+ * pause Pause posts /api/driver/pause and the strip says Paused; Resume brings it back
13
+ * work a Work task with a follow-up shows the agent line; Review opens the sheet with the draft and "the agent sent nothing";
14
+ * Keep records the keep; a task with a next-step shows Keep and Drop in the line
15
+ * setting Board settings has the Agent setting; pressing Shadow posts /api/driver/mode and the ledger holds it
16
+ * briefing Needs you shows the agent's two kinds of row and a press opens the initiative
17
+ * phone at 400 wide the strip has no horizontal scroll
18
+ * Every wait polls the page. The page must raise no script error.
19
+ */
20
+ const path = require('path');
21
+ const { findBrowser } = require('./lib/find-browser');
22
+ const F = require('./c3-driver-fixture');
23
+
24
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
+ const until = async (fn, ms = 20000, step = 120) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
26
+ const results = [];
27
+ const ok = (n, cond, extra = '') => { results.push({ n, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${n}${!cond && extra !== '' ? ' — ' + String(extra).slice(0, 400) : ''}`); };
28
+ let fx = null; let browser = null;
29
+ const finish = async (code) => {
30
+ if (browser) await browser.close().catch(() => {});
31
+ if (fx) fx.stop();
32
+ const bad = results.filter((x) => !x.cond).length;
33
+ console.log(`\n${results.length - bad}/${results.length} c3-driver assertions passed`);
34
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
35
+ };
36
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
37
+
38
+ (async () => {
39
+ const exe = findBrowser();
40
+ if (!exe) { console.log('· no Chrome/Chromium on this machine — c3-driver suite skipped'); process.exit(0); }
41
+ let chromium;
42
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('· playwright-core not installed — c3-driver suite skipped'); process.exit(0); }
43
+ fx = await F.start({ name: 'c3-driver' });
44
+ const { BASE, ws, ids, D, ledger } = fx;
45
+ const state = () => D.fold(D.readLines(ledger));
46
+ browser = await chromium.launch({ executablePath: exe });
47
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
48
+ const page = await ctx.newPage();
49
+ const errors = []; page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
50
+ const posts = []; page.on('request', (r) => { if (r.method() === 'POST' && r.url().includes('/api/driver/')) posts.push({ path: new URL(r.url()).pathname, body: JSON.parse(r.postData() || '{}') }); });
51
+ const st = (f, a) => page.evaluate(f, a);
52
+ const sel = (s) => until(() => page.$(s));
53
+ const INI = `/c3.html?tab=init&init=ini:${ws}:drv-ui:overview`;
54
+
55
+ /* ── strip ── */
56
+ await page.goto(BASE + INI, { waitUntil: 'load' });
57
+ await sel('[data-drv-row]');
58
+ const rows = await st(() => [...document.querySelectorAll('[data-drv-row]')].map((r) => r.dataset.drvRow));
59
+ ok('S1 the Agent strip draws one row per open proposal, in the order the agent filed them', rows.join() === [ids.problem, ids.evidence, ids.sizing, ids.move].join(), rows.join());
60
+ const head = await st(() => document.querySelector('[data-drv="strip"] .sh2').textContent);
61
+ ok('S2 the header says AGENT 4, has Pause and "1 of 3 runs used today"', /Agent\s*4/i.test(head) && /Pause/.test(head) && /1 of 3 runs used today/.test(head), head);
62
+ const body = await st(() => document.querySelector('[data-drv="strip"]').textContent);
63
+ ok('S3 the status line says the agent is idle and waiting on you, with the last action', /Agent is idle · waiting on you/.test(body) && /The oldest proposal is 2 days old/.test(body) && /Last: filed 4 proposals/.test(body), body.slice(0, 300));
64
+ ok('S4 the blocker line names who stopped the agent', /Stopped by: you\./.test(await st(() => document.querySelector('[data-drv="blocker"]').textContent)));
65
+ ok('S5 the old proposal says how long it waited', /waiting 2 days/.test(await st(() => document.querySelector(`[data-drv-row="${'p-aaaaaaaa04'}"]`).textContent)));
66
+ await page.click(`[data-drv-title="${ids.evidence}"]`);
67
+ await until(() => st((id) => document.querySelector(`[data-drv-row="${id}"] .xp.open`), ids.evidence));
68
+ const ev = await st((id) => { const r = document.querySelector(`[data-drv-row="${id}"]`); return { text: r.textContent, link: (r.querySelector('a[href^="https://example.invalid"]') || {}).href || '', n: r.querySelectorAll('[data-drv-src]').length }; }, ids.evidence);
69
+ ok('S6 a row opens to its text and its sources (label and ref); a URL ref is a link', /Six sources back the problem statement/.test(ev.text) && /IDPF DSU, 8 Oct/.test(ev.text) && ev.n === 3 && /partner-council/.test(ev.link), JSON.stringify(ev));
70
+ await page.click('[data-drv="runs"]');
71
+ await sel('[data-drv="runs-sheet"]');
72
+ const runs = await st(() => document.querySelector('[data-shell="sheet"]').textContent);
73
+ ok('S7 the runs chip opens a sheet with today\'s run', /Agent runs today/.test(runs) && /1 of 3 runs used today/.test(runs) && /Filed 4 proposals/.test(runs), runs.slice(0, 300));
74
+ await page.keyboard.press('Escape');
75
+
76
+ /* ── decide ── */
77
+ await page.click(`[data-drv-row="${ids.problem}"] [data-drv-act=keep]`);
78
+ await until(() => posts.some((p) => p.path === '/api/driver/keep'));
79
+ ok('D1 Keep posts /api/driver/keep with the proposal id and no writer field', posts.some((p) => p.path === '/api/driver/keep' && p.body.proposal === ids.problem && !('by' in p.body)), JSON.stringify(posts));
80
+ await until(() => st((id) => { const r = document.querySelector(`[data-drv-row="${id}"]`); return r && r.dataset.drvState === 'kept'; }, ids.problem));
81
+ ok('D2 the row turns "Kept by you" and loses its buttons; the ledger holds the keep by the PM', await st((id) => { const r = document.querySelector(`[data-drv-row="${id}"]`); return /Kept by you/.test(r.textContent) && !r.querySelector('[data-drv-act]'); }, ids.problem)
82
+ && state().proposals.get(ids.problem).state === 'kept' && state().proposals.get(ids.problem).decided_by === 'pm:wawan');
83
+ await until(() => st(() => /Kept: Problem statement|Kept/.test(document.querySelector('[data-c3-toast]') ? document.querySelector('[data-c3-toast]').textContent : '')));
84
+ await page.click(`[data-drv-row="${ids.sizing}"] [data-drv-act=back]`);
85
+ await sel('[data-drv="send-back"]');
86
+ await page.click('[data-drv="send-back"]');
87
+ ok('D3 Send back with an empty note is refused in plain words and posts nothing', !!(await until(() => page.$('[data-drv="note-error"]:not([hidden])'))) && !posts.some((p) => p.path === '/api/driver/return'));
88
+ await page.fill('[data-drv="note"]', 'Use active partners, not signed ones');
89
+ await page.click('[data-drv="send-back"]');
90
+ await until(() => posts.some((p) => p.path === '/api/driver/return'));
91
+ ok('D4 Send back posts the note; the row says it was sent back; the ledger holds it', posts.some((p) => p.path === '/api/driver/return' && p.body.note === 'Use active partners, not signed ones')
92
+ && !!(await until(() => st((id) => { const r = document.querySelector(`[data-drv-row="${id}"]`); return r && r.dataset.drvState === 'back'; }, ids.sizing))) && state().proposals.get(ids.sizing).state === 'returned');
93
+ await page.click(`[data-drv-row="${ids.evidence}"] [data-drv-act=drop]`);
94
+ await until(() => st((id) => !document.querySelector(`[data-drv-row="${id}"]`), ids.evidence));
95
+ ok('D5 Drop posts /api/driver/drop and the row leaves', posts.some((p) => p.path === '/api/driver/drop' && p.body.proposal === ids.evidence) && state().proposals.get(ids.evidence).state === 'dropped');
96
+
97
+ /* ── pause ── */
98
+ await page.click('[data-drv-act=pause]');
99
+ await until(() => st(() => /Agent is paused/.test(document.querySelector('[data-drv="status"]').textContent)));
100
+ ok('P1 Pause posts /api/driver/pause for the initiative and the strip says Paused with a Resume button', posts.some((p) => p.path === '/api/driver/pause' && p.body.target_kind === 'initiative' && p.body.id === 'drv-ui')
101
+ && !!(await page.$('[data-drv-act=resume]')) && D.isPaused(state(), fx.tI));
102
+ await page.click('[data-drv-act=resume]');
103
+ await until(() => st(() => !!document.querySelector('[data-drv-act=pause]')));
104
+ ok('P2 Resume lifts it', !D.isPaused(state(), fx.tI));
105
+
106
+ /* ── setting ── */
107
+ await page.goto(BASE + `/c3.html?tab=init&init=board:${ws}:summary`, { waitUntil: 'load' });
108
+ await (await sel('[data-ini=board-settings]')).click();
109
+ await sel('[data-drv="setting"] [data-drv-mode]');
110
+ ok('T1 Board settings shows the Agent setting at Approve, with the one-line explanations', await st(() => { const s = document.querySelector('[data-drv="setting"]'); return s.querySelector('[data-drv-mode=approve]').getAttribute('aria-pressed') === 'true' && /The agent drafts and asks you/.test(s.textContent) && /The agent does nothing on this board/.test(s.textContent); }));
111
+ await page.click('[data-drv-mode=shadow]');
112
+ await until(() => posts.some((p) => p.path === '/api/driver/mode'));
113
+ await until(() => st(() => document.querySelector('[data-drv-mode=shadow]').getAttribute('aria-pressed') === 'true'));
114
+ ok('T2 pressing Shadow posts /api/driver/mode for the board and the ledger holds it', posts.some((p) => p.path === '/api/driver/mode' && p.body.mode === 'shadow' && p.body.board === ws) && D.modeOf(state(), ws) === 'shadow');
115
+ await page.goto(BASE + INI, { waitUntil: 'load' });
116
+ await sel('[data-drv="shadow"]');
117
+ ok('T3 in Shadow the strip says so and offers no Keep', !(await page.$('[data-drv-act=keep]')) && /Shadow mode/.test(await st(() => document.querySelector('[data-drv="shadow"]').textContent)));
118
+ await page.goto(BASE + `/c3.html?tab=init&init=board:${ws}:summary`, { waitUntil: 'load' });
119
+ await (await sel('[data-ini=board-settings]')).click();
120
+ await sel('[data-drv-mode=approve]');
121
+ await page.click('[data-drv-mode=approve]');
122
+ await until(() => D.modeOf(state(), ws) === 'approve');
123
+ ok('T4 pressing Approve puts it back', D.modeOf(state(), ws) === 'approve');
124
+
125
+ /* ── work ── */
126
+ await page.goto(BASE + '/c3.html?tab=work', { waitUntil: 'load' });
127
+ await sel('[data-pm-lenses]');
128
+ await page.click('[data-pm-lenses] [data-lens=waiting]');
129
+ const line = await sel(`[data-drv-line="${fx.item.id}"]`);
130
+ ok('W1 a Waiting task with a follow-up shows "Agent drafted a follow-up to Dana. Nothing is sent." and Review', /Agent drafted a follow-up to Dana\. Nothing is sent\./.test(await line.textContent()) && !!(await page.$('[data-drv-wnote-act=review]')));
131
+ await page.click('[data-drv-wnote-act=review]');
132
+ await sel('[data-drv="work-sheet"]');
133
+ const sheet = await st(() => document.querySelector('[data-drv="work-sheet"]').textContent);
134
+ ok('W2 Review opens a sheet with the draft and the line that the agent sent nothing', /Quick follow-up on the sandbox access/.test(sheet) && /The agent sent nothing and cannot send/.test(sheet) && !!(await page.$('[data-drv=sheet-keep]')) && !!(await page.$('[data-drv=sheet-back]')) && !!(await page.$('[data-drv=sheet-drop]')));
135
+ await ctx.grantPermissions(['clipboard-read', 'clipboard-write'], { origin: BASE }).catch(() => {});
136
+ await page.click('[data-drv=sheet-keep]');
137
+ await until(() => state().proposals.get(ids.follow).state === 'kept');
138
+ ok('W3 Keep records the keep (by the PM) and the sheet closes', state().proposals.get(ids.follow).decided_by === 'pm:wawan' && !!(await until(async () => !(await page.$('[data-drv="work-sheet"]')))));
139
+ const clip = await page.evaluate(() => navigator.clipboard.readText().catch(() => null));
140
+ ok('W4 the draft text was copied to the clipboard (or the browser refused, and the text was selected)', (clip && /Quick follow-up on the sandbox access/.test(clip)) || clip === null, String(clip));
141
+ await page.click('[data-pm-lenses] [data-lens=inbox]');
142
+ await sel(`[data-drv-line="${fx.itemDue.id}"]`);
143
+ ok('W5 a task with a next-step shows the suggestion with Keep and Drop in the line', /Agent suggests a next step: Send the list to Lina, due Thu 15 Oct/.test(await st((id) => document.querySelector(`[data-drv-line="${id}"]`).textContent, fx.itemDue.id)) && !!(await page.$('[data-drv-wnote-act=keep]')) && !!(await page.$('[data-drv-wnote-act=drop]')));
144
+ await page.click('[data-drv-wnote-act=drop]');
145
+ await until(() => state().proposals.get(ids.step).state === 'dropped');
146
+ ok('W6 Drop in the line drops the proposal and the line goes', !!(await until(async () => !(await page.$(`[data-drv-line="${fx.itemDue.id}"]`)))));
147
+
148
+ /* ── briefing ── */
149
+ await page.goto(BASE + '/c3.html?tab=brief', { waitUntil: 'load' });
150
+ await sel('[data-drv-need]');
151
+ const needs = await st(() => [...document.querySelectorAll('[data-drv-need]')].map((r) => r.dataset.drvNeed + ':' + r.textContent.replace(/\s+/g, ' ')));
152
+ ok('B1 Needs you shows the agent\'s rows: the drafts left on the initiative, and the initiative waiting on you', needs.some((t) => /^driver-drafts:Agent has 1 draft on Partner API onboarding/.test(t)) && needs.some((t) => /^driver-blocked:.*Partner API onboarding is waiting on you/.test(t)), needs.join(' | '));
153
+ await page.click('[data-drv-need="driver-drafts"] .tt');
154
+ await until(() => page.$('[data-drv="strip"]'));
155
+ ok('B2 a press on the row opens the initiative with its Agent strip', !!(await page.$('[data-drv="strip"]')));
156
+
157
+ /* ── phone ── */
158
+ await page.setViewportSize({ width: 400, height: 900 });
159
+ await page.goto(BASE + INI, { waitUntil: 'load' });
160
+ await sel('[data-drv-row]');
161
+ ok('M1 at 400 wide the page has no horizontal scroll', await st(() => document.documentElement.scrollWidth <= window.innerWidth + 1), await st(() => document.documentElement.scrollWidth));
162
+ ok('N1 no page error was thrown', errors.length === 0, errors.join(' | '));
163
+ await finish();
164
+ })().catch((e) => { console.error(e); finish(1); });
@@ -0,0 +1,33 @@
1
+ ---
2
+ type: Guide
3
+ title: "PMOS for Google Meet: the Chrome extension"
4
+ timestamp: 2026-10-12
5
+ ---
6
+
7
+ # PMOS for Google Meet
8
+
9
+ A small panel over a Google Meet call. It shows the PMOS meeting companion: the topic the room is on, the time left, what is being said, the notes from PMOS and a line to ask PMOS quietly. Only you see it. The extension has no build step and no remote code. It runs only on `https://meet.google.com/*`, reads the address and the title of the Meet tab, and talks only to the PMOS on your own computer.
10
+
11
+ ## Load it in Chrome
12
+
13
+ 1. Open `chrome://extensions`.
14
+ 2. Turn on **Developer mode** (top right).
15
+ 3. Press **Load unpacked** and pick this folder. `pmos extension` prints its path.
16
+
17
+ If PMOS does not run on `http://localhost:7433`, open the extension's **Options** and set the address that `pmos` printed. A Meet tab that is already open picks up the new address when you reload it.
18
+
19
+ ## Use it
20
+
21
+ Open a Meet call, for example `meet.google.com/abc-defg-hij`. The panel shows at the bottom right of the call. It finds the meeting on your PMOS calendar by the Meet link in the invite, then by the title of the tab, then by the meeting that is under way now. If none fits, the panel says so and lists today's meetings to pick from. **Fold** shrinks the panel to a pill, and the same button opens it again. **Open focus in PMOS** opens the full Meeting focus page in a new tab. When PMOS does not answer, the panel says "PMOS is not running at <address>" and links to the options.
22
+
23
+ ## Files
24
+
25
+ | File | What it is |
26
+ |---|---|
27
+ | `manifest.json` | Manifest V3. Permission: `storage` only. Content script on `https://meet.google.com/*`. |
28
+ | `content.js` | Adds the panel on a call page (`/abc-defg-hij`) and removes it when you leave. |
29
+ | `panel.html`, `panel.js`, `panel.css` | The page inside the panel. It checks that PMOS answers, then frames PMOS's compact Meeting focus page. |
30
+ | `options.html`, `options.js`, `options.css`, `address.js` | The one setting: where PMOS is. |
31
+ | `icons/` | The PMOS icon at four sizes. |
32
+
33
+ The extension ships in the workspace package. Publishing it in the Chrome Web Store is not part of it yet.
@@ -0,0 +1,20 @@
1
+ /* address.js — where PMOS is. Shared by panel.js and options.js (classic script; no build step).
2
+ * The address is kept in chrome.storage.sync under `pmosAddress`. The default is the workspace's default port.
3
+ * normalizeAddress('localhost:7500') → 'http://localhost:7500'; anything that is not an http or https address → ''. */
4
+ (function () {
5
+ 'use strict';
6
+ var DEFAULT_ADDRESS = 'http://localhost:7433';
7
+ var KEY = 'pmosAddress';
8
+ function normalizeAddress(text) {
9
+ var t = String(text == null ? '' : text).trim();
10
+ if (!t) return '';
11
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) t = 'http://' + t;
12
+ var u;
13
+ try { u = new URL(t); } catch (e) { return ''; }
14
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
15
+ return u.origin;
16
+ }
17
+ var api = { DEFAULT_ADDRESS: DEFAULT_ADDRESS, KEY: KEY, normalizeAddress: normalizeAddress };
18
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
19
+ else window.PMOS_ADDRESS = api;
20
+ })();
@@ -0,0 +1,76 @@
1
+ /* content.js — the PMOS panel over a Google Meet call. Runs only on https://meet.google.com/* (manifest.json).
2
+ *
3
+ * On a call path (/abc-defg-hij) it puts a fixed host element at the bottom right of the page. Its shadow root holds one iframe: panel.html of this
4
+ * extension, with the Meet code and the tab's title in the address. When the panel folds, the page inside it sends { pmosFocus: 'fold' } (and 'open'
5
+ * when it unfolds) and the host shrinks to a pill. On any other Meet page (the home page, a lobby list) there is no panel.
6
+ *
7
+ * Meet is a single-page app, so the address is watched: it is read again every second and on popstate. The only things read from the page are
8
+ * the address and the title. Nothing is sent from here. No call audio or captions are read. */
9
+ (function () {
10
+ 'use strict';
11
+ var CALL_PATH = /^\/[a-z]{3}-[a-z]{4}-[a-z]{3}$/;
12
+ var HOST_ID = 'pmos-meet-panel';
13
+ var OPEN = { width: 360, height: 520 };
14
+ var PILL_HEIGHT = 44;
15
+ var TITLE_WAIT_MS = 3000; // Meet sets a title like "Meet - <name>" a moment after the call opens
16
+
17
+ /** The Meet code of a path, or '' when the path is not a call. */
18
+ function callCode(pathname) { return CALL_PATH.test(String(pathname || '')) ? String(pathname).slice(1) : ''; }
19
+ /** The panel's address: panel.html with the Meet code and the tab title. */
20
+ function panelSrc(base, code, title) { return base + '?meet=' + encodeURIComponent(code) + '&title=' + encodeURIComponent(title || ''); }
21
+
22
+ var api = { CALL_PATH: CALL_PATH, callCode: callCode, panelSrc: panelSrc };
23
+ if (typeof module !== 'undefined' && module.exports) { module.exports = api; return; }
24
+ if (typeof document === 'undefined' || typeof chrome === 'undefined' || !chrome.runtime || !chrome.runtime.getURL) return;
25
+ if (window.top !== window) return;
26
+
27
+ var CSS = [
28
+ ':host { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; width: ' + OPEN.width + 'px; height: ' + OPEN.height + 'px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px); }',
29
+ ':host([data-folded]) { height: ' + PILL_HEIGHT + 'px; }',
30
+ 'iframe { display: block; width: 100%; height: 100%; border: 1px solid rgba(128, 128, 128, 0.35); border-radius: 12px; background: Canvas; color-scheme: light dark; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); }',
31
+ ':host([data-folded]) iframe { border-radius: 22px; }',
32
+ ].join('\n');
33
+
34
+ var mounted = null; // { code, host, frame }
35
+ var seenAt = 0; // when this call path was first seen
36
+
37
+ function unmount() {
38
+ if (mounted && mounted.host.parentNode) mounted.host.parentNode.removeChild(mounted.host);
39
+ mounted = null;
40
+ }
41
+ function mount(code) {
42
+ var host = document.createElement('div');
43
+ host.id = HOST_ID;
44
+ var root = host.attachShadow({ mode: 'closed' });
45
+ try { var sheet = new CSSStyleSheet(); sheet.replaceSync(CSS); root.adoptedStyleSheets = [sheet]; }
46
+ catch (e) { var st = document.createElement('style'); st.textContent = CSS; root.appendChild(st); }
47
+ var frame = document.createElement('iframe');
48
+ frame.title = 'PMOS meeting helper';
49
+ frame.src = panelSrc(chrome.runtime.getURL('panel.html'), code, document.title);
50
+ root.appendChild(frame);
51
+ document.documentElement.appendChild(host);
52
+ mounted = { code: code, host: host, frame: frame };
53
+ }
54
+
55
+ window.addEventListener('message', function (e) {
56
+ if (!mounted || e.source !== mounted.frame.contentWindow) return;
57
+ if (e.origin !== chrome.runtime.getURL('').replace(/\/$/, '')) return; // the panel's own origin, chrome-extension://<id>
58
+ var m = e.data && e.data.pmosFocus;
59
+ if (m === 'fold') mounted.host.setAttribute('data-folded', '');
60
+ else if (m === 'open') mounted.host.removeAttribute('data-folded');
61
+ });
62
+
63
+ function check() {
64
+ var code = callCode(location.pathname);
65
+ if (!code) { seenAt = 0; unmount(); return; }
66
+ if (mounted && mounted.code === code) return;
67
+ if (mounted) unmount();
68
+ if (!seenAt) seenAt = Date.now();
69
+ var titled = document.title && !/^\s*(Google )?Meet\s*$/i.test(document.title);
70
+ if (titled || Date.now() - seenAt >= TITLE_WAIT_MS) mount(code);
71
+ }
72
+ window.addEventListener('popstate', check);
73
+ window.addEventListener('hashchange', check);
74
+ setInterval(check, 1000);
75
+ check();
76
+ })();
@@ -0,0 +1,15 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "PMOS for Google Meet",
4
+ "version": "0.1.0",
5
+ "description": "Shows your PMOS meeting companion in a small panel over a Google Meet call. Only you see it. It talks only to the PMOS on your own computer.",
6
+ "icons": { "16": "icons/icon-16.png", "32": "icons/icon-32.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" },
7
+ "permissions": ["storage"],
8
+ "content_scripts": [
9
+ { "matches": ["https://meet.google.com/*"], "js": ["content.js"], "run_at": "document_idle" }
10
+ ],
11
+ "web_accessible_resources": [
12
+ { "resources": ["panel.html"], "matches": ["https://meet.google.com/*"] }
13
+ ],
14
+ "options_ui": { "page": "options.html", "open_in_tab": true }
15
+ }
@@ -0,0 +1,33 @@
1
+ /* options.css — the options page. PMOS does not serve this file, so the colours are PMOS's tokens by value. */
2
+ :root {
3
+ color-scheme: light dark;
4
+ --background: oklch(1 0 0);
5
+ --foreground: oklch(0.148 0.004 228.8);
6
+ --muted-foreground: oklch(0.48 0.021 213.5);
7
+ --border: oklch(0.925 0.005 214.3);
8
+ --accent: oklch(0.488 0.243 264.376);
9
+ --destructive: oklch(0.577 0.245 27.325);
10
+ }
11
+ @media (prefers-color-scheme: dark) {
12
+ :root {
13
+ --background: oklch(0.148 0.004 228.8);
14
+ --foreground: oklch(0.987 0.002 197.1);
15
+ --muted-foreground: oklch(0.723 0.014 214.4);
16
+ --border: oklch(1 0 0 / 10%);
17
+ --accent: oklch(0.623 0.214 259.815);
18
+ --destructive: oklch(0.704 0.191 22.216);
19
+ }
20
+ }
21
+ body { margin: 0; background: var(--background); color: var(--foreground); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
22
+ main { max-width: 560px; margin: 0 auto; padding: 32px 16px; }
23
+ h1 { margin: 0 0 8px; font-size: 20px; font-weight: 600; }
24
+ .lede { margin: 0 0 24px; color: var(--muted-foreground); }
25
+ code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }
26
+ label { display: block; margin-bottom: 6px; font-weight: 600; }
27
+ .row { display: flex; gap: 8px; }
28
+ input { flex: 1; min-width: 0; height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; }
29
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
30
+ button { height: 32px; padding: 0 16px; border: 0; border-radius: 6px; background: var(--foreground); color: var(--background); font: inherit; font-weight: 600; cursor: pointer; }
31
+ .status { min-height: 21px; margin: 8px 0 0; color: var(--muted-foreground); }
32
+ .status.bad { color: var(--destructive); }
33
+ .fine { margin: 24px 0 0; color: var(--muted-foreground); font-size: 12px; }
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>PMOS for Google Meet: options</title>
7
+ <link rel="stylesheet" href="options.css">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>PMOS for Google Meet</h1>
12
+ <p class="lede">The panel over a Meet call is a page of PMOS on your own computer. Type the address where your PMOS runs. The default is <code>http://localhost:7433</code>, the port the workspace starts on. If <code>pmos</code> printed another port, use that one.</p>
13
+ <form id="form">
14
+ <label for="addr">PMOS address</label>
15
+ <div class="row">
16
+ <input id="addr" name="addr" type="text" inputmode="url" autocomplete="off" spellcheck="false" placeholder="http://localhost:7433">
17
+ <button type="submit">Save</button>
18
+ </div>
19
+ <p id="status" class="status" role="status" aria-live="polite"></p>
20
+ </form>
21
+ <p class="fine">The extension runs only on meet.google.com. It reads the address and the title of the Meet tab, and it talks only to this PMOS address. It does not read the call's audio or captions.</p>
22
+ </main>
23
+ <script src="address.js"></script>
24
+ <script src="options.js"></script>
25
+ </body>
26
+ </html>
@@ -0,0 +1,24 @@
1
+ /* options.js — the one setting: where PMOS is (chrome.storage.sync, key pmosAddress). */
2
+ (function () {
3
+ 'use strict';
4
+ var A = window.PMOS_ADDRESS;
5
+ var input = document.getElementById('addr');
6
+ var status = document.getElementById('status');
7
+ var say = function (t, bad) { status.textContent = t; status.className = 'status' + (bad ? ' bad' : ''); };
8
+
9
+ try {
10
+ var o = {}; o[A.KEY] = A.DEFAULT_ADDRESS;
11
+ chrome.storage.sync.get(o, function (v) { input.value = A.normalizeAddress(v && v[A.KEY]) || A.DEFAULT_ADDRESS; });
12
+ } catch (e) { input.value = A.DEFAULT_ADDRESS; }
13
+
14
+ document.getElementById('form').addEventListener('submit', function (e) {
15
+ e.preventDefault();
16
+ var addr = A.normalizeAddress(input.value) || (input.value.trim() === '' ? A.DEFAULT_ADDRESS : '');
17
+ if (!addr) { say('That is not an address. Use a form like http://localhost:7433.', true); return; }
18
+ var o = {}; o[A.KEY] = addr;
19
+ chrome.storage.sync.set(o, function () {
20
+ input.value = addr;
21
+ say('Saved. A Meet tab that is open picks it up when you reload the tab.');
22
+ });
23
+ });
24
+ })();
@@ -0,0 +1,21 @@
1
+ /* panel.css — the panel's own page, shown only while PMOS is being looked for. PMOS does not serve this file, so the colours are PMOS's tokens by value. */
2
+ :root {
3
+ color-scheme: light dark;
4
+ --background: oklch(1 0 0);
5
+ --foreground: oklch(0.148 0.004 228.8);
6
+ --muted-foreground: oklch(0.48 0.021 213.5);
7
+ --accent: oklch(0.488 0.243 264.376);
8
+ }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root {
11
+ --background: oklch(0.148 0.004 228.8);
12
+ --foreground: oklch(0.987 0.002 197.1);
13
+ --muted-foreground: oklch(0.723 0.014 214.4);
14
+ --accent: oklch(0.623 0.214 259.815);
15
+ }
16
+ }
17
+ html, body { height: 100%; margin: 0; }
18
+ body { background: var(--background); color: var(--foreground); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
19
+ .msg { margin: 0; padding: 16px; color: var(--muted-foreground); }
20
+ .msg a { color: var(--accent); text-underline-offset: 2px; }
21
+ iframe { display: block; width: 100%; height: 100%; border: 0; background: var(--background); }
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>PMOS meeting helper</title>
7
+ <link rel="stylesheet" href="panel.css">
8
+ </head>
9
+ <body>
10
+ <main id="msg" class="msg" role="status" aria-live="polite">Looking for PMOS.</main>
11
+ <script src="address.js"></script>
12
+ <script src="panel.js"></script>
13
+ </body>
14
+ </html>