create-pmos 0.236.0 → 0.237.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.236.0",
3
+ "version": "0.237.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@d874247
1
+ kernel-source: pmos@c49efbe
2
2
  assembled: 2026-10-14
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-14T10:47:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T11:55:57Z"}
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-14T10:47:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T11:55:57Z"}
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-14T10:47:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T11:55:57Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -76,6 +76,8 @@ A refresh waits for the sources it uses. The briefing waits for work assigned to
76
76
  - **Tell me**: a failed scheduled refresh adds one item to Needs you on the Briefing.
77
77
  - **Runs at once**: at most this many scheduled refreshes run together. A refresh you press is never held.
78
78
  - No scheduled refresh starts in your quiet hours; the background jobs keep to the day check's working hours.
79
+ - **One server runs the schedules.** When several PMOS servers run on one computer (for example an npm install and a checkout), only one of them runs the schedules and starts background runs; the others serve pages only. If that server stops, another one takes over within 3 minutes. A refresh you press runs on the server where you press it.
80
+ - **Quiet start**: a server starts no background run in its first 5 minutes, so the first pages load quickly. A missed refresh catches up after that.
79
81
 
80
82
  ### The refresh log
81
83
 
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+ /* schedule-owner.js — ONE PMOS SERVER PER COMPUTER RUNS THE BACKGROUND SCHEDULES (initiative background-runs-one-server).
3
+ *
4
+ * create({ file, pid, port, staleMs, quietMs, bootAt, alive })
5
+ * .claim(now) → true when this server owns the schedules now (it writes or refreshes the lock), false when another
6
+ * live server owns them
7
+ * .mayRun(now) → claim(now) and the quiet start is over: the tick may start background work
8
+ * .release() → drop the lock if this server holds it (on shutdown)
9
+ * lockFile(env) → where the lock lives
10
+ *
11
+ * Every server ticks its schedules (core documents, the initiative driver, the scan, the finder, the data refresh, …),
12
+ * and each of those can start headless Claude runs. Several servers on one computer (an npm install, a checkout, a
13
+ * baseline) each started their own, for the same repositories. The lock makes one of them the owner; the others serve
14
+ * pages only. The owner refreshes the lock on every tick. Another server takes over when the owner's process is gone
15
+ * or its lock is older than staleMs.
16
+ *
17
+ * WHERE: one file per tmux socket, in the temp folder. Every real server on a computer shares the `pmos` socket, so
18
+ * they share the lock; a test suite has a private socket (test-run.js), so its servers never wait on the PM's.
19
+ * PMOS_SCHEDULE_LOCK names the file explicitly.
20
+ *
21
+ * QUIET START: a server that has just started starts no background work for quietMs (5 minutes; PMOS_QUIET_START_MS),
22
+ * so the first page loads have the computer to themselves. Under PMOS_TICK_MS (the suites' fast clock) it is 0.
23
+ */
24
+ const fs = require('fs');
25
+ const os = require('os');
26
+ const path = require('path');
27
+
28
+ const STALE_MS = 3 * 60 * 1000;
29
+ const QUIET_MS = 5 * 60 * 1000;
30
+
31
+ function lockFile(env = process.env) {
32
+ if (env.PMOS_SCHEDULE_LOCK) return path.resolve(env.PMOS_SCHEDULE_LOCK);
33
+ const sock = String(env.PMOS_TMUX_SOCK || 'pmos').replace(/[^A-Za-z0-9._-]/g, '_');
34
+ return path.join(os.tmpdir(), `pmos-schedules-${sock}.lock`);
35
+ }
36
+
37
+ function quietMs(env = process.env) {
38
+ const n = Number(env.PMOS_QUIET_START_MS);
39
+ if (env.PMOS_QUIET_START_MS != null && env.PMOS_QUIET_START_MS !== '' && Number.isFinite(n) && n >= 0) return n;
40
+ return env.PMOS_TICK_MS ? 0 : QUIET_MS;
41
+ }
42
+
43
+ function pidAlive(pid) {
44
+ if (!Number.isInteger(pid) || pid <= 0) return false;
45
+ try { process.kill(pid, 0); return true; } catch (e) { return !!(e && e.code === 'EPERM'); }
46
+ }
47
+
48
+ function create(o = {}) {
49
+ const file = o.file || lockFile();
50
+ const pid = o.pid || process.pid;
51
+ const port = o.port || null;
52
+ const staleMs = o.staleMs == null ? STALE_MS : o.staleMs;
53
+ const quiet = o.quietMs == null ? quietMs() : o.quietMs;
54
+ const bootAt = o.bootAt == null ? Date.now() : o.bootAt;
55
+ const alive = o.alive || pidAlive;
56
+
57
+ const read = () => { try { const x = JSON.parse(fs.readFileSync(file, 'utf8')); return x && typeof x === 'object' ? x : null; } catch { return null; } };
58
+ const write = (now) => {
59
+ try {
60
+ fs.mkdirSync(path.dirname(file), { recursive: true });
61
+ const tmp = `${file}.${pid}.tmp`;
62
+ fs.writeFileSync(tmp, JSON.stringify({ pid, port, at: now }));
63
+ fs.renameSync(tmp, file);
64
+ } catch { /* an unwritable temp folder: claim() then answers from what it reads back */ }
65
+ };
66
+
67
+ function claim(now = Date.now()) {
68
+ const cur = read();
69
+ const free = !cur || cur.pid === pid || !alive(cur.pid) || !(typeof cur.at === 'number' && now - cur.at < staleMs);
70
+ if (!free) return false;
71
+ write(now);
72
+ const back = read();
73
+ // two servers that both found it free: the last rename wins, and the other sees that here
74
+ return !back || back.pid === pid;
75
+ }
76
+
77
+ return {
78
+ file,
79
+ claim,
80
+ mayRun(now = Date.now()) { return claim(now) && now - bootAt >= quiet; },
81
+ owner() { return read(); },
82
+ release() { const cur = read(); if (cur && cur.pid === pid) { try { fs.unlinkSync(file); } catch { /* gone */ } } },
83
+ };
84
+ }
85
+
86
+ module.exports = { create, lockFile, quietMs, pidAlive, STALE_MS, QUIET_MS };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.236.0",
3
+ "version": "0.237.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.236.0",
9
+ "version": "0.237.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.236.0",
3
+ "version": "0.237.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",
@@ -2688,7 +2688,11 @@ refreshRun = refreshRunLib.create({
2688
2688
  warn: (what, e) => warnWriteFail(what, e),
2689
2689
  });
