create-pmos 0.123.0 → 0.125.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.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/layout.md +2 -0
  15. package/workspace/guide/notes.md +1 -0
  16. package/workspace/lib/engine/nine-stages.js +81 -0
  17. package/workspace/lib/engine/workflow-state.js +2 -0
  18. package/workspace/lib/facts-async.js +51 -0
  19. package/workspace/lib/item-steps.js +52 -0
  20. package/workspace/lib/jira-status.js +8 -3
  21. package/workspace/lib/jobs-to-be-done.js +52 -0
  22. package/workspace/lib/outbox-store.js +87 -0
  23. package/workspace/lib/pm-items.js +115 -0
  24. package/workspace/lib/route-modules.js +21 -0
  25. package/workspace/lib/routes-briefing.js +100 -0
  26. package/workspace/lib/routes-c3-init.js +53 -0
  27. package/workspace/lib/routes-c3-life.js +119 -0
  28. package/workspace/lib/routes-c3-strategy.js +169 -0
  29. package/workspace/lib/routes-c3-timeline.js +191 -0
  30. package/workspace/lib/routes-calendar-import.js +60 -0
  31. package/workspace/lib/routes-calendar-views.js +61 -0
  32. package/workspace/lib/routes-doc-health.js +79 -0
  33. package/workspace/lib/routes-doc-sheet.js +169 -0
  34. package/workspace/lib/routes-forecast.js +85 -0
  35. package/workspace/lib/routes-hooks.js +121 -0
  36. package/workspace/lib/routes-initiatives.js +3 -0
  37. package/workspace/lib/routes-intake.js +24 -0
  38. package/workspace/lib/routes-outbox.js +97 -0
  39. package/workspace/lib/routes-pm-items.js +216 -0
  40. package/workspace/lib/routes-records.js +61 -0
  41. package/workspace/lib/routes-releases.js +129 -0
  42. package/workspace/lib/routes-report-facts.js +84 -0
  43. package/workspace/lib/routes-report-families.js +32 -0
  44. package/workspace/lib/routes-roadmap-ops.js +141 -0
  45. package/workspace/lib/routes-runs.js +119 -0
  46. package/workspace/lib/routes-signals.js +50 -0
  47. package/workspace/lib/run-answers.js +69 -0
  48. package/workspace/package-lock.json +2 -2
  49. package/workspace/package.json +1 -1
  50. package/workspace/public/c3.html +26 -0
  51. package/workspace/public/pm/briefing-deck.js +415 -0
  52. package/workspace/public/pm/briefing-model.js +176 -0
  53. package/workspace/public/pm/c3-breakdown.js +240 -0
  54. package/workspace/public/pm/c3-brief.js +278 -0
  55. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  56. package/workspace/public/pm/c3-case.js +173 -0
  57. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  58. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  59. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  60. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  61. package/workspace/public/pm/c3-demo-data.js +560 -0
  62. package/workspace/public/pm/c3-demo-init.js +373 -0
  63. package/workspace/public/pm/c3-demo-life.js +77 -0
  64. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  65. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  66. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  67. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  68. package/workspace/public/pm/c3-demo-term.js +153 -0
  69. package/workspace/public/pm/c3-demo-time.js +49 -0
  70. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  71. package/workspace/public/pm/c3-demo-work2.js +13 -0
  72. package/workspace/public/pm/c3-demo.js +113 -0
  73. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  74. package/workspace/public/pm/c3-dock.js +180 -0
  75. package/workspace/public/pm/c3-docs.js +290 -0
  76. package/workspace/public/pm/c3-expand.js +96 -0
  77. package/workspace/public/pm/c3-init-extras.js +170 -0
  78. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  79. package/workspace/public/pm/c3-misc-panels.js +89 -0
  80. package/workspace/public/pm/c3-plan-panels.js +163 -0
  81. package/workspace/public/pm/c3-plan.js +192 -0
  82. package/workspace/public/pm/c3-roadmap.js +249 -0
  83. package/workspace/public/pm/c3-sheet.js +107 -0
  84. package/workspace/public/pm/c3-shell.js +637 -0
  85. package/workspace/public/pm/c3-signals.js +196 -0
  86. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  87. package/workspace/public/pm/c3-swipe.js +138 -0
  88. package/workspace/public/pm/c3-terminal-data.js +26 -0
  89. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  90. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  91. package/workspace/public/pm/c3-terminal-results.js +141 -0
  92. package/workspace/public/pm/c3-terminal.js +52 -0
  93. package/workspace/public/pm/c3-tickets.js +163 -0
  94. package/workspace/public/pm/c3-timeline.js +201 -0
  95. package/workspace/public/pm/c3-toast.js +16 -0
  96. package/workspace/public/pm/calendar-views.js +1015 -0
  97. package/workspace/public/pm/console3-lift.md +220 -0
  98. package/workspace/public/pm/console3.css +1480 -0
  99. package/workspace/public/pm/dest-initiatives.js +16 -3
  100. package/workspace/public/pm/dest-mywork.js +8 -1
  101. package/workspace/public/pm/dest-notes.js +25 -3
  102. package/workspace/public/pm/dest-reports.js +47 -1
  103. package/workspace/public/pm/dest-today.js +55 -1
  104. package/workspace/public/pm/doc-health-panel.js +110 -0
  105. package/workspace/public/pm/doc-rail.js +9 -3
  106. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  107. package/workspace/public/pm/lifecycle-lens.js +261 -0
  108. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  109. package/workspace/public/pm/outbox-panel.js +125 -0
  110. package/workspace/public/pm/pm-work-view.js +919 -0
  111. package/workspace/public/pm/releases-view.js +248 -0
  112. package/workspace/public/pm/report-families-view.js +503 -0
  113. package/workspace/public/pm/run-card.js +244 -0
  114. package/workspace/public/pm/sessions-panel.js +149 -0
  115. package/workspace/public/pm/shell-bell.js +162 -0
  116. package/workspace/public/pm/shell-model.js +83 -0
  117. package/workspace/public/pm/shell-phone.js +124 -0
  118. package/workspace/public/pm/shell-readaloud.js +53 -0
  119. package/workspace/public/pm/shell-recording.js +110 -0
  120. package/workspace/public/pm/shell-ticker.js +90 -0
  121. package/workspace/public/pm/slide-confirm.js +163 -0
  122. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  123. package/workspace/public/pm/today-day.js +8 -2
  124. package/workspace/public/pm/tokens.css +1 -0
  125. package/workspace/server.js +5 -2
  126. package/workspace/test-files.json +8 -0
  127. package/workspace/test-lists/browser-suites-green.json +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.123.0",
