create-pmos 0.95.0 → 0.96.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.95.0",
3
+ "version": "0.96.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@1e39390
1
+ kernel-source: pmos@a11a7a5
2
2
  assembled: 2026-10-07
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-07T23:23:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:35:56Z"}
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-07T23:23:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:35:56Z"}
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-07T23:23:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T23:35:56Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -39,6 +39,7 @@ Settings holds settings only, in eight sections. On a wide screen they are cards
39
39
  - **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** at the top of the page to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, one button shows: **Update to** and the version. It updates every part that is older, in this order:
40
40
  - **The kit** in the current repository, when it is older. This does what `npx create-pmos@latest --update` does in a terminal. It changes only the kit's own files under `pmos/` (skills, scripts, templates and guides). Your records in `state/`, `planning/` and `okf/product/` do not change. The last lines of the output show under the button. The changed files are under `pmos/`, which Sync treats as records, so Sync (or automatic sync) sends them with your other records. To read them first, look at `git diff pmos/` before you sync.
41
41
  - **The workspace app**, when it is older. This does what `pmos upgrade` does in a terminal. The button asks you first. The workspace stops, installs the new version and starts again, and the page reloads when the new version starts. The reloaded page runs the new version's code: each start gives the page files a new address, so neither the browser nor the Online address's tunnel can keep the old ones. You do not need a terminal or a hard refresh. Sessions keep running in tmux. It works on the computer that runs the workspace and at its Online address, because the page comes back there after the restart. It also works on a phone or public address that signs in with a token set in `PMOS_TOKEN`, which the workspace keeps. When the workspace made its own token at start, the token changes on the restart, so the page refuses and says where to press it. When the workspace runs from a PMOS checkout, update it with `git pull` instead.
42
+ - **Testing an update before it is published.** Set `PMOS_UPDATE_SOURCE` to a built create-pmos package on the same computer: its folder, or the `.tgz` that `npm pack` makes. Then **Check for updates** reads that package's version and does not ask npm, and both updates install that package. The Updates card names the package it checks. A checkout may update the app this way. When the path is wrong, the card says why and does not fall back to npm. Unset the variable, or set it to `npm`, to use the registry again.
42
43
 
43
44
  A new screen or a renamed menu comes with the workspace app, not with the kit.
44
45
 
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * readKit(root) → { present:false } | { present:true, version, kernelSource, installed, mode }
5
5
  * status({ root, appVersion, appDir }) → { app: { version, fromCheckout }, kit } local only, no network
6
- * latest({ exec }) → Promise<{ ok, version } | { ok:false, error }> npm view create-pmos version
6
+ * source(env) → { kind:'npm' } | { kind:'local', path } | { kind:'local', path, error }
7
+ * latest({ exec, source }) → Promise<{ ok, version } | { ok:false, error }> npm view create-pmos version
7
8
  * compare(st, latestVersion) → { appBehind, kitBehind }
8
9
  * updateKit(root, { version, exec }) → Promise<{ ok, output, kit } | { ok:false, error, output }>
9
10
  * upgradeApp(root, { version, logFile, spawn, env }) → { ok, log } | { ok:false, error }
@@ -14,6 +15,10 @@
14
15
  * check npm view create-pmos version
15
16
  * kit npx -y create-pmos@<version> --update <repo> (the kernel only; state/ and planning/ untouched)
16
17
  * app npx -y create-pmos@<version> workspace --reinstall <repo> (what `pmos upgrade` runs)
18
+ * LOCAL SOURCE. PMOS_UPDATE_SOURCE names a built create-pmos package on this computer: its folder (with package.json, bin/
19
+ * and payload/ or workspace/) or its .tgz from `npm pack`. The check then reads that package's version instead of asking
20
+ * the registry, and the update runs `npx -y <that path> ...` in place of `npx -y create-pmos@<version> ...`. Nothing
21
+ * reaches the network, so an update can be tried before it is published. Unset means npm (the default).
17
22
  * The version is the one the check found, so what is installed is what the PM was shown, not whatever `latest` is a
18
23
  * minute later. The app upgrade stops this server (create-pmos runs `pmos stop`), so it runs DETACHED, writes its output
19
24
  * to a log file, and the page waits for the server to come back.
@@ -81,8 +86,34 @@ function fromCheckout(appDir) {
81
86
  try { return fs.existsSync(path.join(String(appDir), '..', '.git')); } catch { return false; }
82
87
  }
83
88
 
