create-pmos 0.10.0 → 0.10.1

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/README.md CHANGED
@@ -333,6 +333,24 @@ Refreshes the skills, concepts, and scripts only. Your records — `state/`, `pl
333
333
  `okf/product/` — are yours, and an update never touches them. `pmos/KIT-VERSION` always says
334
334
  exactly what you're running.
335
335
 
336
+ ## What's new in 0.10.1
337
+
338
+ **Two fixes found by installing 0.10.0 and using it, rather than by reading the code.**
339
+
340
+ - **Upgrades will keep reaching you.** The launcher decides whether to reinstall the workspace by
341
+ comparing the app version it bundles against the one you have. 0.10.0 shipped the *kit* at 0.10.0
342
+ but the bundled app still labelled **0.9.1** — so the next release would have compared equal,
343
+ decided you were current, and **silently skipped the upgrade**. Which is precisely the failure
344
+ 0.9.1 existed to fix. The two versions are now checked against each other by the test suite, so it
345
+ cannot drift again unnoticed.
346
+ - **Alignment no longer offers to edit its own notes.** The change-context digest quotes every change
347
+ you make, so it matched the "quotes text this change altered" rule on the very first save in any
348
+ repository — and topped the list of documents to realign. A generated file proposing to realign
349
+ itself. Machine-written state is now excluded from alignment entirely.
350
+
351
+ If you installed 0.10.0, run `npx create-pmos@latest workspace` once. Nothing was damaged by either
352
+ bug; the first would have cost you a future upgrade, and the second a confusing first suggestion.
353
+
336
354
  ## What's new in 0.10.0
337
355
 
338
356
  **Editing a document now does something for the rest of them.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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. `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@1771814
1
+ kernel-source: pmos@84f5b38
2
2
  assembled: 2026-07-28
3
3
  mode: vendor
@@ -163,9 +163,25 @@
163
163
  return { layouts, currentId: DEFAULT_ID };
164
164
  }
165
165
 
166
+ /* What a GET /api/layouts response entitles the client to DO. Split out as a pure function
167
+ because the original inline condition conflated two very different states — "this workspace
168
+ has never stored a layout" and "I could not read what it stored" — and answered both by
169
+ migrating and then WRITING, silently replacing a real stored document whenever a read failed.
170
+ A read failure must never author a write.
171
+
172
+ 'migrate' — the server affirmatively says nothing is stored (first run here): R8 applies.
173
+ 'use' — a stored document came back: adopt it.
174
+ 'keep' — the answer is unusable (transport error, non-JSON, !ok, or no `stored` flag).
175
+ Hold the boot defaults in memory and WRITE NOTHING. */
176
+ function loadAction(doc) {
177
+ if (!doc || doc.ok !== true || typeof doc.stored !== 'boolean') return 'keep';
178
+ return doc.stored ? 'use' : 'migrate';
179
+ }
180
+
166
181
  return {
167
182
  SIDE, READER, SPLIT, FOLD_KEYS, STOCK_IDS, DEFAULT_ID,
168
183
  SIDE_MIN, SIDE_MAX, READ_MIN, READ_MAX, BOTTOM_H,
169
184
  isStock, stockLayout, stockLayouts, layout, sanitize, gridFor, hasEscape, migrateLegacy,
185
+ loadAction,
170
186
  };
171
187
  });
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
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",
7
- "bin": { "pmos": "bin/pmos.js" },
7
+ "bin": {
8
+ "pmos": "bin/pmos.js"
9
+ },
8
10
  "scripts": {
9
11
  "start": "node server.js",
10
12
  "test": "node test.js",
11
13
  "test:ui": "node ui-test.js",
12
14
  "doctor": "node doctor.js"
13
15
  },