3
+ "version": "0.125.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@d13a7b9
2
- assembled: 2026-10-09
1
+ kernel-source: pmos@d18afe7
2
+ assembled: 2026-10-11
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-09T03:00:14Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T02:26:05Z"}
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-09T03:00:14Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T02:26:05Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+ /*
3
+ * The nine stage names as a label layer over the workflow engine's derived stage (run 3).
4
+ *
5
+ * The engine derives one of six rungs (intake, spec, contract, build, eval, accepted) from the steps
6
+ * it found. This file names the nine stages the PM sees and says which of them each lane uses. It
7
+ * never decides a stage itself: a stage is `done` only when a derived signal shows it, and a stage
8
+ * no record can yet show is `no-signal`, not guessed.
9
+ *
10
+ * The lane sets below are a proposal for the PM to correct (see the run 3 contract in the PRD).
11
+ */
12
+
13
+ const STAGES = ['discover', 'ideate', 'strategize', 'prioritize', 'plan', 'build', 'deliver', 'monitor', 'evaluate'];
14
+
15
+ const LABELS = {
16
+ discover: 'Discover', ideate: 'Ideate', strategize: 'Strategize', prioritize: 'Prioritize', plan: 'Plan',
17
+ build: 'Build', deliver: 'Deliver', monitor: 'Monitor', evaluate: 'Evaluate',
18
+ };
19
+
20
+ // Which stages each product-code lane uses. Stages not listed read `skipped`.
21
+ const LANE_STAGES = {
22
+ feature: STAGES,
23
+ discovery: ['discover', 'ideate', 'strategize', 'prioritize', 'evaluate'],
24
+ chore: ['plan', 'build', 'evaluate'],
25
+ incident: ['build', 'deliver', 'monitor', 'evaluate'],
26
+ content: ['ideate', 'plan', 'build', 'deliver'],
27
+ prototype: ['discover', 'ideate', 'evaluate'],
28
+ };
29
+
30
+ // Stages that have a derived signal today. Every other stage reads `no-signal` until its record exists.
31
+ const SIGNALLED = new Set(['discover', 'plan', 'build', 'evaluate']);
32
+
33
+ const RANK = { intake: 0, spec: 1, contract: 2, build: 3, eval: 4, accepted: 5 };
34
+
35
+ // Stages that gain a signal only when the caller passes the records (scripts/records readAll plus optional `releases`).
36
+ // With no records the output is exactly the older output, so the lifecycle-lens mirror stays equal.
37
+ const RECORD_SIGNALLED = new Set(['ideate', 'strategize', 'prioritize', 'monitor', 'deliver']);
38
+
39
+ // records: { initiative?, anchor?, ideas, hypotheses, decisions, bets, 'kr-metrics', releases } (every list optional).
40
+ // The initiative id and anchor come from `derived` (id, anchor) or from records.initiative / records.anchor.
41
+ function recordSignals(derived, records, buildDone) {
42
+ const id = (derived && derived.id) || records.initiative || null;
43
+ const anchor = (derived && derived.anchor) || records.anchor || null;
44
+ const mine = (list) => (list || []).filter((r) => id && r.initiative === id);
45
+ const kr = (records['kr-metrics'] || []).filter((m) => anchor && (m.rid === anchor || m.parent_kr === anchor || 'kr:' + m.rid === anchor));
46
+ return {
47
+ ideate: mine(records.ideas).length + mine(records.hypotheses).length > 0,
48
+ strategize: mine(records.decisions).length + mine(records.bets).length > 0,
49
+ prioritize: mine(records.bets).some((b) => ['now', 'next', 'later'].includes(b.horizon) && Number.isInteger(b.rank)),
50
+ monitor: !!buildDone && kr.some((m) => (m.readings || []).length > 0),
51
+ deliver: mine(records.releases).length > 0,
52
+ };
53
+ }
54
+
55
+ function nineStages(lane, derived, records) {
56
+ const laneSet = new Set(LANE_STAGES[lane] || STAGES);
57
+ const rank = RANK[derived && derived.stage] ?? 0;
58
+ const step = (key) => (derived && derived.steps || []).find((s) => s.key === key);
59
+ const done = {
60
+ discover: rank >= 1,
61
+ plan: rank >= 2,
62
+ build: rank >= 3 || (step('build') && step('build').status === 'done'),
63
+ evaluate: rank >= 4,
64
+ };
65
+ const accepted = rank >= 5;
66
+ const fromRecords = records && typeof records === 'object' ? recordSignals(derived, records, done.build) : null;
67
+ const out = [];
68
+ let currentSet = false;
69
+ for (const key of STAGES) {
70
+ let status;
71
+ if (!laneSet.has(key)) status = 'skipped';
72
+ else if (!SIGNALLED.has(key) && !(fromRecords && RECORD_SIGNALLED.has(key) && (key !== 'deliver' || fromRecords.deliver))) status = 'no-signal';
73
+ else if (done[key] || (fromRecords && fromRecords[key])) status = 'done';
74
+ else status = 'pending';
75
+ if (status === 'pending' && !currentSet) { status = 'current'; currentSet = true; }
76
+ out.push({ key, label: LABELS[key], status, accepted: key === 'evaluate' ? accepted : undefined });
77
+ }
78
+ return out;
79
+ }
80
+
81
+ module.exports = { STAGES, LABELS, LANE_STAGES, nineStages };
@@ -36,6 +36,7 @@
36
36
  * Zero dependencies, stock Node. It runs in CI, in a terminal, and inside the workspace server.
