create-pmos 0.246.0 → 0.248.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.246.0",
3
+ "version": "0.248.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@0e2738a
1
+ kernel-source: pmos@3aea5ef
2
2
  assembled: 2026-10-15
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-15T05:12:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:57:22Z"}
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-15T05:12:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:57:22Z"}
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-15T05:12:16Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T07:57:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  /* calendar-cli.js — `pmos calendar …`: the calendar events an agent session read, saved for Briefing and Calendar
3
3
  * (workspace-data-freshness R8).
4
- * pmos calendar import --from <file|-> --sensitivity public|internal|confidential|private [--dry-run] [--json]
4
+ * pmos calendar import --from <file|-> --sensitivity public|internal|confidential|private [--window <from>..<to>] [--dry-run] [--json]
5
5
  * pmos calendar status [--json]
6
6
  * This command never talks to a calendar. The calendar-sync skill reads it in an agent session, with the PM's own
7
7
  * connection, and hands the events to `import`. The import goes through scripts/calendar-import.js unchanged: hidden text
@@ -18,13 +18,14 @@ const L = require(repoScript('trust/ledger.js'));
18
18
  const COMMANDS = { calendar: ['import', 'status'] };
19
19
  const USAGE = [
20
20
  'usage: pmos calendar import|status — the calendar events an agent session read for Briefing and Calendar',
21
- ' pmos calendar import --from <file|-> --sensitivity <label> [--dry-run] save the events (the calendar-sync skill runs this)',
21
+ ' pmos calendar import --from <file|-> --sensitivity <label> [--window YYYY-MM-DD..YYYY-MM-DD] [--dry-run] save the events (the calendar-sync skill runs this)',
22
+ ' --window: the days the events cover. Saved events in those days that are not in the file are removed (moved, declined, cancelled).',
22
23
  ' pmos calendar status how many events are saved, and when they were read',
23
24
  ' every command: [--json]',
24
25
  'This command never calls a calendar. Your agent reads it with your own connection.',
25
26
  ];
26
27
  const BOOL = new Set(['json', 'dry-run', 'help']);
27
- const VALUE = new Set(['from', 'sensitivity']);
28
+ const VALUE = new Set(['from', 'sensitivity', 'window']);
28
29
 
29
30
  function parseArgs(argv) {
30
31
  const out = { _: [] };
@@ -69,16 +70,25 @@ async function run(argv, { say = console.log, env = process.env, stateDir, stdin
69
70
  try { raw = JSON.parse(readInput(a.from, stdin)); } catch (e) { say(`✗ the events file is not JSON: ${String(e.message).slice(0, 160)}`); return 1; }
70
71
  const list = Array.isArray(raw) ? raw : raw && Array.isArray(raw.events) ? raw.events : null;
71
72
  if (!list) { say('✗ the file must hold a list of events, or { "events": [ … ] }'); return 1; }
73
+ let window;
74
+ if (a.window) {
75
+ const m = /^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/.exec(a.window);
76
+ if (!m) { say('✗ --window must look like 2026-10-15..2026-10-29'); return 2; }
77
+ window = { from: m[1], to: m[2] };
78
+ try { CI.checkWindow(window); } catch (e) { say(`✗ ${e.message}`); return 2; }
79
+ }
72
80
  const at = now().toISOString();
73
81
  let evs;
74
82
  try { evs = CI.importEvents(list, { fetchedAt: at, sensitivity: a.sensitivity }); }
75
83
  catch (e) { say(`✗ ${e.message}`); return 1; }
76
84
  if (a['dry-run']) { out({ ok: true, dryRun: true, count: evs.length }, `would save ${evs.length} events (dry run, nothing written)`); return 0; }
77
85
  const actor = L.actorFrom({ env, isTTY: isTTY == null ? !!(process.stdin.isTTY && process.stdout.isTTY) : isTTY });
78
- try { CI.appendImport(file, evs, actor, at); }
86
+ let lines;
87
+ try { lines = CI.appendImport(file, evs, actor, at, { window }); }
79
88
  catch (e) { say(`✗ the events could not be saved: ${String(e.message).slice(0, 160)}`); return 1; }
89
+ const removed = lines.length - evs.length;
80
90
  const flagged = evs.filter((e) => e.findings && e.findings.length).length;
81
- out({ ok: true, count: evs.length, flagged, read_at: at }, `✓ saved ${evs.length} calendar events${flagged ? ` (${flagged} carried hidden or suspicious text, marked)` : ''}`);
91
+ out({ ok: true, count: evs.length, removed, flagged, read_at: at }, `✓ saved ${evs.length} calendar events${removed ? `, removed ${removed} that the calendar no longer holds in those days` : ''}${flagged ? ` (${flagged} carried hidden or suspicious text, marked)` : ''}`);
82
92
  return 0;
83
93
  }
84
94
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * importEvents(rawEvents, { fetchedAt, sensitivity }) → [event] pure; a missing or unknown sensitivity is refused
9
9
  * foldEvents(lines) → [event] current events; the PM's notes survive a refresh
10
- * appendImport(file, events, actor, now) → lines ledger append (scripts/trust/ledger.js)
10
+ * appendImport(file, events, actor, now, {window}) → lines ledger append (scripts/trust/ledger.js); a window removes the saved events of its days the import no longer holds
11
11
  * appendNote(file, eventId, text, actor, now) → line a PM note on one event
12
12
  * readLines(file) · cacheMarkdown(events, dir, now) the Markdown cache is the PM's trust/import-calendar.js, unchanged
13
13
  */