14
- "engines": { "node": ">=18" },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
15
19
  "dependencies": {
16
20
  "@xterm/addon-fit": "^0.11.0",
17
21
  "@xterm/addon-web-links": "^0.12.0",
@@ -1059,7 +1059,13 @@ async function loadLayouts() {
1059
1059
  const r = await fetch('/api/layouts?ws=' + encodeURIComponent(state.wsId || ''));
1060
1060
  doc = await r.json();
1061
1061
  } catch { doc = null; }
1062
- if (!doc || !doc.ok || !Array.isArray(doc.layouts) || !doc.layouts.length) {
1062
+ // A read failure must never author a write. loadAction() is the one place that decides, and it
1063
+ // only says 'migrate' when the SERVER affirmatively reports nothing stored — never because the
1064
+ // answer was unreadable. Anything unusable is 'keep': show the boot defaults, persist nothing,
1065
+ // and leave whatever is on disk exactly as it is until a load succeeds.
1066
+ const act = LAY.loadAction(doc);
1067
+ if (act === 'keep') return;
1068
+ if (act === 'migrate') {
1063
1069
  // R8 — a workspace with nothing stored inherits whatever the PM had already tuned by hand
1064
1070
  // in the pre-feature client, so the upgrade never costs them their rail width.
1065
1071
  const legacy = {};
@@ -225,6 +225,9 @@ function wsList() {
225
225
  }));
226
226
  }
227
227
  const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'vendor', 'target', 'coverage', '.git', 'tmp', 'out']);
228
+ // The directories stateDirFor() resolves to. Their contents are machine-written runtime and curated
229
+ // records — never documents alignment should offer to edit.
230
+ const STATE_DIR_NAMES = new Set(['state', '.pmos']);
228
231
  const PLAIN_MAX_DEPTH = 3; // the curated (knowledge) view stays shallow
229
232
  const TREE_MAX_DEPTH = 6; // the folder lens is a real explorer, but bounded on huge/slow repos
230
233
  const TREE_MAX_NODES = 4000; // …and node-budgeted so a giant repo (or Google Drive) never hangs
@@ -317,6 +320,11 @@ function markdownDocs(root, limit = 800) {
317
320
  if (e.name.startsWith('.') || SKIP_DIRS.has(e.name)) continue;
318
321
  const full = path.join(dir, e.name);
319
322
  const r = rel ? rel + '/' + e.name : e.name;
323
+ // Machine-written state is not a document the PM maintains, and alignment must never propose
324
+ // realigning it. change-context.md in particular QUOTES every change, so it matches the
325
+ // quoted-text rule on the first save in any repository and would top the candidate list —
326
+ // a generated file suggesting itself, found by installing the published artifact and using it.
327
+ if (STATE_DIR_NAMES.has(e.name)) continue;
320
328
  if (e.isDirectory()) walk(full, r, depth + 1);
321
329
  else if (preview.previewKind(e.name) === 'markdown') out.push(r);
322
330
  }
@@ -1510,11 +1518,17 @@ const LAYOUT_MAX = 40_000;
1510
1518
  function layoutPath(w) { return w ? path.join(LAYOUT_DIR, w.id + '.json') : null; }
1511
1519
  // Always returns a usable document: an absent, empty, truncated or hand-mangled file yields the
1512
1520
  // four stock layouts rather than an error, because the alternative is a window the PM cannot use.
1521
+ // `stored` is the client's ONLY safe signal that R8's one-time migration should run. sanitize()
1522
+ // always yields the four stock layouts, so the response body alone cannot distinguish "this
1523
+ // workspace has never stored anything" from "I could not read what it stored" — and the client
1524
+ // answers those two cases very differently (one writes, one must not). A file that exists but is
1525
+ // corrupt still counts as stored: this workspace has been through the layout system, so replaying
1526
+ // the pre-feature localStorage over it would be a second loss, not a recovery.
1513
1527
  function readLayouts(w) {
1514
1528
  const f = layoutPath(w);
1515
1529
  let raw = null;
1516
1530
  if (f) { try { raw = fs.readFileSync(f, 'utf8'); } catch { raw = null; } }
1517
- return layoutModel.sanitize(raw);
1531
+ return Object.assign(layoutModel.sanitize(raw), { stored: raw !== null });
1518
1532
  }
1519
1533
  function writeLayouts(w, doc) {
1520
1534
  const f = layoutPath(w);
@@ -2122,7 +2136,7 @@ async function handleRequest(req, res) {
2122
2136
  const w = getWs(url.searchParams.get('ws'));
2123
2137
  if (!w) return json(res, { ok: false, error: 'no workspace' }, 404);
2124
2138
  const doc = readLayouts(w);
2125
- return json(res, { ok: true, layouts: doc.layouts, currentId: doc.currentId });
2139
+ return json(res, { ok: true, layouts: doc.layouts, currentId: doc.currentId, stored: doc.stored });
2126
2140
  }
2127
2141
  if (p === '/api/doc') {
2128
2142
  const w = getWs(url.searchParams.get('ws'));