37
37
  */
38
38
 
39
+ const { nineStages } = require('./nine-stages');
39
40
  const fs = require('fs');
40
41
  const path = require('path');
41
42
  const { execFileSync } = require('child_process');
@@ -1178,6 +1179,7 @@ function computeWorkflowDerived(id, opts) {
1178
1179
  ok: true, id, lane, laneKnown: knownLane, legacy, in_change: inChange,
1179
1180
  last_activity: lastActivity, accepted: accepted.length > 0, stage, record: recRel,
1180
1181
  title: fm.title || id, anchor: fm.parent_kr || null, profile: 'product-code',
1182
+ nine_stages: nineStages(effLane, { stage, steps }),
1181
1183
  ...facts,
1182
1184
  steps, blockers: blockers.map((s) => s.key), gate_failures: gateFailures.map((s) => s.rule || s.key),
1183
1185
  next: next ? { step: next.key, why: next.evidence, action: next.next || next.evidence } : null,
@@ -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-09T03:00:14Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-11T02:26:05Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  Newest first, ISO dates.
4
4
 
5
- ## 2026-10-09
5
+ ## 2026-10-11
6
6
 
7
7
  - Adopted OKF v0.2 (D73).
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-one-home-test.js — the one-home rule as a check (console3 feedback plan, section 2; initiative browser-suites-green).
4
+ *
5
+ * Rule: an item is actionable in exactly one place, its home. Anywhere else it may appear only as a count, a date marker or
6
+ * a reference that opens the home. This suite loads the console3 page with the demo fixture (/c3.html?demo=1), opens every
7
+ * tab, and collects each item that carries an action control (a button, or an element with data-a) inside that tab's pane.
8
+ * An item is named by its title text, so the same item drawn by two different renderers still matches. It fails when one
9
+ * item has an action control on more than one tab.
10
+ *
11
+ * The page breaks the rule today, and the restructure that fixes it is Phase 2 of the plan. Until then the duplicates that
12
+ * exist now are listed in fixtures/one-home-baseline.json: they are printed, not failed. A duplicate that is NOT in the
13
+ * baseline fails the suite, so the count can only go down. When a duplicate goes away, remove it from the baseline (the
14
+ * suite says which ones are stale). Run with --write-baseline to rewrite the file from the current page.
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawn } = require('child_process');
19
+ const { findBrowser } = require('./lib/find-browser');
20
+ const T = require('./test-run').configure({ name: 'one-home' });
21
+
22
+ const BASE = T.BASE;
23
+ const BASELINE = path.join(__dirname, 'fixtures', 'one-home-baseline.json');
24
+ const WRITE = process.argv.includes('--write-baseline');
25
+ const TABS = ['brief', 'cal', 'work', 'init', 'rep'];
26
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
27
+ const results = [];
28
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
29
+ let srv = null; let browser = null;
30
+ const finish = async (code) => {
31
+ if (browser) await browser.close().catch(() => {});
32
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
33
+ T.killTmux();
34
+ const bad = results.filter((x) => !x.cond).length;
35
+ console.log(`\n${results.length - bad}/${results.length} one-home assertions passed`);
36
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
37
+ };
38
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
39
+
40
+ /* Runs in the page: every item in a pane that carries an action control, named by its title. */
41
+ function collect(tab) {
42
+ const pane = document.querySelector('[data-c3-pane="' + tab + '"]');
43
+ if (!pane) return null;
44
+ const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
45
+ const visible = (e) => !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length) && !e.closest('[hidden]');
46
+ const ITEM = '[data-row], [data-id], [data-due-row], [data-needs-row], [data-draft-row], li, article';
47
+ const out = new Map();
48
+ // A control is a button, an element with a press action (data-a), or a row that acts by itself: a swipe row or a focusable
49
+ // row (Work's rows have no buttons; a swipe or an arrow key is the action).
50
+ for (const ctl of pane.querySelectorAll('button, [data-a], [role="button"], [data-swipe], [data-row][tabindex]')) {
51
+ if (!visible(ctl) || ctl.disabled) continue;
52
+ const item = ctl.closest(ITEM);
53
+ if (!item || !pane.contains(item)) continue;
54
+ const t = item.querySelector('[data-pm-title], .tt, h2, b, strong, .t, [data-title], h3, h4');
55
+ const title = norm(t ? t.textContent : (item.textContent || '').split('\n')[0]).slice(0, 80);
56
+ if (title.length < 4) continue;
57
+ const id = item.getAttribute('data-id') || item.getAttribute('data-needs-row') || item.getAttribute('data-due-row') || item.getAttribute('data-draft-row') || '';
58
+ const label = ctl === item ? 'row' : norm(ctl.getAttribute('aria-label') || ctl.textContent).slice(0, 40);
59
+ if (!out.has(title)) out.set(title, { title, id, controls: [] });
60
+ if (!out.get(title).controls.includes(label)) out.get(title).controls.push(label);
61
+ }
62
+ return [...out.values()];
63
+ }
64
+
65
+ (async () => {
66
+ const exe = findBrowser();
67
+ if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the one-home suite cannot run'); return finish(1); }
68
+ let chromium;
69
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the one-home suite cannot run'); return finish(1); }
70
+
71
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
72
+ await T.waitForServer(BASE);
73
+ browser = await chromium.launch({ executablePath: exe });
74
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
75
+ const page = await ctx.newPage();
76
+ const errors = [];
77
+ page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
78
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
79
+
80
+ await page.goto(BASE + '/c3.html?demo=1');
81
+ const up = await page.waitForSelector('[data-c3-demo="1"] [data-c3-tab="brief"]', { timeout: 30000 }).catch(() => null);
82
+ ok('the console3 page comes up in demo mode', !!up);
83
+ if (!up) return finish(1);
84
+
85
+ const byTab = {};
86
+ for (const tab of TABS) {
87
+ await page.click('[data-c3-tab="' + tab + '"]');
88
+ // A pane builds on first show and its views read the demo's /api answers; wait until its item count settles.
89
+ let last = -1; let items = null;
90
+ for (let i = 0; i < 20; i++) {
91
+ await sleep(250);
92
+ items = await page.evaluate(collect, tab);
93
+ const n = items ? items.length : -1;
94
+ if (n === last && n > 0) break;
95
+ last = n;
96
+ }
97
+ byTab[tab] = items || [];
98
+ ok(`the ${tab} tab shows items with action controls`, byTab[tab].length > 0, JSON.stringify(items));
99
+ }
100
+
101
+ // Every title that has an action control on two or more tabs.
102
+ const where = new Map();
103
+ for (const tab of TABS) for (const it of byTab[tab]) {
104
+ if (!where.has(it.title)) where.set(it.title, []);
105
+ where.get(it.title).push({ tab, controls: it.controls });
106
+ }
107
+ const dupes = [...where.entries()].filter(([, w]) => w.length > 1).map(([title, w]) => ({ title, tabs: w.map((x) => x.tab) }))
108
+ .sort((a, b) => a.title.localeCompare(b.title));
109
+
110
+ if (process.env.ONE_HOME_DUMP) for (const tab of TABS) console.log(tab, JSON.stringify(byTab[tab].map((i) => [i.title, i.id, i.controls.join(",")])));
111
+ console.log(`\none-home: ${where.size} actionable items over ${TABS.length} tabs; ${dupes.length} actionable on more than one tab`);
112
+ for (const d of dupes) console.log(` · ${d.tabs.join(' + ')} ${d.title}`);
113
+
114
+ if (WRITE) {
115
+ fs.writeFileSync(BASELINE, JSON.stringify({
116
+ _comment: 'Items the console3 page makes actionable on more than one tab today (c3-one-home-test.js). Known-failing until the Phase 2 restructure of the console3 feedback plan; the suite fails only on a duplicate that is not listed here. Remove an entry when its duplicate is gone.',
117
+ duplicates: dupes,
118
+ }, null, 2) + '\n');
119
+ console.log(`wrote ${path.relative(__dirname, BASELINE)} with ${dupes.length} entries`);
120
+ }
121
+ const base = fs.existsSync(BASELINE) ? JSON.parse(fs.readFileSync(BASELINE, 'utf8')).duplicates || [] : [];
122
+ const known = new Set(base.map((d) => d.title));
123
+ const fresh = dupes.filter((d) => !known.has(d.title));
124
+ const stale = base.filter((d) => !where.has(d.title) || where.get(d.title).length < 2);
125
+ ok('no item is actionable on more than one tab, other than the known duplicates in the baseline', fresh.length === 0,
126
+ fresh.map((d) => `${d.tabs.join('+')}: ${d.title}`).join(' | '));
127
+ if (stale.length) console.log(`· ${stale.length} baseline entr${stale.length === 1 ? 'y is' : 'ies are'} no longer duplicated; remove ${stale.length === 1 ? 'it' : 'them'} from the baseline: ${stale.map((d) => d.title).join(' | ')}`);
128
+ console.log(`· known-failing: ${dupes.length - fresh.length} duplicate(s) wait for the Phase 2 restructure`);
129
+ ok('the page raised no script error', errors.length === 0, errors.slice(0, 3).join(' | '));
130
+ return finish();
131
+ })();
@@ -69,7 +69,63 @@
69
69
  "label": "7 of 12"
