create-pmos 0.248.0 → 0.249.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.248.0",
3
+ "version": "0.249.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@3aea5ef
1
+ kernel-source: pmos@381bd87
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-15T07:57:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T09:38:27Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-15T07:57:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T09:38:27Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -78,12 +78,43 @@ function gitCap(root, args) {
78
78
  } catch (e) { return { ok: false, out: '', err: String((e && e.message) || e) }; }
79
79
  }
80
80
 
81
+ // `trunkRef` runs on every call, and each probe is a `git` subprocess started with spawnSync, which holds the whole
82
+ // event loop of a long-lived server (the workspace calls this on every Initiatives and Library request). The answer
83
+ // only changes when a ref the probe reads changes, so it is kept per root against those refs' own content: the loose
84
+ // ref files and the stat of packed-refs. A key that cannot be read falls back to asking git, exactly as before.
85
+ const TRUNK_CANDIDATES = ['origin/main', 'main', 'origin/master', 'master'];
86
+ const trunkMemo = new Map(); // root -> { key, value }
87
+ function trunkKey(root) {
88
+ const fs = require('fs');
89
+ const path = require('path');
90
+ const dotgit = path.join(root, '.git');
91
+ let common = dotgit;
92
+ if (!fs.statSync(dotgit).isDirectory()) {
93
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(dotgit, 'utf8'));
94
+ if (!m) throw new Error('no gitdir');
95
+ const dir = path.resolve(root, m[1].trim());
96
+ common = dir;
97
+ try { common = path.resolve(dir, fs.readFileSync(path.join(dir, 'commondir'), 'utf8').trim()); } catch { /* not a linked worktree */ }
98
+ }
99
+ const loose = (rel) => { try { return fs.readFileSync(path.join(common, rel), 'utf8').trim(); } catch { return '-'; } };
100
+ let packed = '-';
101
+ try { const st = fs.statSync(path.join(common, 'packed-refs')); packed = `${st.size}:${st.mtimeMs}:${st.ino}`; } catch { /* no packed refs */ }
102
+ const parts = TRUNK_CANDIDATES.map((c) => loose(c.startsWith('origin/') ? `refs/remotes/${c}` : `refs/heads/${c}`));
103
+ return [packed, ...parts].join('|');
104
+ }
105
+
81
106
  function trunkRef(root) {
82
- for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
107
+ let key = null;
108
+ try { key = trunkKey(root); } catch { key = null; }
109
+ const hit = trunkMemo.get(root);
110
+ if (key && hit && hit.key === key) return hit.value;
111
+ let value = null;
112
+ for (const c of TRUNK_CANDIDATES) {
83
113
  const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
84
- if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
114
+ if (r.ok && r.out.trim()) { value = { ref: c, head: r.out.trim() }; break; }
85
115
  }
86
- return null;
116
+ if (key) trunkMemo.set(root, { key, value });
117
+ return value;
87
118
  }
88
119
 
89
120
  // ONE `git log` per trunk head, memoised on that sha — ~0.3 s and ~800 KB on this repository's
@@ -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-15T07:57:22Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-15T09:38:27Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -78,12 +78,43 @@ function gitCap(root, args) {
78
78
  } catch (e) { return { ok: false, out: '', err: String((e && e.message) || e) }; }
79
79
  }
80
80
 