84
- function status({ root, appVersion, appDir }) {
85
- return { app: { version: appVersion || null, fromCheckout: fromCheckout(appDir) }, kit: readKit(root) };
89
+ /**
90
+ * Where "latest" comes from. Unset or "npm" is the registry. Anything else is a path to a built create-pmos package: a
91
+ * folder or a .tgz. `error` names what is wrong with it, so the page can say so instead of silently using npm.
92
+ */
93
+ function source(env = process.env) {
94
+ const raw = String((env && env.PMOS_UPDATE_SOURCE) || '').trim();
95
+ if (!raw || raw.toLowerCase() === 'npm') return { kind: 'npm' };
96
+ const p = path.resolve(raw);
97
+ let st;
98
+ try { st = fs.statSync(p); } catch { return { kind: 'local', path: p, error: `PMOS_UPDATE_SOURCE points to ${p}, which does not exist` }; }
99
+ if (st.isDirectory()) {
100
+ let pkg;
101
+ try { pkg = JSON.parse(fs.readFileSync(path.join(p, 'package.json'), 'utf8')); } catch { pkg = null; }
102
+ if (!pkg || pkg.name !== PKG_NAME) return { kind: 'local', path: p, error: `${p} is not a ${PKG_NAME} package folder (no package.json named ${PKG_NAME})` };
103
+ if (!fs.existsSync(path.join(p, 'payload')) && !fs.existsSync(path.join(p, 'workspace'))) return { kind: 'local', path: p, error: `${p} is not a built package: it has no payload/ or workspace/ folder. Point PMOS_UPDATE_SOURCE at the .tgz from \`npm pack\` instead` };
104
+ return { kind: 'local', path: p, version: validVersion(pkg.version) ? pkg.version : null };
105
+ }
106
+ const m = /^create-pmos-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\.tgz$/.exec(path.basename(p));
107
+ if (!m) return { kind: 'local', path: p, error: `${p} is not a create-pmos-<version>.tgz file` };
108
+ return { kind: 'local', path: p, version: m[1] };
109
+ }
110
+
111
+ /** What npx installs: the registry package at a version, or the local path. */
112
+ const specOf = (src, version) => (src && src.kind === 'local' ? src.path : `${PKG_NAME}@${version}`);
113
+
114
+ function status({ root, appVersion, appDir, env }) {
115
+ const src = source(env);
116
+ return { app: { version: appVersion || null, fromCheckout: fromCheckout(appDir) }, kit: readKit(root), source: src.kind === 'local' ? { kind: 'local', path: src.path, error: src.error || null } : { kind: 'npm' } };
86
117
  }
87
118
 
88
119
  function compare(st, latestVersion) {
@@ -115,7 +146,12 @@ function why(r, what) {
115
146
  return `${what} did not finish${said ? ': ' + said : ''}`;
116
147
  }
117
148
 
118
- async function latest({ exec = cp.execFile } = {}) {
149
+ async function latest({ exec = cp.execFile, source: src = source() } = {}) {
150
+ if (src.kind === 'local') {
151
+ if (src.error) return { ok: false, error: src.error };
152
+ if (!src.version) return { ok: false, error: `${src.path} has no version number` };
153
+ return { ok: true, version: src.version, source: 'local' };
154
+ }
119
155
  const r = await run(exec, NPM, ['view', PKG_NAME, 'version', '--json'], { timeout: CHECK_TIMEOUT_MS });
120
156
  const bad = why(r, 'The check');
121
157
  if (bad) return { ok: false, error: bad };
@@ -135,10 +171,11 @@ function notServedYet(text, version) {
135
171
  }
136
172
  const notServedYetError = (version) => `npm lists ${PKG_NAME} ${version}, but its download is not available yet. A version this new can take up to five minutes to reach every npm server. Press Update again in five minutes`;
137
173
 
138
- async function updateKit(root, { version, exec = cp.execFile } = {}) {
174
+ async function updateKit(root, { version, exec = cp.execFile, source: src = source() } = {}) {
139
175
  if (!validVersion(version)) return { ok: false, error: 'Check for updates first' };
176
+ if (src.kind === 'local' && src.error) return { ok: false, error: src.error };
140
177
  if (!readKit(root).present) return { ok: false, error: 'This repository has no PMOS kit folder (pmos/KIT-VERSION)' };
141
- const r = await run(exec, NPX, ['-y', `${PKG_NAME}@${version}`, '--update', String(root)], { timeout: UPDATE_TIMEOUT_MS, cwd: String(root) });
178
+ const r = await run(exec, NPX, ['-y', specOf(src, version), '--update', String(root)], { timeout: UPDATE_TIMEOUT_MS, cwd: String(root) });
142
179
  const output = tail(r.stdout + (r.stderr ? '\n' + r.stderr : ''));
143
180
  if (r.err && notServedYet(r.stdout + r.stderr, version)) return { ok: false, error: notServedYetError(version), output };
144
181
  const bad = why(r, 'The kit update');
@@ -175,15 +212,16 @@ function updateEnv(env = process.env, nodePath = process.execPath) {
175
212
  return parts.includes(dir) ? { ...env } : { ...env, [key]: [...parts, dir].join(path.delimiter) };
176
213
  }
177
214
 
178
- function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv() } = {}) {
215
+ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(), source: src = source() } = {}) {
179
216
  if (!validVersion(version)) return { ok: false, error: 'Check for updates first' };
217
+ if (src.kind === 'local' && src.error) return { ok: false, error: src.error };
180
218
  let fd;
181
219
  try {
182
220
  fs.mkdirSync(path.dirname(logFile), { recursive: true });
183
221
  fd = fs.openSync(logFile, 'w');
184
222
  fs.writeSync(fd, `${new Date().toISOString()} clearing npm cache…\n`);
185
223
  fs.writeSync(fd, `${new Date().toISOString()} ${NPM} cache clean --force\n`);
186
- fs.writeSync(fd, `${new Date().toISOString()} ${NPX} -y ${PKG_NAME}@${version} workspace --reinstall ${root}\n`);
224
+ fs.writeSync(fd, `${new Date().toISOString()} ${NPX} -y ${specOf(src, version)} workspace --reinstall ${root}\n`);
187
225
  } catch (e) { return { ok: false, error: `Could not write the log file: ${e.message}` }; }
188
226
  try {
189
227
  // Clear npm cache synchronously before spawning the update, so npx downloads the fresh version
@@ -192,7 +230,7 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(
192
230
  try { fs.appendFileSync(logFile, `Note: cache clear had an issue, but continuing: ${e.message}\n`); } catch { /* silent */ }
193
231
  }
194
232
  try {
195
- const child = spawn(NPX, ['-y', `${PKG_NAME}@${version}`, 'workspace', '--reinstall', String(root)],
233
+ const child = spawn(NPX, ['-y', specOf(src, version), 'workspace', '--reinstall', String(root)],
196
234
  { cwd: String(root), env, detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
197
235
  // A child that could not start writes nothing itself: say why in the log, which the page names when it gives up.
198
236
  if (child && typeof child.on === 'function') {
@@ -207,4 +245,4 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(
207
245
  }
208
246
  }
209
247
 
210
- module.exports = { PKG_NAME, cmpVer, notServedYet, validVersion, readKit, fromCheckout, status, compare, latest, updateKit, updateEnv, upgradeApp, whereFrom };
248
+ module.exports = { PKG_NAME, cmpVer, notServedYet, validVersion, source, specOf, readKit, fromCheckout, status, compare, latest, updateKit, updateEnv, upgradeApp, whereFrom };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.95.0",
9
+ "version": "0.96.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.95.0",
3
+ "version": "0.96.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",
@@ -341,7 +341,9 @@ async function waitForRestart(from, log) {
341
341
  * server, installs the new version, starts it again, and this page reloads on it. A kit update alone changes no page.
342
342
  */
343
343
  async function updateAll(ws, app, cmp) {
344
- const appToo = !!cmp.appBehind && !app.fromCheckout;
344
+ const cur = kitByWs.get(ws);
345
+ const local = !!(cur && cur.st && cur.st.source && cur.st.source.kind === 'local');
346
+ const appToo = !!cmp.appBehind && (!app.fromCheckout || local);
345
347
  if (appToo) {
346
348
  const yes = await askConfirm({ title: P.updateAppTitle, body: P.updateAppBody(app.version || '?', upd.latest), confirmLabel: P.updateAll });
347
349
  if (!yes) return;
@@ -368,9 +370,11 @@ function updatesSection(scope) {
368
370
  const cmp = upd.compare || {};
369
371
  const kids = [];
370
372
  const kitPresent = !!(kit && kit.present);
371
- const canUpdate = (cmp.appBehind && !app.fromCheckout) || (cmp.kitBehind && kitPresent);
373
+ const canUpdate = (cmp.appBehind && (!app.fromCheckout || (st && st.source && st.source.kind === 'local'))) || (cmp.kitBehind && kitPresent);
372
374
  kids.push(el('div', { class: 'fr-set__line fr-set__line--box' }, icon('panel-top'), el('span', { 'data-fr': 'app-version' }, P.updatesApp(app.version))));
373
- if (cmp.appBehind && app.fromCheckout) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'app-checkout' }, P.updatesCheckout));
375
+ const src = st && st.source;
376
+ if (src && src.kind === 'local') kids.push(el('p', { class: 'fr-set__note' + (src.error ? ' fr-set__error' : ''), 'data-fr': 'update-source' }, src.error ? P.updatesLocalBad(src.error) : P.updatesLocal(src.path)));
377
+ if (cmp.appBehind && app.fromCheckout && !(src && src.kind === 'local')) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'app-checkout' }, P.updatesCheckout));
374
378
  const kitText = scope === 'all' || !ws ? P.updatesNoRepo : !kit ? '' : kit.present ? P.updatesKit(kit.version) : P.updatesNoKit;
375
379
  kids.push(el('div', { class: 'fr-set__line fr-set__line--box' }, icon('cloud'), el('span', { 'data-fr': 'kit-version' }, kitText)));
376
380
  if (canUpdate) {
@@ -223,6 +223,8 @@ export const COPY = {
223
223
  updatesNever: 'Press Check for updates to compare with the latest published version.',
224
224
  updatesLatest: (v) => 'Latest published version: ' + v + '.',
225
225
  updatesCurrent: (v) => 'Up to date. The latest published version is ' + v + '.',
226
+ updatesLocal: (p) => 'Checking against the local package ' + p + ', not npm. Set PMOS_UPDATE_SOURCE to change this.',
227
+ updatesLocalBad: (e) => e,
226
228
  updatesCheckout: 'This workspace runs from a PMOS checkout. Update it with git pull.',
227
229
  updatesNote: 'Updating the kit changes only its own files (skills, scripts, templates and guides). Your records in state/, planning/ and okf/product/ do not change.',
228
230
  updateKitBusy: 'Updating the kit…',
@@ -7261,7 +7261,7 @@ async function handleRequest(req, res) {
7261
7261
  if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
7262
7262
  const st = () => kitUpdate.status({ root: w.root, appVersion: APP_VERSION, appDir: __dirname });
7263
7263
  if (p === '/api/kit/check') {
7264
- const r = await kitUpdate.latest();
7264
+ const r = await kitUpdate.latest({ source: kitUpdate.source() });
7265
7265
  const now = st();
7266
7266
  if (!r.ok) return json(res, { ok: false, error: r.error, ws: w.id, ...now }, 502);
7267
7267
  return json(res, { ok: true, ws: w.id, ...now, latest: r.version, ...kitUpdate.compare(now, r.version) });
@@ -7283,7 +7283,8 @@ async function handleRequest(req, res) {
7283
7283
  tokenKept: !!TOKEN && !BIND.generated,
7284
7284
  });
7285
7285
  if (!where.ok) return json(res, { ok: false, error: where.error }, 403);
7286
- if (st().app.fromCheckout) return json(res, { ok: false, error: 'This workspace runs from a PMOS checkout. Update it with git pull' }, 409);
7286
+ // A local source (PMOS_UPDATE_SOURCE) is how a checkout tries an update, so only the npm source is refused there.
7287
+ if (st().app.fromCheckout && st().source.kind !== 'local') return json(res, { ok: false, error: 'This workspace runs from a PMOS checkout. Update it with git pull' }, 409);
7287
7288
  const r = kitUpdate.upgradeApp(w.root, { version: body.version, logFile: path.join(STATE, 'app-update.log') });
7288
7289
  if (!r.ok) return json(res, { ok: false, error: r.error }, 400);
7289
7290
  kitBusy = 'app';
@@ -8,6 +8,7 @@
8
8
  "test.js"
9
9
  ],
10
10
  "modules": [
11
+ "report-template-cadence.test.js",
11
12
  "proactive.test.js",
12
13
  "goals.test.js",
13
14
  "agenda.test.js",
@@ -100,6 +101,7 @@
100
101
  "pm-r29-skills-scheduled-test.js",
101
102
  "pm-r29-validate-guide-test.js",
102
103
  "pm-r29-reports-addrepo-test.js",
104
+ "pm-report-tab-test.js",
103
105
  "pm-r29-filespad-test.js",
104
106
  "pm-r29-newsession-alignment-test.js",
105
107
  "pm-live-notes-test.js",
@@ -199,6 +201,8 @@
199
201
  "pm-r29-skills-scheduled-test.js": 6,
200
202
  "pm-r29-validate-guide-test.js": 20,
201
203
  "pm-r29-reports-addrepo-test.js": 4,
204
+ "pm-report-tab-test.js": 10,
205
+ "report-template-cadence.test.js": 1,
202
206
  "pm-r29-filespad-test.js": 5,
203
207
  "pm-r29-newsession-alignment-test.js": 8,
204
208
  "pm-live-notes-test.js": 24,