70
70
  },
71
71
  "next_due": "2026-11-14",
72
- "last_activity": "2026-10-04T09:00:00Z"
72
+ "last_activity": "2026-10-04T09:00:00Z",
73
+ "nine_stages": [
74
+ {
75
+ "key": "discover",
76
+ "label": "Discover",
77
+ "status": "pending",
78
+ "accepted": false
79
+ },
80
+ {
81
+ "key": "ideate",
82
+ "label": "Ideate",
83
+ "status": "pending",
84
+ "accepted": false
85
+ },
86
+ {
87
+ "key": "strategize",
88
+ "label": "Strategize",
89
+ "status": "pending",
90
+ "accepted": false
91
+ },
92
+ {
93
+ "key": "prioritize",
94
+ "label": "Prioritize",
95
+ "status": "pending",
96
+ "accepted": false
97
+ },
98
+ {
99
+ "key": "plan",
100
+ "label": "Plan",
101
+ "status": "pending",
102
+ "accepted": false
103
+ },
104
+ {
105
+ "key": "build",
106
+ "label": "Build",
107
+ "status": "pending",
108
+ "accepted": false
109
+ },
110
+ {
111
+ "key": "deliver",
112
+ "label": "Deliver",
113
+ "status": "pending",
114
+ "accepted": false
115
+ },
116
+ {
117
+ "key": "monitor",
118
+ "label": "Monitor",
119
+ "status": "pending",
120
+ "accepted": false
121
+ },
122
+ {
123
+ "key": "evaluate",
124
+ "label": "Evaluate",
125
+ "status": "pending",
126
+ "accepted": false
127
+ }
128
+ ]
73
129
  }