81
+ // `trunkRef` runs on every call, and each probe is a `git` subprocess started with spawnSync, which holds the whole
82
+ // event loop of a long-lived server (the workspace calls this on every Initiatives and Library request). The answer
83
+ // only changes when a ref the probe reads changes, so it is kept per root against those refs' own content: the loose
84
+ // ref files and the stat of packed-refs. A key that cannot be read falls back to asking git, exactly as before.
85
+ const TRUNK_CANDIDATES = ['origin/main', 'main', 'origin/master', 'master'];
86
+ const trunkMemo = new Map(); // root -> { key, value }
87
+ function trunkKey(root) {
88
+ const fs = require('fs');
89
+ const path = require('path');
90
+ const dotgit = path.join(root, '.git');
91
+ let common = dotgit;
92
+ if (!fs.statSync(dotgit).isDirectory()) {
93
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(dotgit, 'utf8'));
94
+ if (!m) throw new Error('no gitdir');
95
+ const dir = path.resolve(root, m[1].trim());
96
+ common = dir;
97
+ try { common = path.resolve(dir, fs.readFileSync(path.join(dir, 'commondir'), 'utf8').trim()); } catch { /* not a linked worktree */ }
98
+ }
99
+ const loose = (rel) => { try { return fs.readFileSync(path.join(common, rel), 'utf8').trim(); } catch { return '-'; } };
100
+ let packed = '-';
101
+ try { const st = fs.statSync(path.join(common, 'packed-refs')); packed = `${st.size}:${st.mtimeMs}:${st.ino}`; } catch { /* no packed refs */ }
102
+ const parts = TRUNK_CANDIDATES.map((c) => loose(c.startsWith('origin/') ? `refs/remotes/${c}` : `refs/heads/${c}`));
103
+ return [packed, ...parts].join('|');
104
+ }
105
+
81
106
  function trunkRef(root) {
82
- for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
107
+ let key = null;
108
+ try { key = trunkKey(root); } catch { key = null; }
109
+ const hit = trunkMemo.get(root);
110
+ if (key && hit && hit.key === key) return hit.value;
111
+ let value = null;
112
+ for (const c of TRUNK_CANDIDATES) {
83
113
  const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
84
- if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
114
+ if (r.ok && r.out.trim()) { value = { ref: c, head: r.out.trim() }; break; }
85
115
  }
86
- return null;
116
+ if (key) trunkMemo.set(root, { key, value });
117
+ return value;
87
118
  }
88
119
 
89
120
  // ONE `git log` per trunk head, memoised on that sha — ~0.3 s and ~800 KB on this repository's
@@ -78,12 +78,43 @@ function gitCap(root, args) {
78
78
  } catch (e) { return { ok: false, out: '', err: String((e && e.message) || e) }; }
79
79
  }
80
80
 
