create-pmos 0.109.0 → 0.110.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.109.0",
3
+ "version": "0.110.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@3e0aefb
1
+ kernel-source: pmos@83b40d1
2
2
  assembled: 2026-10-08
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-08T03:57:21Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
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-08T03:57:21Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
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-08T03:57:21Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-08T04:20:34Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -163,7 +163,7 @@ The task appears in your My Work view and on the initiative's board when the not
163
163
  Under the title, a note shows two lines: **Meeting** and **Initiative**.
164
164
 
165
165
  - **Meeting** is the calendar event the note was taken in, or **Not on the calendar** for a hallway chat or a call. Press **Change** to select an event of the note's day or the day before, or **Not on the calendar**.
166
- - **Initiative** is the initiative the meeting was about. It says how it was set: **from the calendar event**, **suggested from what was said**, or **chosen by you**. Press **Change** to select a suggestion, any open initiative of the selected repository, or **None**. Press the initiative's name to open it.
166
+ - **Initiative** is the initiative the meeting was about. It says how it was set: **from the calendar event**, **suggested from what was said**, or **chosen by you**. Press **Change** to open the list. The list shows the suggestion first, then the initiatives that are active now, then **None**. Initiatives planned for Next and Later are in the menu under them, each marked with its horizon. When more than one repository is open, a **Repository** menu at the top of the list changes which repository's initiatives you see. If the repository is still being read, the list says so and fills in by itself. Press the initiative's name to open it.
167
167
 
168
168
  When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
169
169
 
@@ -269,6 +269,32 @@ function glossaryOf(root) {
269
269
  glossaryCache.set(root, { at: Date.now(), terms: out });
270
270
  return out;
271
271
  }
272
+ /** The open initiatives of one repository, the active ones first. The Initiatives view reads the repository's documents
273
+ * (lib/initiative-rows.js) and places each one Now, Next or Later; a repository kept as documents has no planning/initiatives
274
+ * records, so those alone left the picker empty. Placed rows come first in horizon order; records the view does not place follow
275
+ * as Now. `pending` is true while the first read of the documents is still running. */
276
+ function openInitiativesOf(w) {
277
+ const legacy = notesLinks.initiativesOf(w.root);
278
+ let placed = []; let pending = false;
279
+ try {
280
+ const rowsLib = require('./initiative-rows.js');
281
+ const ri = require('./routes-initiatives.js');
282
+ const got = rowsLib.rowsFor(w.root);
283
+ if (got.pending) pending = true;
284
+ else if (got.ok) {
285
+ const rm = ri.roadmapOf(w, ri.frameOf(w, got.rows, { now: Date.now() }));
286
+ const byId = new Map(got.rows.map((r) => [r.id, r]));
287
+ for (const h of ['now', 'next', 'later']) {
288
+ for (const r of rm.horizons[h]) { const rec = byId.get(r.id); if (!(rec && rec.stopped)) placed.push({ id: r.id, title: r.title, anchor: r.anchor, intent: '', horizon: h }); }
289
+ }
290
+ }
291
+ } catch { placed = []; }
292
+ const seen = new Set(placed.map((i) => i.id));
293
+ const rest = legacy.filter((i) => !seen.has(i.id)).map((i) => ({ ...i, horizon: 'now' }));
294
+ const order = { now: 0, next: 1, later: 2 };
295
+ const open = [...placed, ...rest].sort((a, b) => order[a.horizon] - order[b.horizon]);
296
+ return { open, pending };
297
+ }
272
298
  /** The repository's terms for a note: its initiative's title, the glossary, and the product terms in the open initiatives' titles. */
273
299
  function repoTermsFor(ctx, note) {
274
300
  const w = note && note.ws && typeof ctx.getWs === 'function' ? ctx.getWs(note.ws) : null;
@@ -500,12 +526,12 @@ async function handle(req, res, url, ctx) {
500
526
  const wsId = url.searchParams.get('ws');
501
527
  const w = wsId ? ctx.getWs(wsId) : null;
502
528
  if (!w || w.id !== wsId) { ctx.json(res, { ok: false, error: 'This repository is not registered.' }, 404); return true; }
503
- const open = notesLinks.initiativesOf(w.root);
529
+ const { open, pending } = openInitiativesOf(w);
504
530
  const nid = url.searchParams.get('id');
505
531
  const r = nid ? notes.read(dir, nid) : null;
506
532
  if (r && !r.ok) { send(ctx, res, r); return true; }
507
533
  const s = r ? notesLinks.suggest(r.note, open) : { pick: null, ranked: [] };
508
- ctx.json(res, { ok: true, open: open.map((i) => ({ id: i.id, title: i.title })), pick: s.pick });
534
+ ctx.json(res, { ok: true, open: open.map((i) => ({ id: i.id, title: i.title, horizon: i.horizon })), pick: s.pick, pending });
509
535
  return true;
510
536
  }
511
537
  if (p === '/api/notes/engine') {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.109.0",
3
+ "version": "0.110.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.109.0",
9
+ "version": "0.110.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.109.0",
3
+ "version": "0.110.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",
@@ -1067,32 +1067,69 @@ async function meetingSheet(ctx, n) {
1067
1067
  }), evs.length ? null : el('p', { class: 'nt-hint' }, 'No calendar events on these days.'),
1068
1068
  opt('adhoc', 'Not on the calendar', 'a hallway chat, a call', pick === 'adhoc'));