74
130
  ]
75
131
  }
@@ -124,7 +180,63 @@
124
180
  "label": "3 of 5"
125
181
  },
126
182
  "next_due": "2026-10-20",
127
- "last_activity": "2026-09-29T15:00:00Z"
183
+ "last_activity": "2026-09-29T15:00:00Z",
184
+ "nine_stages": [
185
+ {
186
+ "key": "discover",
187
+ "label": "Discover",
188
+ "status": "pending",
189
+ "accepted": false
190
+ },
191
+ {
192
+ "key": "ideate",
193
+ "label": "Ideate",
194
+ "status": "pending",
195
+ "accepted": false
196
+ },
197
+ {
198
+ "key": "strategize",
199
+ "label": "Strategize",
200
+ "status": "pending",
201
+ "accepted": false
202
+ },
203
+ {
204
+ "key": "prioritize",
205
+ "label": "Prioritize",
206
+ "status": "pending",
207
+ "accepted": false
208
+ },
209
+ {
210
+ "key": "plan",
211
+ "label": "Plan",
212
+ "status": "pending",
213
+ "accepted": false
214
+ },
215
+ {
216
+ "key": "build",
217
+ "label": "Build",
218
+ "status": "pending",
219
+ "accepted": false
220
+ },
221
+ {
222
+ "key": "deliver",
223
+ "label": "Deliver",
224
+ "status": "pending",
225
+ "accepted": false
226
+ },
227
+ {
228
+ "key": "monitor",
229
+ "label": "Monitor",
230
+ "status": "pending",
231
+ "accepted": false
232
+ },
233
+ {
234
+ "key": "evaluate",
235
+ "label": "Evaluate",
236
+ "status": "pending",
237
+ "accepted": false
238
+ }
239
+ ]
128
240
  },