81
+ // `trunkRef` runs on every call, and each probe is a `git` subprocess started with spawnSync, which holds the whole
82
+ // event loop of a long-lived server (the workspace calls this on every Initiatives and Library request). The answer
83
+ // only changes when a ref the probe reads changes, so it is kept per root against those refs' own content: the loose
84
+ // ref files and the stat of packed-refs. A key that cannot be read falls back to asking git, exactly as before.
85
+ const TRUNK_CANDIDATES = ['origin/main', 'main', 'origin/master', 'master'];
86
+ const trunkMemo = new Map(); // root -> { key, value }
87
+ function trunkKey(root) {
88
+ const fs = require('fs');
89
+ const path = require('path');
90
+ const dotgit = path.join(root, '.git');
91
+ let common = dotgit;
92
+ if (!fs.statSync(dotgit).isDirectory()) {
93
+ const m = /^gitdir:\s*(.+)\s*$/m.exec(fs.readFileSync(dotgit, 'utf8'));
94
+ if (!m) throw new Error('no gitdir');
95
+ const dir = path.resolve(root, m[1].trim());
96
+ common = dir;
97
+ try { common = path.resolve(dir, fs.readFileSync(path.join(dir, 'commondir'), 'utf8').trim()); } catch { /* not a linked worktree */ }
98
+ }
99
+ const loose = (rel) => { try { return fs.readFileSync(path.join(common, rel), 'utf8').trim(); } catch { return '-'; } };
100
+ let packed = '-';
101
+ try { const st = fs.statSync(path.join(common, 'packed-refs')); packed = `${st.size}:${st.mtimeMs}:${st.ino}`; } catch { /* no packed refs */ }
102
+ const parts = TRUNK_CANDIDATES.map((c) => loose(c.startsWith('origin/') ? `refs/remotes/${c}` : `refs/heads/${c}`));
103
+ return [packed, ...parts].join('|');
104
+ }
105
+
81
106
  function trunkRef(root) {
82
- for (const c of ['origin/main', 'main', 'origin/master', 'master']) {
107
+ let key = null;
108
+ try { key = trunkKey(root); } catch { key = null; }
109
+ const hit = trunkMemo.get(root);
110
+ if (key && hit && hit.key === key) return hit.value;
111
+ let value = null;
112
+ for (const c of TRUNK_CANDIDATES) {
83
113
  const r = gitCap(root, ['rev-parse', '--verify', '--quiet', c]);
84
- if (r.ok && r.out.trim()) return { ref: c, head: r.out.trim() };
114
+ if (r.ok && r.out.trim()) { value = { ref: c, head: r.out.trim() }; break; }
85
115
  }
86
- return null;
116
+ if (key) trunkMemo.set(root, { key, value });
117
+ return value;
87
118
  }
88
119
 
89
120
  // ONE `git log` per trunk head, memoised on that sha — ~0.3 s and ~800 KB on this repository's
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.248.0",
3
+ "version": "0.249.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.248.0",
9
+ "version": "0.249.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.248.0",
3
+ "version": "0.249.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",
@@ -5864,19 +5864,48 @@ function fsList(dir) {
5864
5864
  const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
5865
5865
  '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.map': 'application/json',
5866
5866
  '.svg': 'image/svg+xml', '.woff2': 'font/woff2' };
5867
+ // Compression and validators (workspace-event-loop-lag). The app shell is 5.7 MB of script and style and a library answer
5868
+ // is about 1 MB of JSON, and none of it was compressed, so a phone or an Online-address visit paid for every byte on every
5869
+ // load. A response is gzipped only when the client asks for it and the body is text over the threshold. Gzip runs on the
5870
+ // thread pool and never blocks the loop; a served file's gzip is made once per mtime and kept.
5871
+ const zlib = require('zlib');
5872
+ const GZIP_MIN_BYTES = 1024;
5873
+ const GZIP_TYPES = /^(?:text\/|application\/(?:json|javascript|manifest\+json)|image\/svg\+xml)/;
5874
+ const wantsGzip = (res) => { const q = res && res.req; return !!(q && q.headers && /\bgzip\b/i.test(String(q.headers['accept-encoding'] || ''))); };
5875
+ const fileMemo = new Map(); // file -> { sig, buf, etag, gz }
5876
+ const FILE_MEMO_MAX = 400;
5867
5877
  function serveFile(res, file, type, noCache) {
5868
- fs.readFile(file, (err, buf) => {
5869
- if (err) { res.writeHead(404); res.end('not found'); return; }
5870
- const headers = { 'Content-Type': type || TYPES[path.extname(file)] || 'application/octet-stream' };
5871
- // The app SHELL (index.html + the /pm/ module graph) must revalidate every load: without this a tab left open
5872
- // across a `git merge` + server restart keeps running the pre-merge JS, so a fix that IS being
5873
- // served never reaches the screen. `no-cache` = "always revalidate", not "never store".
5874
- // Every script and stylesheet revalidates too, not only /pm/: a /lib/ module served with no header was kept by the
5875
- // browser's heuristic cache, and at the Online address by Cloudflare's edge, so an updated workspace still drew
5876
- // the old labels (workspace-kit-update). Images and fonts keep the default.
5877
- if (noCache || /\.(js|mjs|css)$/.test(file)) headers['Cache-Control'] = 'no-cache';
5878
- res.writeHead(200, headers);
5879
- res.end(buf);
5878
+ fs.stat(file, (serr, st) => {
5879
+ if (serr || !st.isFile()) { res.writeHead(404); res.end('not found'); return; }
5880
+ const sig = `${st.size}:${st.mtimeMs}`;
5881
+ const send = (m) => {
5882
+ const ctype = type || TYPES[path.extname(file)] || 'application/octet-stream';
5883
+ const headers = { 'Content-Type': ctype, 'ETag': m.etag };
5884
+ // The app SHELL (index.html + the /pm/ module graph) must revalidate every load: without this a tab left open
5885
+ // across a `git merge` + server restart keeps running the pre-merge JS, so a fix that IS being
5886
+ // served never reaches the screen. `no-cache` = "always revalidate", not "never store".
5887
+ // Every script and stylesheet revalidates too, not only /pm/: a /lib/ module served with no header was kept by the
5888
+ // browser's heuristic cache, and at the Online address by Cloudflare's edge, so an updated workspace still drew
5889
+ // the old labels (workspace-kit-update). Images and fonts keep the default.
5890
+ // The ETag keeps that rule cheap: the revalidation of an unchanged file is a 304 with no body.
5891
+ if (noCache || /\.(js|mjs|css)$/.test(file)) headers['Cache-Control'] = 'no-cache';
5892
+ if (GZIP_TYPES.test(ctype)) headers['Vary'] = 'Accept-Encoding';
5893
+ const inm = res.req && res.req.headers && res.req.headers['if-none-match'];
5894
+ if (inm && String(inm).split(',').some((t) => t.trim() === m.etag)) { res.writeHead(304, headers); res.end(); return; }
5895
+ if (m.gz && wantsGzip(res)) { headers['Content-Encoding'] = 'gzip'; headers['Content-Length'] = m.gz.length; res.writeHead(200, headers); res.end(m.gz); return; }
5896
+ res.writeHead(200, headers);
5897
+ res.end(m.buf);
5898
+ };
5899
+ const hit = fileMemo.get(file);
5900
+ if (hit && hit.sig === sig) return send(hit);
5901
+ fs.readFile(file, (err, buf) => {
5902
+ if (err) { res.writeHead(404); res.end('not found'); return; }
5903
+ const m = { sig, buf, etag: `W/"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`, gz: null };
5904
+ const ctype = type || TYPES[path.extname(file)] || '';
5905
+ const finish = () => { if (fileMemo.size >= FILE_MEMO_MAX) fileMemo.clear(); fileMemo.set(file, m); send(m); };
5906
+ if (buf.length >= GZIP_MIN_BYTES && GZIP_TYPES.test(ctype)) zlib.gzip(buf, (e, gz) => { if (!e) m.gz = gz; finish(); });
5907
+ else finish();
5908
+ });
5880
5909
  });
5881
5910
  }
5882
5911
  // claims-ledger — the claims ledger and its local checks. Local only: no key, no network. The ledger is file-mode
@@ -5986,8 +6015,17 @@ function readBody(req) {
5986
6015
  return new Promise((resolve) => { let b = ''; req.on('data', (c) => (b += c)); req.on('end', () => resolve(b)); });
5987
6016
  }
5988
6017
  function json(res, obj, code = 200) {
6018
+ const body = JSON.stringify(obj);
6019
+ if (body.length >= GZIP_MIN_BYTES && wantsGzip(res)) {
6020
+ // Async, so a 1 MB library answer costs the loop nothing beyond the stringify (workspace-event-loop-lag).
6021
+ return zlib.gzip(body, (e, gz) => {
6022
+ if (e) { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(body); return; }
6023
+ res.writeHead(code, { 'Content-Type': 'application/json', 'Content-Encoding': 'gzip', 'Vary': 'Accept-Encoding', 'Content-Length': gz.length });
6024
+ res.end(gz);
6025
+ });
6026
+ }
5989
6027
  res.writeHead(code, { 'Content-Type': 'application/json' });
5990
- res.end(JSON.stringify(obj));
6028
+ res.end(body);
5991
6029
  }
5992
6030
 
5993
6031
  async function handleRequest(req, res) {
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "workspace-event-loop-lag: the server gzips text responses on request, sends an ETag with every served file, and reads the delivery trunk once per ref state (http-compression).",
3
+ "modules": ["http-compression.test.js"],
4
+ "seconds": { "http-compression.test.js": 8 }
5
+ }