2690
2690
  const gated = (id, fn) => () => { let g = { ok: true }; try { g = refreshRun.jobGate(id); } catch { g = { ok: true }; } if (g.ok) fn(); };
2691
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } try { refreshRun.tick(); } catch (e) { warnWriteFail('refresh tick', e); } gated('sync', () => autoSync.tick().catch(() => {}))(); dashboardScheduleTick(); gated('driver', driverTick)(); gated('scan', scanTick)(); gated('finder', reportFinderTick)(); gated('coredocs', coreDocsTick)(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2691
+ // background-runs-one-server: one server per computer runs the schedules (lib/schedule-owner.js), and none in its first 5 minutes.
2692
+ // data-refresh-settings: the seed schedules move to Settings › Data refresh (refreshRun.settings(), once) BEFORE checkDueSchedules reads
2693
+ // schedules.json, so a schedule that is moving never fires once more from the old list on the first tick after an upgrade.
2694
+ const scheduleOwner = require('./lib/schedule-owner.js').create({ port: PORT });
2695
+ try { const _tick = setInterval(() => { try { if (!scheduleOwner.mayRun()) return; } catch { /* no lock: run as before */ } try { refreshRun.settings(); } catch { /* the refresh tick tries again */ } checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } try { refreshRun.tick(); } catch (e) { warnWriteFail('refresh tick', e); } gated('sync', () => autoSync.tick().catch(() => {}))(); dashboardScheduleTick(); gated('driver', driverTick)(); gated('scan', scanTick)(); gated('finder', reportFinderTick)(); gated('coredocs', coreDocsTick)(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2692
2696
 
2693
2697
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2694
2698
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -9546,6 +9550,7 @@ if (require.main === module) {
9546
9550
  for (const l of listeners) { try { l.close(); } catch {} }
9547
9551
  try { tunnel.stop(); } catch {} // workspace-online-mode: the tunnel is this server's child and ends with it
9548
9552
  try { fs.unlinkSync(PID_FILE); } catch {} // stop being discoverable the moment we stop serving
9553
+ try { scheduleOwner.release(); } catch {} // another server can run the schedules at once
9549
9554
  process.exit(0);
9550
9555
  };
9551
9556
  process.on('SIGTERM', shutdown);
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Initiative background-runs-one-server: one PMOS server per computer runs the background schedules (a lock per tmux socket), and a server starts no background work in its first 5 minutes.",
3
+ "modules": ["schedule-owner.test.js"],
4
+ "seconds": { "schedule-owner.test.js": 25 }
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "_comment": "today.test.js times `pmos today` cold against a 1 s budget (0b). Beside three other suites on a CI runner it measured 1.0 to 1.2 s while it takes about 0.45 s alone, so it runs alone, after the shared suites.",
3
+ "exclusive": ["today.test.js"]
4
+ }