create-pmos 0.212.0 → 0.213.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.212.0",
3
+ "version": "0.213.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@316d11d
1
+ kernel-source: pmos@4425c43
2
2
  assembled: 2026-10-14
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-14T02:50:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
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-14T02:50:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
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-14T02:50:31Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T03:05:22Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -155,7 +155,7 @@ const { harness } = require('./c3-ini-harness');
155
155
  ok('P1 an on-hold initiative: the banner with the reason and the check-back day, Resume and Cancel', /On hold since/.test(ban) && /Q1 budget/.test(ban) && /Check back/.test(ban) && /Resume/.test(ban), ban);
156
156
  await page.click('[data-il-menu="IDPF-152"]');
157
157
  const pm = await st(() => [...document.querySelectorAll('[data-ini="menu"] .ini-mlbl')].map((n) => n.textContent).join(','));
158
- ok('P2 the page "…" menu holds Status and Tidy up', pm === 'Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-resume]')), pm);
158
+ ok('P2 the page "…" menu holds This initiative, Status and Tidy up', pm === 'This initiative,Status,Tidy up' && await st(() => !!document.querySelector('[data-ini="menu"] [data-il-resume]')), pm);
159
159
  await page.keyboard.press('Escape');
160
160
  await page.click('[data-il="banner"] [data-il-resume]');
161
161
  await until(async () => ((await row('IDPF-152')) || {}).status === 'on track');
@@ -121,11 +121,15 @@ const { harness } = require('./c3-ini-harness');
121
121
 
122
122
  /* ── an initiative: tools, stepper, workflow editor ── */
123
123
  await open('ini:idpf:IDPF-07:overview');
124
- const strip = await st(() => [...document.querySelectorAll('[data-ini="tools"] [data-ini-tool]')].map((b) => b.dataset.iniTool).join(','));
125
- ok('I1 the tools strip shows the tools of the current stage (Plan: RICE, PRD, Done-criteria, ...)', /^rice,prd,rubric/.test(strip), strip);
126
- await page.click('[data-ini="more-tools"]');
127
- const more = await until(() => st(() => document.querySelectorAll('[data-ini="menu"] [data-ini-moretool]').length));
128
- ok('I2 More tools lists the tools of the other stages, grouped by stage', more > 3 && /Discover/.test(await st(() => document.querySelector('[data-ini="menu"]').textContent)), String(more));
124
+ const head = await st(() => { const p = document.querySelector('[data-ini-page]'); const c = document.querySelector('[data-ini="crumbs"]').parentElement;
125
+ return { title: !!p.querySelector(':scope > .lh'), dash: [...c.querySelectorAll('button')].some((b) => b.textContent.trim() === 'Dashboard'), tag: /Jira not set|Jira · |PMOS tasks only/.test(p.textContent.split('Overview')[0]),
126
+ status: !!c.querySelector('[data-ini="ini-chips"]'), gear: !!document.querySelector('[data-ini="stepper"]').parentElement.querySelector('[data-ini="edit-workflow"]') }; });
127
+ ok('H1 the header is the breadcrumb row with the status, and the stepper row with the gear; no title row, Dashboard button or Jira tag', !head.title && !head.dash && !head.tag && head.status && head.gear, JSON.stringify(head));
128
+ await page.click('[data-ini="tools"]');
129
+ const strip = await until(() => st(() => { const m = document.querySelector('[data-ini="menu"]'); return m && [...m.querySelectorAll('[data-ini-tool]')].map((b) => b.dataset.iniTool).join(','); }));
130
+ ok('I1 Tools lists the tools of the current stage first (Plan: RICE, PRD, Done-criteria, ...)', /^rice,prd,rubric/.test(strip) && (await st(() => document.querySelector('[data-ini="menu"] .ini-mlbl').textContent)) === 'For Plan', strip);
131
+ const more = await st(() => document.querySelectorAll('[data-ini="menu"] [data-ini-moretool]').length);
132
+ ok('I2 then the tools of the other stages, grouped by stage', more > 3 && /Discover/.test(await st(() => document.querySelector('[data-ini="menu"]').textContent)), String(more));
129
133
  await page.keyboard.press('Escape');
130
134
  ok('I3 Escape closes the menu', await until(() => st(() => !document.querySelector('[data-ini="menu"]'))));
