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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/layout.md +1 -0
- package/workspace/lib/kit-update.js +48 -10
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +7 -3
- package/workspace/public/pm/frame-copy.js +2 -0
- package/workspace/server.js +3 -2
- package/workspace/test-files.json +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
85
|
-
|
|
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',
|
|
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 ${
|
|
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',
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.96.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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…',
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
|
|
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,
|