1069
1069
  }
1070
+ const HORIZON_WORDS = { now: 'Now', next: 'Next', later: 'Later' };
1071
+ /** The repositories the notes can be about, one row per repository (its main tree), from the workspace server. */
1072
+ async function reposForNotes() {
1073
+ const r = await pmGet('/api/workspaces');
1074
+ const seen = new Map();
1075
+ for (const w of (r && r.workspaces) || []) {
1076
+ if (!w || !w.id) continue;
1077
+ const key = w.repo_key || w.root;
1078
+ if (!seen.has(key) || w.root === key) seen.set(key, { ws: w.id, name: w.label || w.repo_name || w.name || w.id });
1079
+ }
1080
+ return [...seen.values()];
1081
+ }
1070
1082
  async function initiativeSheet(ctx, n) {
1071
- const ws = n.ws || wsOf(ctx);
1083
+ let ws = n.ws || wsOf(ctx);
1084
+ let repo = (n.initiative && n.initiative.repo) || (ctx.state && ctx.state.product) || ctx.repo || null;
1072
1085
  const box = el('div', { class: 'nt-picks' }, el('p', { class: 'nt-hint' }, 'reading the initiatives …'));
1086
+ const where = el('div', { 'data-nt-ini-where': '' });
1073
1087
  let pick = n.initiative ? n.initiative.id : '';
1074
1088
  let open = [];
1075
- const h = sheet({ title: 'Which initiative is this note about?', sub: 'The note shows on the initiative\'s page.', body: [box], attrs: { 'data-nt-ini-sheet': '' },
1089
+ let radios = [];
1090
+ let sel = null;
1091
+ let loadId = 0;
1092
+ const h = sheet({ title: 'Which initiative is this note about?', sub: 'The note shows on the initiative\'s page.', body: [where, box], attrs: { 'data-nt-ini-sheet': '' },
1076
1093
  actions: [button({ label: 'Cancel', variant: 'ghost', onClick: () => h.close() }), button({ label: 'Save', variant: 'primary', attrs: { 'data-nt-ini-save': '' }, onClick: () => {
1077
1094
  h.close();
1078
1095
  if (pick === (n.initiative ? n.initiative.id : '') && (!n.initiative || n.initiative.by === 'pm')) return;
1079
1096
  if (!pick) return link(ctx, n, { initiative: null }, () => 'Not linked to an initiative.');
1080
1097
  const i = open.find((x) => x.id === pick) || { id: pick, title: pick };
1081
- return link(ctx, n, { initiative: { id: i.id, ws, repo: (ctx.state && ctx.state.product) || ctx.repo || null, title: i.title } }, () => 'Now on ' + i.title + '. The note shows on its page.');
1098
+ return link(ctx, n, { initiative: { id: i.id, ws, repo, title: i.title } }, () => 'Now on ' + i.title + '. The note shows on its page.');
1082
1099
  } })] });
1083
- if (!ws) { box.replaceChildren(el('p', { class: 'nt-hint' }, 'Open a repository first: its initiatives are the ones a note can belong to.')); return; }
1084
- const r = await pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id));
1085
- if (!h.open) return;
1086
- if (!r || !r.ok) { box.replaceChildren(el('p', { class: 'nt-hint', role: 'alert' }, (r && r.error) || 'The initiatives could not be read.')); return; }
1087
- open = r.open || [];
1088
- const radios = [];
1089
- const choose = (v) => { pick = v; for (const b of radios) b.setAttribute('aria-checked', b.dataset.ntIniPick === v ? 'true' : 'false'); if (sel.control.value !== v) sel.control.value = open.some((x) => x.id === v) ? v : ''; };
1100
+ const choose = (v) => { pick = v; for (const b of radios) b.setAttribute('aria-checked', b.dataset.ntIniPick === v ? 'true' : 'false'); if (sel && sel.control.value !== v) sel.control.value = open.some((x) => x.id === v && x.horizon !== 'now') ? v : ''; };
1090
1101
  const opt = (v, title, sub) => { const b = button({ label: el('span', { class: 'nt-pick__txt' }, el('b', null, title), el('span', { class: 'nt-hint' }, sub)), variant: 'ghost', attrs: { class: 'nt-pick', role: 'radio', 'aria-checked': pick === v ? 'true' : 'false', 'data-nt-ini-pick': v }, onClick: () => choose(v) }); radios.push(b); return b; };
1091
- const sel = field({ label: 'Or choose any open initiative', kind: 'select', value: open.some((x) => x.id === pick) ? pick : '', options: [{ value: '', label: 'Choose …' }, ...open.map((x) => ({ value: x.id, label: x.title }))], attrs: { 'data-nt-ini-all': '' } });
1092
- sel.control.addEventListener('change', () => choose(sel.control.value));
1093
- const sugg = r.pick ? [el('h3', { class: 'nt-sec__h' }, 'Suggested from what was said'), opt(r.pick.id, r.pick.title, r.pick.shared + ' words in common')] : [];
1094
- const current = n.initiative && (!r.pick || r.pick.id !== n.initiative.id) ? [opt(n.initiative.id, n.initiative.title || n.initiative.id, BY_WORDS[n.initiative.by] || '')] : [];
1095
- box.replaceChildren(el('div', { role: 'radiogroup', 'aria-label': 'Initiative' }, ...sugg, ...current, opt('', 'None', 'not about an initiative')), open.length ? sel : el('p', { class: 'nt-hint' }, 'This repository has no open initiatives.'));
1102
+ async function load(tries = 0) {
1103
+ const mine = ++loadId;
1104
+ open = []; radios = []; sel = null;
1105
+ if (!ws) { box.replaceChildren(el('p', { class: 'nt-hint' }, 'Choose a repository: its initiatives are the ones a note can belong to.')); return; }
1106
+ box.replaceChildren(el('p', { class: 'nt-hint' }, 'reading the initiatives …'));
1107
+ const r = await pmGet('/api/notes/initiatives?ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(n.id));
1108
+ if (!h.open || mine !== loadId) return;
1109
+ if (!r || !r.ok) { box.replaceChildren(el('p', { class: 'nt-hint', role: 'alert' }, (r && r.error) || 'The initiatives could not be read.')); return; }
1110
+ open = r.open || [];
1111
+ if (r.pending && !open.length && tries < 8) { setTimeout(() => load(tries + 1), 1500); return; }
1112
+ // the active initiatives (Now) are listed as choices; Next and Later are in the list below them
1113
+ const active = open.filter((x) => x.horizon === 'now' && !(r.pick && r.pick.id === x.id));
1114
+ const later = open.filter((x) => x.horizon !== 'now');
1115
+ sel = field({ label: later.length ? 'Or choose a later initiative' : 'Or choose any open initiative', kind: 'select', value: later.some((x) => x.id === pick) ? pick : '',
1116
+ options: [{ value: '', label: 'Choose …' }, ...later.map((x) => ({ value: x.id, label: (HORIZON_WORDS[x.horizon] || 'Now') + ' · ' + x.title }))], attrs: { 'data-nt-ini-all': '' } });
1117
+ sel.control.addEventListener('change', () => choose(sel.control.value));
1118
+ const sugg = r.pick ? [el('h3', { class: 'nt-sec__h' }, 'Suggested from what was said'), opt(r.pick.id, r.pick.title, r.pick.shared + ' words in common')] : [];
1119
+ const current = n.initiative && (!r.pick || r.pick.id !== n.initiative.id) && !active.some((x) => x.id === n.initiative.id) ? [opt(n.initiative.id, n.initiative.title || n.initiative.id, BY_WORDS[n.initiative.by] || '')] : [];
1120
+ const now = active.length ? [el('h3', { class: 'nt-sec__h' }, 'Active now'), ...active.map((x) => opt(x.id, x.title, 'active'))] : [];
1121
+ box.replaceChildren(el('div', { role: 'radiogroup', 'aria-label': 'Initiative' }, ...sugg, ...current, ...now, opt('', 'None', 'not about an initiative')),
1122
+ later.length ? sel : (open.length ? null : el('p', { class: 'nt-hint' }, 'This repository has no open initiatives. Choose another repository above, or open the Initiatives tab to add one.')));
1123
+ }
1124
+ const repos = await reposForNotes();
1125
+ if (!h.open) return;
1126
+ if (repos.length > 1) {
1127
+ const rs = field({ label: 'Repository', kind: 'select', value: ws || '', options: [...(ws && !repos.some((x) => x.ws === ws) ? [{ value: ws, label: repo || ws }] : []), ...repos.map((x) => ({ value: x.ws, label: x.name }))], attrs: { 'data-nt-ini-repo': '' } });
1128
+ rs.control.addEventListener('change', () => { ws = rs.control.value; const f = repos.find((x) => x.ws === ws); repo = f ? f.name : repo; pick = ''; load(); });
1129
+ where.replaceChildren(rs);
1130
+ }
1131
+ if (!ws && repos.length) { ws = repos[0].ws; repo = repos[0].name; if (where.firstChild) where.firstChild.control && (where.firstChild.control.value = ws); }
1132
+ await load();
1096
1133
  }
1097
1134
  async function openInitiativeOf(ctx, ini) {
1098
1135
  const reg = await import('./registry.js');