131
135
  await page.click('[data-ini-step="Build & Deliver"]');
@@ -150,8 +154,9 @@ const { harness } = require('./c3-ini-harness');
150
154
  ok('W4 the initiative now runs Research only and moved to Plan; other initiatives keep theirs', r07.stage === 'Plan' && r07.wf === 'Discover,Plan,Evaluate'
151
155
  && await store(() => !window.__c3boards.rows.find((r) => r.id === 'IDPF-12').template), JSON.stringify(r07));
152
156
 
153
- /* ── Jira epics of an initiative (jira-epic-items) ── */
154
- await page.click('[data-ini="jira-epics"]');
157
+ /* ── Jira epics of an initiative (jira-epic-items): in the page "…" menu ── */
158
+ await page.click('[data-il-menu="IDPF-07"]');
159
+ await page.click('[data-ini="menu"] [data-ini="jira-epics"]');
155
160
  await until(() => st(() => !!document.querySelector('[data-ini="epics-input"]')));
156
161
  await page.fill('[data-ini="epics-input"]', 'IDPF-900, XX-1');
157
162
  await page.click('[data-ini="epics-save"]');
@@ -160,8 +165,11 @@ const { harness } = require('./c3-ini-harness');
160
165
  await page.fill('[data-ini="epics-input"]', 'idpf-900, idpf-901');
161
166
  await page.click('[data-ini="epics-save"]');
162
167
  await until(() => store(() => (window.__c3boards.rows.find((r) => r.id === 'IDPF-07').epics || []).length === 2));
163
- const epChip = await until(() => st(() => { const c = document.querySelector('[data-ini="jira-epics"]'); return c && /Jira epics IDPF-900, IDPF-901/.test(c.textContent) && c.textContent; }));
164
- ok('E2 the epics are saved upper-cased, and the initiative header names them', !!epChip, epChip);
168
+ await until(() => st(() => !document.querySelector('[data-ini="epics-input"]')));
169
+ await page.click('[data-il-menu="IDPF-07"]');
170
+ const epChip = await until(() => st(() => { const c = document.querySelector('[data-ini="menu"] [data-ini="jira-epics"]'); return c && /Jira epics IDPF-900, IDPF-901/.test(c.textContent) && c.textContent; }));
171
+ ok('E2 the epics are saved upper-cased, and the page menu names them', !!epChip, epChip);
172
+ await page.keyboard.press('Escape');
165
173
 
166
174
  /* ── phone width ── */
167
175
  await page.setViewportSize({ width: 400, height: 860 });
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Initiatives on the console: Portfolio, boards and workflows"
4
4
  description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
5
- timestamp: 2026-10-13
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # Initiatives on the console: Portfolio, boards and workflows
@@ -86,9 +86,9 @@ Press the gear in the board header to open the board settings.
86
86
 
87
87
  ## An initiative
88
88
 
89
- Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip, for example "Skipped Plan" or "No spec yet". Press **Workflow** to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes.
89
+ Each initiative has its own workflow, picked from a template: for example Product, Quick fix, Research only, or Content and launch. The stepper shows the stages of that workflow. Press a stage to move the initiative there. The move is your decision, so nothing blocks it. An order rule shows as a warning chip next to the status, at the right of the breadcrumb, for example "Skipped Plan" or "No spec yet". Press the gear next to the stepper to change the template or edit the stages. When you remove a stage that holds the initiative, the editor asks where it goes.
90
90
 
91
- Under the stepper are the tools of the current stage. **More tools** lists the tools of the other stages. The tabs are **Overview**, **Breakdown**, **Docs**, **Tickets**, **Timeline** and **History**.
91
+ Press **Tools**, at the right of the stepper, to open a tool. The menu lists the tools of the current stage first, then the tools of the other stages. The **…** menu at the right of the breadcrumb opens the Dashboard and the Jira epics of the initiative, and holds Status and Tidy up. The tabs are **Overview**, **Breakdown**, **Docs**, **Tickets**, **Timeline** and **History**.
92
92
 
93
93
  The **Overview** shows the stage records of the current stage:
94
94
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.212.0",
3
+ "version": "0.213.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.212.0",
9
+ "version": "0.213.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.212.0",
3
+ "version": "0.213.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",
@@ -7,7 +7,7 @@
7
7
  * after moving its Jira epic links and PMOS tasks.