@@ -52,6 +52,7 @@ function foldEvents(lines) {
52
52
  const by = new Map();
53
53
  for (const l of lines || []) {
54
54
  if (!l) continue;
55
+ if (l.kind === 'calendar_event_removed') { by.delete(l.event_id); continue; }
55
56
  if (l.kind === 'calendar_event' && l.event && l.event.id) {
56
57
  const prev = by.get(l.event.id);
57
58
  by.set(l.event.id, { ...l.event, agent_prep_notes: prev ? prev.agent_prep_notes : l.event.agent_prep_notes || '', pm_notes: prev ? prev.pm_notes : [] });
@@ -64,9 +65,33 @@ function foldEvents(lines) {
64
65
 
65
66
  const readLines = (file) => L.read(file);
66
67
 
67
- function appendImport(file, events, actor, now) {
68
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
69
+ /** A window is { from, to }, local days YYYY-MM-DD, from <= to. Throws on anything else. */
70
+ function checkWindow(w) {
71
+ if (!w || !DAY.test(String(w.from)) || !DAY.test(String(w.to)) || String(w.from) > String(w.to)) throw new Error('window needs from and to as YYYY-MM-DD, from not after to');
72
+ return { from: String(w.from), to: String(w.to) };
73
+ }
74
+ /** The saved events a windowed import replaces: those that start inside the window (the day written in their `at`) and are not in the import. Pure. */
75
+ function supersededIds(saved, imported, window) {
76
+ const w = checkWindow(window);
77
+ const keep = new Set((imported || []).map((e) => e.id));
78
+ return (saved || []).filter((e) => { const d = String(e.at || '').slice(0, 10); return d >= w.from && d <= w.to && !keep.has(e.id); }).map((e) => e.id);
79
+ }
80
+
81
+ // opts.window: the days this import covers. Saved events in those days that the import does not hold are removed (calendar_event_removed),
82
+ // so a rescheduled, declined or cancelled meeting leaves the ledger. The ledger itself stays append only.
83
+ function appendImport(file, events, actor, now, opts = {}) {
84
+ let gone = [];
85
+ if (opts.window) {
86
+ const w = checkWindow(opts.window);
87
+ if (!events.length) throw new Error('a window import needs at least one event: an empty read is not a calendar with no meetings');
88
+ gone = supersededIds(foldEvents(readLines(file)), events, w);
89
+ }
68
90
  fs.mkdirSync(path.dirname(file), { recursive: true });
69
- return events.map((event) => L.append(file, { id: L.newId(), kind: 'calendar_event', why: 'calendar import (reported, untrusted)', at: now || new Date().toISOString(), event }, actor));
91
+ const at = now || new Date().toISOString();
92
+ const lines = events.map((event) => L.append(file, { id: L.newId(), kind: 'calendar_event', why: 'calendar import (reported, untrusted)', at, event }, actor));
93
+ for (const event_id of gone) lines.push(L.append(file, { id: L.newId(), kind: 'calendar_event_removed', why: 'not in the import for its days (rescheduled, declined or cancelled)', at, event_id }, actor));
94
+ return lines;
70
95
  }
71
96
 
72
97
  function appendNote(file, eventId, text, actor, now) {
@@ -81,4 +106,4 @@ function cacheMarkdown(events, dir, now) {
81
106
  return sampleCache.importEvents(events.map((e) => ({ id: e.id, title: e.title, start: e.at, end: e.end || e.at, attendees: e.attendees, initiative: e.initiative })), dir, now);
82
107
  }
83
108
 
84
- module.exports = { SENSITIVITY, importEvents, foldEvents, appendImport, appendNote, readLines, cacheMarkdown };
109
+ module.exports = { SENSITIVITY, importEvents, foldEvents, appendImport, supersededIds, checkWindow, appendNote, readLines, cacheMarkdown };
@@ -26,13 +26,21 @@ const path = require('path');
26
26
  // matching (which is the same skip-looks-like-pass failure, arriving later).
27
27
  function playwrightCandidates() {
28
28
  const roots = [process.env.PLAYWRIGHT_BROWSERS_PATH, '/opt/pw-browsers',
29
- path.join(process.env.HOME || '', '.cache', 'ms-playwright')].filter(Boolean);
29
+ path.join(process.env.HOME || '', '.cache', 'ms-playwright'),
30
+ path.join(process.env.HOME || '', 'Library', 'Caches', 'ms-playwright')].filter(Boolean);
30
31
  const out = [];
31
32
  for (const root of roots) {
32
33
  let entries = [];
33
34
  try { entries = fs.readdirSync(root); } catch { continue; }
34
35
  for (const dir of entries.sort().reverse()) { // newest version first
35
36
  if (!/^chromium/.test(dir)) continue;
37
+ // On a Mac or Windows PC a full Chromium shows in the Dock or taskbar, so a headless shell goes first there. Linux
38
+ // keeps its order: CI runs the browser suites on the browser it always picked.
39
+ if (process.platform !== 'linux') out.push(
40
+ path.join(root, dir, 'chrome-headless-shell-mac-arm64', 'chrome-headless-shell'),
41
+ path.join(root, dir, 'chrome-headless-shell-mac-x64', 'chrome-headless-shell'),
42
+ path.join(root, dir, 'chrome-headless-shell-win64', 'chrome-headless-shell.exe'),
43
+ );
36
44
  out.push(
37
45
  path.join(root, dir, 'chrome-linux', 'chrome'),
38
46
  path.join(root, dir, 'chrome-linux', 'headless_shell'),
@@ -44,15 +52,22 @@ function playwrightCandidates() {
44
52
  return out;
45
53
  }
46
54
 
55
+ /* THE PM'S OWN CHROME IS NOT A TEST BROWSER. A suite that launches /Applications/Google Chrome.app opens a second Chrome
56
+ * that shows in the Dock and the taskbar for a few seconds, so a parallel run makes Chrome icons flicker on the PM's screen
57
+ * (and they cannot be opened). Only a headless browser is picked unless PMOS_CHROME / PMOS_CHROMIUM names one, or
58
+ * PMOS_ALLOW_SYSTEM_CHROME=1 says the visible app is fine (initiative chrome-dock-flicker). */
47
59
  function findBrowser() {
60
+ const visible = process.env.PMOS_ALLOW_SYSTEM_CHROME === '1' ? [
61
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
62
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
63
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
64
+ ] : [];
48
65
  const cands = [
49
66
  process.env.PMOS_CHROME,
50
67
  process.env.PMOS_CHROMIUM,
51
68
  ...playwrightCandidates(),
52
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
53
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
54
69
  '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',
55
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
70
+ ...visible,
56
71
  ].filter(Boolean);
57
72
  return cands.find((c) => { try { fs.accessSync(c, fs.constants.X_OK); return true; } catch { return false; } });
58
73
  }
@@ -2,7 +2,8 @@
2
2
  /* routes-calendar-import.js — cached calendar events, supplied by a session (console3 wave E2a).
3
3
  *
4
4
  * GET /api/calendar-import/events → { ok, events } the folded cache
5
- * POST /api/calendar-import/events { events, sensitivity } → { ok, count } page press; sensitivity is required
5
+ * POST /api/calendar-import/events { events, sensitivity, window? } → { ok, count, removed } page press; sensitivity is required;
6
+ * window { from, to } (local days) removes the saved events of those days the import does not hold
6
7
  * POST /api/calendar-import/note { event_id, text } → { ok } the PM's note on an event
7
8
  *
8
9
  * Nothing here fetches a calendar: PMOS holds no credentials. Events come in the body. Imports are untrusted data
@@ -28,9 +29,11 @@ function importRoute(req, body, runtimeDir, now = new Date()) {
28
29
  let evs;
29
30
  try { evs = CI.importEvents(body.events, { fetchedAt: now.toISOString(), sensitivity: body.sensitivity }); }
30
31
  catch (e) { return fail(400, e.message); }
31
- try { CI.appendImport(fileOf(runtimeDir), evs, L.actorFrom({ ui: true }), now.toISOString()); }
32
+ let removed = 0;
33
+ if (body.window) { try { CI.checkWindow(body.window); } catch (e) { return fail(400, e.message); } if (!evs.length) return fail(400, 'a window import needs at least one event'); }
34
+ try { removed = CI.appendImport(fileOf(runtimeDir), evs, L.actorFrom({ ui: true }), now.toISOString(), { window: body.window }).length - evs.length; }
32
35
  catch (e) { return fail(500, e.message); }
33
- return { status: 200, body: { ok: true, count: evs.length } };
36
+ return { status: 200, body: { ok: true, count: evs.length, removed } };
34
37
  }
35
38
 
36
39
  function noteRoute(req, body, runtimeDir, now = new Date()) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.246.0",
3
+ "version": "0.248.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.246.0",
9
+ "version": "0.248.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.246.0",
3
+ "version": "0.248.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -24,8 +24,8 @@ Procedure:
24
24
  1. List the PM's events from the start of today to the end of the 14th day after today, from the primary calendar. Fetch every page. Skip an event the PM declined.
25
25
  2. Write the events as JSON in the scratch folder, for example `<scratch>/calendar.json`, as a list with one object per event:
26
26
  `{"id", "title", "start", "end", "attendees": ["Display Name", …], "initiative": "<initiative id or null>"}`
27
- `id` is the calendar's own id for that event (for a repeating meeting, the id of that one occurrence). Use the same id on every run, never a made-up one: PMOS keeps one copy per id.
27
+ `id` is the calendar's own id for that event (for a repeating meeting, the id of that one occurrence). Use the same id on every run, never a made-up one: PMOS keeps one copy per id. If an id changes anyway, the window import removes the old copy.
28
28
  `start` and `end` are ISO times with the offset, for example `2026-10-12T09:00:00+07:00`. An all-day event has the date only.
29
- 3. Run `pmos calendar import --from <scratch>/calendar.json --sensitivity internal`. If it refuses the file, fix what it names and run it once more.
29
+ 3. Run `pmos calendar import --from <scratch>/calendar.json --sensitivity internal --window <today>..<day 14>`, with both days as `YYYY-MM-DD`. The window makes PMOS drop a saved event in those days that is not in the file, so a meeting that moved, was declined or was cancelled leaves its old slot. If it refuses the file, fix what it names and run it once more.
30
30
  4. Delete the scratch file.
31
31
  5. Report in four short lists: Imported (the count, and the first and last day), Left out (the count of private events), Not reached, and Needs you (only when something blocked the pull).
@@ -8,6 +8,7 @@
8
8
  "test.js"
9
9
  ],
10
10
  "modules": [
11
+ "find-browser.test.js",
11
12
  "own-profile.test.js",
12
13
  "stage-tools.test.js",
13
14
  "report-template-cadence.test.js",
@@ -35,6 +36,7 @@
35
36
  "work-assigned.test.js",
36
37
  "briefing-real-data.test.js",
37
38
  "calendar-events.test.js",
39
+ "calendar-supersede.test.js",
38
40
  "tracker-read.test.js",
39
41
  "item-steps.test.js",
40
42
  "status-facts-panel.test.js",
@@ -186,6 +188,7 @@
186
188
  "work-assigned.test.js": 1,
187
189
  "briefing-real-data.test.js": 1,
188
190
  "calendar-events.test.js": 1,
191
+ "calendar-supersede.test.js": 1,
189
192
  "tracker-read.test.js": 1,
190
193
  "pm-brief-page-test.js": 30,
191
194
  "item-steps.test.js": 1,