129
241
  {
130
242
  "id": "old-import",
@@ -159,7 +271,63 @@
159
271
  "label": "no breakdown"
160
272
  },
161
273
  "next_due": null,
162
- "last_activity": "2026-07-01T10:00:00Z"
274
+ "last_activity": "2026-07-01T10:00:00Z",
275
+ "nine_stages": [
276
+ {
277
+ "key": "discover",
278
+ "label": "Discover",
279
+ "status": "pending",
280
+ "accepted": false
281
+ },
282
+ {
283
+ "key": "ideate",
284
+ "label": "Ideate",
285
+ "status": "pending",
286
+ "accepted": false
287
+ },
288
+ {
289
+ "key": "strategize",
290
+ "label": "Strategize",
291
+ "status": "pending",
292
+ "accepted": false
293
+ },
294
+ {
295
+ "key": "prioritize",
296
+ "label": "Prioritize",
297
+ "status": "pending",
298
+ "accepted": false
299
+ },
300
+ {
301
+ "key": "plan",
302
+ "label": "Plan",
303
+ "status": "pending",
304
+ "accepted": false
305
+ },
306
+ {
307
+ "key": "build",
308
+ "label": "Build",
309
+ "status": "pending",
310
+ "accepted": false
311
+ },
312
+ {
313
+ "key": "deliver",
314
+ "label": "Deliver",
315
+ "status": "pending",
316
+ "accepted": false
317
+ },
318
+ {
319
+ "key": "monitor",
320
+ "label": "Monitor",
321
+ "status": "pending",
322
+ "accepted": false
323
+ },
324
+ {
325
+ "key": "evaluate",
326
+ "label": "Evaluate",
327
+ "status": "pending",
328
+ "accepted": false
329
+ }
330
+ ]
163
331
  }
164
332
  ]
165
333
  }