8
8
  *
9
9
  * rowMenu(anchor, L, r) the row's "…" menu: Move to a stage, Status, Tidy up
10
- * pageMenu(anchor, L, r) the initiative page's "…" menu: Status and Tidy up
10
+ * pageMenu(anchor, L, r, own) the initiative page's "…" menu: the page's own items, Status and Tidy up
11
11
  * lifeBanner(L, r) the page banner of an on-hold, cancelled or archived initiative, or null
12
12
  * lifeChips(r) · archivedChips(r) · dupChip(L, r) the chips a row shows
13
13
  * restoreBtn(L, r) Restore, or Undo merge
@@ -329,9 +329,9 @@ export function rowMenu(anchor, L, r, onMove) {
329
329
  ...(status.length ? [{ group: 'Status' }, ...status] : []),
330
330
  { group: 'Tidy up' }, ...tidyItems(L, r)] });
331
331
  }
332
- export function pageMenu(anchor, L, r) {
332
+ export function pageMenu(anchor, L, r, own) {
333
333
  const status = statusItems(L, r);
334
- openMenu(anchor, { label: 'Actions for ' + r.id, items: [...(status.length ? [{ group: 'Status' }, ...status] : []), { group: 'Tidy up' }, ...tidyItems(L, r)] });
334
+ openMenu(anchor, { label: 'Actions for ' + r.id, items: [...(own && own.length ? [{ group: 'This initiative' }, ...own] : []), ...(status.length ? [{ group: 'Status' }, ...status] : []), { group: 'Tidy up' }, ...tidyItems(L, r)] });
335
335
  }
336
336
 
337
337
  /* ── chips and the banner ── */
@@ -738,20 +738,21 @@ export function mountInitiatives(pane, opts) {
738
738
  const ctxI = makeCtx(bb, { row, history, tools, toolsByCat, tab, tool: P.loc.tool || '' });
739
739
  const items = await loadItems(ctxI);
740
740
  if (token !== P.seq) return;
741
- const work = items.items.filter((w) => w.ini === row.id); const ep = work.filter((w) => w.kind === 'Epic').length;
741
+ const work = items.items.filter((w) => w.ini === row.id);
742
742
  const wf = wfOf(row, bb); const idx = wf.findIndex((s) => s.name === row.stage); const cat = row.cat || catOf(wf, row.stage);
743
743
  const warnings = Array.isArray(row.warnings) ? row.warnings : [];
744
- const stepper = el('div', { class: 'calbar' },
745
- el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Workflow position', 'data-ini': 'stepper' },
746
- ...wf.map((s, k) => btn({ 'aria-pressed': String(k === idx), 'aria-current': k === idx ? 'step' : 'false', 'data-ini-step': s.name, title: bb.self ? SELF_WHY : k === idx ? 'Current stage' : 'Move to ' + s.name, onclick: () => stepTo(s.name) },
747
- k < idx ? icon('check', 'sm') : sw(s.cat), s.name))),
748
- el('span', { class: 'chips2' }, mono(tplName(row, bb) + ' workflow · ' + cat + ' · you decide when it moves'),
749
- pill({ 'data-ini': 'edit-workflow', 'aria-haspopup': 'dialog', onclick: () => openWorkflow(bb, row) }, icon('settings', 'sm'), 'Edit workflow')));
744
+ // c3-initiative-declutter: three rows above the content. The breadcrumb carries the title, status and warnings;
745
+ // the stepper carries a gear for the workflow and one Tools menu; the tabs carry the counts.
750
746
  const kind = kindOf(row, bb);
751
747
  const toolList = [...recordToolsFor(kind, cat, bb.build).map((id) => ({ id, label: RECORD_META[id][0], rec: true })), ...(Array.isArray(tools) ? tools : []).map((t) => (typeof t === 'string' ? { id: t } : t)).filter((t) => t && t.id)];
752
- const moreBtn = pill({ 'data-ini': 'more-tools', 'aria-haspopup': 'menu', 'aria-expanded': 'false', onclick: (e) => moreTools(e.currentTarget, row, bb, cat, kind) }, icon('more', 'sm'), 'More tools');
753
- const strip = el('div', { class: 'chips2', 'data-ini': 'tools' }, mono('Tools for ' + row.stage),
754
- ...toolList.map((t) => pill({ 'data-ini-tool': t.id, 'aria-pressed': String(P.loc.tool === t.id), onclick: () => pickTool(t.id) }, icon(t.rec ? RECORD_META[t.id][1] : toolIcon(t.id), 'sm'), toolLabel(t))), moreBtn);
748
+ const toolsBtn = pill({ 'data-ini': 'tools', 'aria-haspopup': 'menu', 'aria-expanded': 'false', onclick: (e) => openTools(e.currentTarget, row, bb, cat, kind, toolList) }, icon('bolt', 'sm'), 'Tools');
749
+ const stepper = el('div', { class: 'calbar' },
750
+ el('span', { class: 'chips2' },
751
+ el('div', { class: 'seg ini-seg', role: 'group', 'aria-label': 'Workflow position', 'data-ini': 'stepper' },
752
+ ...wf.map((s, k) => btn({ 'aria-pressed': String(k === idx), 'aria-current': k === idx ? 'step' : 'false', 'data-ini-step': s.name, title: bb.self ? SELF_WHY : k === idx ? 'Current stage' : 'Move to ' + s.name, onclick: () => stepTo(s.name) },
753
+ k < idx ? icon('check', 'sm') : sw(s.cat), s.name))),
754
+ ib('Edit workflow', 'settings', { 'data-ini': 'edit-workflow', 'aria-haspopup': 'dialog', title: tplName(row, bb) + ' workflow, now in ' + cat + '. You decide when it moves. Edit workflow', onclick: () => openWorkflow(bb, row) })),
755
+ toolsBtn);
755
756
  const tabs = INI_TAB_LIST.filter(([k]) => k !== 'tickets' || bb.build).map(([k, l, ic]) => [k, l, ic,
756
757
  k === 'breakdown' && work.length ? el('span', { class: 'bdg g' }, String(work.length)) : k === 'tickets' ? el('span', { class: 'bdg g' }, String(work.filter((w) => w.src === 'jira').length)) : null]);
757
758
  const host = el('div', { class: 'calbody', 'data-ini-panel': tab });
@@ -761,16 +762,15 @@ export function mountInitiatives(pane, opts) {
761
762
  setTimeout(() => { if (token === P.seq && P.expect === ex) { P.ini = null; draw(); } }, 800);
762
763
  } else if (ex && ex.id === row.id) P.expect = null;
763
764
  const L = { ctx: ctxI, done: (change) => { P.expect = change || null; P.ini = null; P.boards = null; dropItems(); draw(); } };
764
- put(root,
765
- pathCrumbs(bb, row, el('div', { class: 'chips2' }, pbtn('outline', 'Dashboard', 'report', { onclick: () => ctxI.showTab('rep') }),
766
- ib('Actions for ' + row.id, 'more', { 'data-il-menu': row.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Pause, cancel, merge or archive', onclick: (e) => pageMenu(e.currentTarget, L, row) }))),
765
+ const own = [{ kids: [icon('report', 'sm'), 'Dashboard'], attrs: { 'data-ini-act': 'dashboard' }, onPick: () => ctxI.showTab('rep') },
766
+ bb.build ? { kids: [icon('jira', 'sm'), (row.epics || []).length ? 'Jira epics ' + row.epics.join(', ') : 'Link Jira epics'], attrs: { 'data-ini': 'jira-epics' }, onPick: () => openEpics(bb, row) } : null].filter(Boolean);
767
+ put(root,
768
+ pathCrumbs(bb, row, el('div', { class: 'chips2' },
769
+ el('span', { class: 'chips2', 'data-ini': 'ini-chips' }, statusChip(row.status), el('span', { class: 'chips2', 'data-ini': 'warnings', 'aria-live': 'polite' }, ...warnings.map(warnChip))),
770
+ ib('Actions for ' + row.id, 'more', { 'data-il-menu': row.id, 'aria-haspopup': 'menu', 'aria-expanded': 'false', title: 'Dashboard, Jira epics, pause, cancel, merge or archive', onclick: (e) => pageMenu(e.currentTarget, L, row, own) }))),
767
771
  el('div', { class: 'calbody', 'data-ini-page': row.id },
768
772
  lifeBanner(L, row),
769
- lh('target', row.title || row.id, [row.id, bb.name, row.anchor || 'no anchor', row.owner || 'no owner', row.start || row.end ? day(row.start) + ' to ' + day(row.end) : 'no dates yet', row.horizon ? row.horizon[0].toUpperCase() + row.horizon.slice(1) : '', Math.round((Number(row.progress) || 0) * 100) + '% done'].filter(Boolean).join(' · ')),
770
- el('div', { class: 'chips2', 'data-ini': 'ini-chips' }, statusChip(row.status), chip(icon('bolt', 'sm'), plural(ep, 'epic') + ', ' + plural(work.length - ep, 'item')), buildTag(bb),
771
- bb.build ? pill({ 'data-ini': 'jira-epics', 'aria-haspopup': 'dialog', title: 'The Jira epics of this initiative', onclick: () => openEpics(bb, row) }, icon('jira', 'sm'), (row.epics || []).length ? 'Jira epics ' + row.epics.join(', ') : 'Link Jira epics') : null,
772
- row.lane ? tag(row.lane) : null, el('span', { class: 'chips2', 'data-ini': 'warnings', 'aria-live': 'polite' }, ...warnings.map(warnChip))),
773
- stepper, bb.self ? note(SELF_WHY) : null, strip,
773
+ stepper, bb.self ? note(SELF_WHY) : null,
774
774
  segTabs(tabs, tab, (k) => go({ view: 'ini', board: bb.id, id: row.id, tab: k }), 'Initiative views', { 'data-ini': 'ini-tabs' }),
775
775
  host));
776
776
  if (P.focusStep) { const f = stepper.querySelector('[data-ini-step="' + CSS.escape(P.focusStep) + '"]'); P.focusStep = null; if (f) f.focus(); }
@@ -809,16 +809,17 @@ export function mountInitiatives(pane, opts) {
809
809
  const n = root.querySelector('[data-ini-tool-card="' + CSS.escape(id) + '"]');
810
810
  if (n) { n.scrollIntoView({ behavior: 'smooth', block: 'start' }); n.classList.add('ini-flash'); setTimeout(() => n.classList.remove('ini-flash'), 1400); }
811
811
  }
812
- function moreTools(anchor, row, b, cat, kind) {
812
+ /* One Tools menu: this stage's tools first, then the other stages' tools under their stage names. */
813
+ function openTools(anchor, row, b, cat, kind, here) {
813
814
  const byCat = (P.ini && P.ini.toolsByCat) || {};
814
815
  const recOf = (c) => recordToolsFor(kind, c, b.build).map((id) => ({ id, label: RECORD_META[id][0], rec: true }));
815
- const seen = new Set([...(byCat[cat] || []), ...recOf(cat)].map((t) => t.id));
816
- const items = [];
816
+ const seen = new Set([...(byCat[cat] || []), ...recOf(cat), ...here].map((t) => t.id));
817
+ const items = here.length ? [{ group: 'For ' + row.stage }, ...here.map((t) => ({ kids: [icon(t.rec ? RECORD_META[t.id][1] : toolIcon(t.id), 'sm'), toolLabel(t)], current: P.loc.tool === t.id, attrs: { 'data-ini-tool': t.id }, onPick: () => pickTool(t.id) }))] : [];
817
818
  for (const s of wfOf(row, b)) {
818
819
  const ks = [...recOf(s.cat), ...(byCat[s.cat] || [])].filter((t) => !seen.has(t.id)); ks.forEach((t) => seen.add(t.id));
819
820
  if (ks.length) items.push({ group: s.name }, ...ks.map((t) => ({ kids: [icon(t.rec ? RECORD_META[t.id][1] : toolIcon(t.id), 'sm'), toolLabel(t)], attrs: { 'data-ini-moretool': t.id }, onPick: () => go({ ...P.loc, tab: 'overview', tool: t.id }) })));
820
821
  }
821
- openMenu(anchor, { label: 'More tools', items: items.length ? items : [{ label: 'No other tools for this workflow.', disabled: true, onPick: () => {} }] });
822
+ openMenu(anchor, { label: 'Tools', items: items.length ? items : [{ label: 'No tools for this workflow.', disabled: true, onPick: () => {} }] });
822
823
  }
823
824
  async function stepTo(stage) {
824
825
  const ini = P.ini; if (!ini) return;