create-pmos 0.52.0 → 0.53.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 (212) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/README.md +13 -0
  4. package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
  6. package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
  7. package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/plan-items-stage.js +271 -0
  11. package/payload/scripts/pmos-log.sh +18 -7
  12. package/payload/scripts/workflow-state.js +28 -2
  13. package/payload/state/discoveries.md +1 -1
  14. package/payload/state/log.md +1 -1
  15. package/workspace/bin/pmos.js +15 -1
  16. package/workspace/cut-stub.js +40 -0
  17. package/workspace/first-paint-audit.js +145 -0
  18. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  19. package/workspace/fixtures/initiatives-api/list.json +176 -0
  20. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  21. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  22. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  23. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  24. package/workspace/fixtures/report-api/draft.json +177 -0
  25. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  26. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  27. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  28. package/workspace/guide/board.md +81 -0
  29. package/workspace/guide/breakdown.md +154 -0
  30. package/workspace/guide/command-palette.md +75 -0
  31. package/workspace/guide/index.md +27 -17
  32. package/workspace/guide/initiatives-roadmap.md +130 -0
  33. package/workspace/guide/initiatives-screen.md +89 -0
  34. package/workspace/guide/initiatives-timeline.md +57 -0
  35. package/workspace/guide/integrated-work.md +94 -0
  36. package/workspace/guide/knowledge-graph.md +3 -68
  37. package/workspace/guide/layout.md +98 -32
  38. package/workspace/guide/live-notes.md +3 -3
  39. package/workspace/guide/pm-only-acts.md +89 -0
  40. package/workspace/guide/pm-only-lines.md +61 -0
  41. package/workspace/guide/reports-page.md +80 -0
  42. package/workspace/guide/reports.md +73 -0
  43. package/workspace/guide/timeline.md +70 -0
  44. package/workspace/guide/today.md +85 -0
  45. package/workspace/guide/validate-with-typesafe.md +1 -1
  46. package/workspace/lib/actor.js +91 -0
  47. package/workspace/lib/board-cache.js +32 -2
  48. package/workspace/lib/board-cli.js +108 -0
  49. package/workspace/lib/board-fold.js +232 -0
  50. package/workspace/lib/breakdown-export.js +80 -0
  51. package/workspace/lib/capture.js +98 -0
  52. package/workspace/lib/engine/plan-items-stage.js +271 -0
  53. package/workspace/lib/engine/workflow-state.js +28 -2
  54. package/workspace/lib/factory-join.js +158 -0
  55. package/workspace/lib/grant-mint.js +113 -0
  56. package/workspace/lib/health-rules-factory.js +92 -0
  57. package/workspace/lib/health-rules-plan.js +164 -0
  58. package/workspace/lib/health.js +79 -0
  59. package/workspace/lib/initiative-cli.js +222 -0
  60. package/workspace/lib/initiative-display.js +183 -0
  61. package/workspace/lib/initiative-needs.js +127 -0
  62. package/workspace/lib/initiative-rows.js +206 -0
  63. package/workspace/lib/initiative-writer.js +141 -0
  64. package/workspace/lib/issues.js +206 -0
  65. package/workspace/lib/item-cli.js +195 -0
  66. package/workspace/lib/item-spans.js +137 -0
  67. package/workspace/lib/measure-cli.js +100 -0
  68. package/workspace/lib/measurements.js +189 -0
  69. package/workspace/lib/mobile-nav.js +57 -53
  70. package/workspace/lib/needs-providers.js +120 -0
  71. package/workspace/lib/placements.js +440 -0
  72. package/workspace/lib/plan-items.js +922 -0
  73. package/workspace/lib/plan-ledgers.js +85 -0
  74. package/workspace/lib/plan-rules.js +72 -0
  75. package/workspace/lib/plan-tree.js +367 -0
  76. package/workspace/lib/pm-guard.js +174 -0
  77. package/workspace/lib/pm-lines-needs.js +331 -0
  78. package/workspace/lib/pmos-verbs.js +66 -0
  79. package/workspace/lib/proactive.js +7 -4
  80. package/workspace/lib/report-cli.js +152 -0
  81. package/workspace/lib/report-draft.js +579 -0
  82. package/workspace/lib/report-needs.js +213 -0
  83. package/workspace/lib/route-modules.js +128 -0
  84. package/workspace/lib/routes-board.js +91 -0
  85. package/workspace/lib/routes-capture.js +52 -0
  86. package/workspace/lib/routes-initiatives.js +371 -0
  87. package/workspace/lib/routes-reports.js +163 -0
  88. package/workspace/lib/routes-timeline.js +197 -0
  89. package/workspace/lib/routes-today.js +32 -0
  90. package/workspace/lib/today-cli.js +99 -0
  91. package/workspace/lib/today.js +333 -0
  92. package/workspace/lib/triage.js +163 -0
  93. package/workspace/lib/verify-work-cli.js +81 -0
  94. package/workspace/lib/work-check-needs.js +99 -0
  95. package/workspace/package-lock.json +1 -1
  96. package/workspace/package.json +4 -5
  97. package/workspace/public/index.html +29 -19
  98. package/workspace/public/pm/app.js +299 -30
  99. package/workspace/public/pm/bd-board.js +113 -0
  100. package/workspace/public/pm/bd-drag.js +59 -0
  101. package/workspace/public/pm/bd-model.js +152 -0
  102. package/workspace/public/pm/bd-timeline.js +233 -0
  103. package/workspace/public/pm/bd-tree.js +244 -0
  104. package/workspace/public/pm/board-copy.js +57 -0
  105. package/workspace/public/pm/breakdown-api.js +58 -0
  106. package/workspace/public/pm/breakdown-copy.js +138 -0
  107. package/workspace/public/pm/components.css +3054 -0
  108. package/workspace/public/pm/cycle-review.js +125 -0
  109. package/workspace/public/pm/data.js +40 -187
  110. package/workspace/public/pm/dest-board.js +438 -0
  111. package/workspace/public/pm/dest-initiatives.js +134 -0
  112. package/workspace/public/pm/dest-reports.js +270 -0
  113. package/workspace/public/pm/dest-settings.js +217 -0
  114. package/workspace/public/pm/dest-today.js +176 -0
  115. package/workspace/public/pm/dialogs.js +88 -0
  116. package/workspace/public/pm/display-word.js +56 -0
  117. package/workspace/public/pm/dock.js +105 -0
  118. package/workspace/public/pm/fixture.js +8 -227
  119. package/workspace/public/pm/frame-copy.js +94 -0
  120. package/workspace/public/pm/handoff-box.js +82 -0
  121. package/workspace/public/pm/http.js +88 -0
  122. package/workspace/public/pm/icons.js +53 -0
  123. package/workspace/public/pm/initiative-list.js +218 -0
  124. package/workspace/public/pm/initiative-sheet.js +261 -0
  125. package/workspace/public/pm/initiatives-api.js +59 -0
  126. package/workspace/public/pm/initiatives-copy.js +215 -0
  127. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  128. package/workspace/public/pm/initiatives-timeline.js +410 -0
  129. package/workspace/public/pm/initiatives-ui.js +123 -0
  130. package/workspace/public/pm/item-sheet.js +251 -0
  131. package/workspace/public/pm/keep-card.js +204 -0
  132. package/workspace/public/pm/lifecycle.js +7 -1
  133. package/workspace/public/pm/mobile.css +49 -20
  134. package/workspace/public/pm/mobile.js +157 -29
  135. package/workspace/public/pm/modes-live.js +7 -883
  136. package/workspace/public/pm/modes.js +32 -1024
  137. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  138. package/workspace/public/pm/next-step-ui.js +3 -2
  139. package/workspace/public/pm/palette-copy.js +51 -0
  140. package/workspace/public/pm/palette.js +312 -0
  141. package/workspace/public/pm/panel-breakdown.js +702 -0
  142. package/workspace/public/pm/panel-cockpit.js +63 -295
  143. package/workspace/public/pm/panel-detail.js +25 -172
  144. package/workspace/public/pm/panel-initiatives.js +1 -19
  145. package/workspace/public/pm/panel-repos.js +31 -81
  146. package/workspace/public/pm/panel-tabs.js +197 -110
  147. package/workspace/public/pm/panel-trees.js +5 -2
  148. package/workspace/public/pm/portfolio-report.js +61 -0
  149. package/workspace/public/pm/primitives.js +656 -0
  150. package/workspace/public/pm/print.css +33 -0
  151. package/workspace/public/pm/provider-cards.js +136 -0
  152. package/workspace/public/pm/registry.js +194 -0
  153. package/workspace/public/pm/release-card.js +110 -0
  154. package/workspace/public/pm/report-view.js +309 -0
  155. package/workspace/public/pm/reports-copy.js +114 -0
  156. package/workspace/public/pm/roadmap.js +271 -0
  157. package/workspace/public/pm/sig-watch.js +41 -0
  158. package/workspace/public/pm/state.js +8 -4
  159. package/workspace/public/pm/status-banner.js +13 -5
  160. package/workspace/public/pm/tab-doc.js +2 -4
  161. package/workspace/public/pm/tab-term.js +12 -2
  162. package/workspace/public/pm/tab-web.js +3 -2
  163. package/workspace/public/pm/timeline-copy.js +48 -0
  164. package/workspace/public/pm/today-cards.js +244 -0
  165. package/workspace/public/pm/today-copy.js +97 -0
  166. package/workspace/public/pm/tokens.css +16 -18
  167. package/workspace/public/pm/topbar.js +102 -0
  168. package/workspace/public/pm/triage-card.js +60 -0
  169. package/workspace/public/pm/undo-hold.js +103 -0
  170. package/workspace/public/pm/view-addrepo.js +2 -1
  171. package/workspace/public/pm/view-livenotes.js +3 -1
  172. package/workspace/public/pm/view-today.js +7 -162
  173. package/workspace/public/vendor/Sortable.min.js +2 -0
  174. package/workspace/public/vendor/lucide-sprite.js +79 -0
  175. package/workspace/replaced-tests.json +2864 -0
  176. package/workspace/run-tests.js +56 -5
  177. package/workspace/server.js +312 -36
  178. package/workspace/sheet-answer.js +61 -0
  179. package/workspace/skills/measure.skill +41 -0
  180. package/workspace/skills/report-biweekly.skill +23 -25
  181. package/workspace/skills/tracker-refresh.skill +50 -0
  182. package/workspace/test-files.json +19 -3
  183. package/workspace/test-lists/base.json +6 -0
  184. package/workspace/test-lists/run2-a.json +6 -0
  185. package/workspace/test-lists/run2-b.json +13 -0
  186. package/workspace/test-lists/run2-c.json +19 -0
  187. package/workspace/test-lists/run2-d.json +22 -0
  188. package/workspace/test-lists/run3-0.json +5 -0
  189. package/workspace/test-lists/run3-a.json +5 -0
  190. package/workspace/test-lists/run3-b.json +6 -0
  191. package/workspace/test-lists/run3-c.json +15 -0
  192. package/workspace/test-lists/run3-d.json +6 -0
  193. package/workspace/test-lists/run4-a.json +8 -0
  194. package/workspace/test-lists/run4-b.json +6 -0
  195. package/workspace/test-lists/run4-c.json +13 -0
  196. package/workspace/test-lists/run4-d.json +9 -0
  197. package/workspace/test-lists/run5-a.json +6 -0
  198. package/workspace/test-lists/run5-b.json +6 -0
  199. package/workspace/test-lists/run5-c.json +15 -0
  200. package/workspace/test-run.js +27 -1
  201. package/workspace/third-party/README.md +44 -0
  202. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  203. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  204. package/workspace/design-parity.js +0 -673
  205. package/workspace/public/pm/tab-task.js +0 -86
  206. package/workspace/public/pm/view-guide.js +0 -141
  207. package/workspace/public/pm/view-knowledge.js +0 -225
  208. package/workspace/public/pm/view-memory.js +0 -122
  209. package/workspace/public/pm/view-portfolio.js +0 -153
  210. package/workspace/public/pm/view-reports.js +0 -190
  211. package/workspace/public/pm/view-scheduled.js +0 -161
  212. package/workspace/public/pm/view-skills.js +0 -216
@@ -0,0 +1,438 @@
1
+ /* pm/dest-board.js — the Board destination: every initiative's items in one Kanban (pm-os-absorb Run 4, slice 4D; PRD R9, R19).
2
+ *
3
+ * A destination module (registry.js): `mountDestination(host, ctx)` runs on EVERY render with the same host and rebuilds only
4
+ * when its own data changed; `disposeDestination(host)` runs when the tab closes. It draws the Breakdown's Board for all
5
+ * the work initiatives at once:
6
+ * - the filters: Active (the default), Mine, and one initiative; the one primary action, + Add item;
7
+ * - the Release cards for the ready batches, above the columns (release-card.js);
8
+ * - the four columns, drawn by bd-board.js renderBoard and moved by the same actions as the Breakdown's Board: a drag,
9
+ * the card's ⋯ menu, or the item sheet's state buttons. Done is held for Undo (undo-hold.js).
10
+ *
11
+ * WHERE THE DATA COMES FROM. GET /api/board (lib/routes-board.js) answers every card with the state that its tree row has,
12
+ * because both read one fold. The page keeps the last answer and draws from it; it stores nothing else (D33). A write is
13
+ * drawn at once on a copy, POSTed to /api/plan-items, and the Board is read again, so the screen never keeps a change
14
+ * that was not saved. An agent's card that you drag reads "moved by you": the writer records the move with override.
15
+ *
16
+ * WHEN IT READS AGAIN. On a change of this workspace's ledgers (data.js polls them while a pane is open) and, through
17
+ * sig-watch.js watchSig, when the `sig` of GET /api/board?only=sig changes: a placement, a factory change or a write from
18
+ * the CLI that the ledger poll does not see.
19
+ *
20
+ * ONE INITIATIVE'S STAGES, MANY INITIATIVES' BOARD. renderBoard draws a card's meta from the layout's stages, so the page
21
+ * gives it one combined list: a stage id is `<initiative>::<stage>` and its label is "<initiative title> · <stage>". The
22
+ * item sheet and the writes use the plain stage id of the item's own initiative.
23
+ */
24
+
25
+ import { el, setChildren } from './dom.js';
26
+ import { state, FIXTURE } from './state.js';
27
+ import { wsOf, watchLedgers } from './data.js';
28
+ import { currentProduct } from './board-read.js';
29
+ import { pmGet } from './http.js';
30
+ import { postEvents } from './breakdown-api.js';
31
+ import { watchSig } from './sig-watch.js';
32
+ import { ensureComponentsCss, layer, tabs, field, button, sheet, toast, empty, menu } from './primitives.js';
33
+ import { ensureSprite } from './icons.js';
34
+ import { COPY as BD } from './breakdown-copy.js';
35
+ import { COPY } from './board-copy.js';
36
+ import { hold, cancel, isHeld, flush, overlay, anyHeld } from './undo-hold.js';
37
+ import { loadSortable, baseOptions, dragSet } from './bd-drag.js';
38
+ import { renderBoard, bindBoardDrag, WIDE_BOARD } from './bd-board.js';
39
+ import { openItemSheet } from './item-sheet.js';
40
+ import { recount, moveLocal, setLocal, flagLocal, removeLocal, keepLocal } from './bd-model.js';
41
+ import { releaseCard, grantScopes } from './release-card.js';
42
+
43
+ const SEP = '::';
44
+ const keyOf = (initiative, stage) => initiative + SEP + stage;
45
+ const plainStage = (stage) => { const s = String(stage); const i = s.indexOf(SEP); return i < 0 ? s : s.slice(i + SEP.length); };
46
+ const enc = encodeURIComponent;
47
+ const clone = (o) => (typeof structuredClone === 'function' ? structuredClone(o) : JSON.parse(JSON.stringify(o)));
48
+ const STATE_LABEL = (s) => BD.states[s] || s;
49
+
50
+ const panes = new WeakMap(); // host -> pane
51
+ const live = new Set();
52
+
53
+ /** The answer of GET /api/board as the layout renderBoard and the item sheet read: one stage list for every initiative. */
54
+ function layoutOf(out) {
55
+ const stages = [];
56
+ for (const ini of out.initiatives || []) {
57
+ for (const s of ini.stages || []) {
58
+ stages.push({ id: keyOf(ini.id, s.id), label: COPY.stageOf(ini.title, s.label), initiative: ini.id, items: [], visible: true });
59
+ }
60
+ }
61
+ const items = {};
62
+ for (const [id, it] of Object.entries(out.items || {})) items[id] = { ...it, stage: keyOf(it.initiative, it.stage) };
63
+ return { items, stages, board: clone(out.board || { todo: [], doing: [], review: [], done: [], doneOlder: 0 }), now: out.now, flags: [], batches: [] };
64
+ }
65
+
66
+ /** recount rebuilds the columns from the items, but the older Done items never reach the page: keep the server's count. */
67
+ function recountBoard(L, older) {
68
+ recount(L);
69
+ L.board.doneOlder = older || 0;
70
+ return L;
71
+ }
72
+
73
+ /** The layout drawn: the last answer, with every held change applied. */
74
+ function view(P) {
75
+ if (!P.raw) return null;
76
+ const L = clone(P.raw);
77
+ return anyHeld() ? recountBoard(overlay(L), P.older) : L;
78
+ }
79
+
80
+ /** The layout of ONE initiative for the item sheet: its own stages with their plain ids, its batches, every item by plain stage. */
81
+ function sheetView(P, initiative) {
82
+ const L = view(P);
83
+ if (!L) return null;
84
+ const ini = ((P.out && P.out.initiatives) || []).find((i) => i.id === initiative) || { stages: [], batches: [] };
85
+ const items = {};
86
+ for (const [id, it] of Object.entries(L.items)) items[id] = { ...it, stage: plainStage(it.stage) };
87
+ return { ...L, items, stages: (ini.stages || []).map((s) => ({ id: s.id, label: s.label, items: [], visible: true })), batches: ini.batches || [] };
88
+ }
89
+
90
+ async function load(P) {
91
+ if (P.loading) { P.again = true; return; }
92
+ P.loading = true;
93
+ try {
94
+ do {
95
+ P.again = false;
96
+ const f = P.filter === 'init' ? 'init:' + P.init : P.filter;
97
+ const out = await pmGet('/api/board?ws=' + enc(P.ws) + '&filter=' + enc(f));
98
+ if (P.disposed) return;
99
+ if (!out || out.ok === false) { P.error = (out && out.error) || 'could not read'; P.loaded = true; }
100
+ else { P.error = null; P.out = out; P.raw = layoutOf(out); P.older = (out.board && out.board.doneOlder) || 0; P.loaded = true; }
101
+ bump(P);
102
+ } while (P.again && !P.disposed);
103
+ } finally { P.loading = false; }
104
+ }
105
+
106
+ function bump(P) {
107
+ P.ver++;
108
+ if (P.dragging) return;
109
+ render(P);
110
+ }
111
+
112
+ function focusKeyIn(P) {
113
+ const a = document.activeElement;
114
+ if (!a || !P.root.contains(a)) return null;
115
+ const k = a.closest('[data-fk]');
116
+ return k ? k.getAttribute('data-fk') : null;
117
+ }
118
+
119
+ function headOf(P) {
120
+ const out = P.out;
121
+ const inits = (out && out.initiatives) || [];
122
+ const f = tabs({
123
+ variant: 'segmented', ariaLabel: COPY.filter, value: P.filter === 'init' ? null : P.filter,
124
+ items: [{ id: 'active', label: COPY.filters.active }, { id: 'mine', label: COPY.filters.mine }],
125
+ onChange: (v) => P.actions.setFilter(v), attrs: { 'data-bo-filter': P.filter === 'init' ? 'init:' + P.init : P.filter },
126
+ });
127
+ for (const b of f.querySelectorAll('[data-tab]')) b.setAttribute('data-fk', 'f:' + b.getAttribute('data-tab'));
128
+ const pick = field({
129
+ label: COPY.initiative, kind: 'select', hideLabel: true, value: P.filter === 'init' ? P.init : '',
130
+ options: [{ value: '', label: COPY.anyInitiative }, ...inits.map((i) => ({ value: i.id, label: i.title }))],
131
+ onCommit: (v) => P.actions.setInit(v), attrs: { 'data-bo-init': '', 'data-fk': 'init' },
132
+ });
133
+ const add = button({
134
+ label: COPY.addItem, variant: 'primary', icon: 'plus', onClick: () => P.actions.openAdd(),
135
+ attrs: { 'data-bo-add': '', 'data-fk': 'add' },
136
+ });
137
+ const n = out ? (out.counts.todo + out.counts.doing + out.counts.review + out.counts.done) : 0;
138
+ return [
139
+ el('div', { class: 'bo-head__filters' }, f, el('div', { class: 'bo-head__pick' }, pick)),
140
+ el('div', { class: 'bo-head__end' }, el('span', { class: 'bo-head__n', 'data-bo-count': String(n) }, COPY.summary(n)), add),
141
+ ];
142
+ }
143
+
144
+ function render(P) {
145
+ P.rendering = true;
146
+ try {
147
+ const fk = focusKeyIn(P);
148
+ P.drags.destroy();
149
+ const L = view(P);
150
+ P.wide = (P.root.getBoundingClientRect().width || P.lastWidth || 0) >= WIDE_BOARD;
151
+ // as the Breakdown's Board: a wide Board with a fine pointer draws cards without the ⋯ button (the 33-control budget)
152
+ P.cardMenus = !P.wide || P.coarse;
153
+ setChildren(P.headEl, headOf(P));
154
+ let kids;
155
+ if (!P.loaded) kids = [el('div', { class: 'bd-note' }, COPY.reading)];
156
+ else if (!L) kids = [el('div', { class: 'bd-note bd-note--error', role: 'alert' }, COPY.readFailed(P.error || ''))];
157
+ else {
158
+ kids = renderBoard({ P, L, A: P.actions, wide: P.wide, cardMenus: P.cardMenus });
159
+ const rel = (P.out && P.out.releases) || [];
160
+ if (rel.length) {
161
+ grantScopes(() => bump(P));
162
+ kids.unshift(el('div', { class: 'bo-releases', 'data-bo-releases': String(rel.length) },
163
+ el('div', { class: 'bo-releases__head' }, COPY.release.head),
164
+ rel.map((r) => releaseCard(r, { ws: P.ws, onDone: () => load(P) })),
165
+ el('div', { class: 'bo-releases__note' }, COPY.release.note)));
166
+ }
167
+ if (!((P.out && P.out.initiatives) || []).length) kids.unshift(el('div', { class: 'bd-note', 'data-bo-none': '' }, COPY.noInitiatives));
168
+ else if (!Object.keys(L.items).length) kids.unshift(el('div', { class: 'bd-note', 'data-bo-empty': '' }, COPY.noItems));
169
+ if (P.error) kids.unshift(el('div', { class: 'bd-note bd-note--error', role: 'alert' }, COPY.readFailed(P.error)));
170
+ }
171
+ setChildren(P.bodyEl, kids);
172
+ // a card dragged to Done before the factory's work was merged says so (R7): text on the card, never a control
173
+ if (L) {
174
+ for (const c of P.bodyEl.querySelectorAll('.bd-card[data-item]')) {
175
+ const it = L.items[c.getAttribute('data-item')];
176
+ if (it && it.state === 'done' && it.factory && it.factory.notMerged) c.appendChild(el('div', { class: 'bo-card__note', 'data-bo-notmerged': '' }, it.factory.label || COPY.notMerged));
177
+ }
178
+ }
179
+ P.root.setAttribute('data-bo-filter', P.filter === 'init' ? 'init:' + P.init : P.filter);
180
+ if (fk) {
181
+ const t = P.root.querySelector('[data-fk="' + (window.CSS && CSS.escape ? CSS.escape(fk) : fk) + '"]');
182
+ if (t && document.activeElement !== t) t.focus({ preventScroll: true });
183
+ }
184
+ P.renderedVer = P.ver;
185
+ if (P.sheet && P.sheet.open) P.sheet.refresh();
186
+ bindDrag(P);
187
+ } finally { P.rendering = false; }
188
+ }
189
+
190
+ function bindDrag(P) {
191
+ if (!P.raw || P.disposed) return;
192
+ const ver = P.ver;
193
+ loadSortable().then((Sortable) => {
194
+ if (!Sortable || P.disposed || P.ver !== ver || P.drags.size) return;
195
+ const L = view(P);
196
+ if (!L) return;
197
+ bindBoardDrag({ P, L, A: P.actions, wide: P.wide, cardMenus: P.cardMenus }, Sortable, P.drags, baseOptions);
198
+ });
199
+ }
200
+
201
+ /* ── writes ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
202
+
203
+ async function write(P, events, apply) {
204
+ if (apply && P.raw) { apply(P.raw); recountBoard(P.raw, P.older); bump(P); }
205
+ const out = await postEvents(P.ws, events);
206
+ if (!out || out.ok === false) {
207
+ toast({ text: (out && out.error) || 'nothing was saved', tone: 'error' });
208
+ await load(P);
209
+ return out;
210
+ }
211
+ await load(P);
212
+ return out;
213
+ }
214
+
215
+ function undone(P, id) {
216
+ bump(P);
217
+ const t = P.root.querySelector('[data-fk="card:' + id + '"]');
218
+ if (t && t.isConnected && !(P.sheet && P.sheet.open)) t.focus({ preventScroll: true });
219
+ }
220
+
221
+ function makeActions(P) {
222
+ const L = () => view(P);
223
+ const A = {
224
+ rerender: () => bump(P),
225
+ refresh: () => load(P),
226
+ setBoardCol(c) { if (P.boardCol === c) return; P.boardCol = c; bump(P); },
227
+ setFilter(f) {
228
+ if (P.filter === f && !P.init) return;
229
+ P.filter = f; P.init = null;
230
+ bump(P); load(P);
231
+ },
232
+ setInit(id) {
233
+ if (!id) { P.filter = 'active'; P.init = null; } else { P.filter = 'init'; P.init = id; }
234
+ bump(P); load(P);
235
+ },
236
+ moveTo(id, to) {
237
+ const it = L() && L().items[id];
238
+ if (!it) { bump(P); return; }
239
+ if (it.state === to) { bump(P); return; }
240
+ if (to === 'done') {
241
+ hold({
242
+ key: 'done:' + id, ws: P.ws, text: BD.markedDone,
243
+ events: [{ ev: 'moved', item: id, to: 'done' }],
244
+ apply: (lay) => moveLocal(lay, id, 'done'),
245
+ onDone: () => load(P), onUndo: () => undone(P, id),
246
+ });
247
+ bump(P);
248
+ return;
249
+ }
250
+ if (isHeld('done:' + id)) cancel('done:' + id);
251
+ write(P, [{ ev: 'moved', item: id, to }], (raw) => moveLocal(raw, id, to));
252
+ },
253
+ withdraw(id) {
254
+ const it = L() && L().items[id];
255
+ if (!it) return;
256
+ flush('done:' + id);
257
+ hold({
258
+ key: 'withdraw:' + id, ws: P.ws, text: BD.withdrawn,
259
+ events: [{ ev: 'withdrawn', item: id }],
260
+ apply: (lay) => removeLocal(lay, id),
261
+ onDone: () => load(P), onUndo: () => undone(P, id),
262
+ });
263
+ bump(P);
264
+ },
265
+ setFields(id, fields) {
266
+ const it = P.raw && P.raw.items[id];
267
+ const local = it && 'stage' in fields ? { ...fields, stage: keyOf(it.initiative, fields.stage) } : fields;
268
+ return write(P, [{ ev: 'set', item: id, fields }], (raw) => setLocal(raw, id, local));
269
+ },
270
+ flag(id, blocked, reason) {
271
+ return write(P, [blocked ? { ev: 'flagged', item: id, blocked: true, reason } : { ev: 'flagged', item: id, blocked: false }],
272
+ (raw) => flagLocal(raw, id, blocked, reason));
273
+ },
274
+ async keepBatch(initiative, b) {
275
+ const n = (b.items || []).length;
276
+ const out = await write(P, [{ ev: 'kept', initiative, batch: b.by, items: b.items }], (raw) => keepLocal(raw, b.by, b.items));
277
+ if (out && out.ok !== false) toast({ text: BD.kept(n) });
278
+ },
279
+ openSheet(id, opts) {
280
+ if (P.sheet && P.sheet.open) P.sheet.close();
281
+ const it = P.raw && P.raw.items[id];
282
+ if (!it) return;
283
+ const initiative = it.initiative;
284
+ const ctx = {
285
+ ws: P.ws, slug: initiative, view: () => sheetView(P, initiative),
286
+ move: (i, to) => A.moveTo(i, to), setFields: (i, f) => A.setFields(i, f), flag: (i, b, r) => A.flag(i, b, r),
287
+ withdraw: (i) => A.withdraw(i), sessions: () => state.sessions || [],
288
+ attachEvidence: (i, evidence) => write(P, [{ ev: 'moved', item: i, to: 'done', evidence }], (raw) => {
289
+ const x = raw.items[i]; const k = String(evidence).indexOf(':');
290
+ if (x && k > 0) { x.evidence = { kind: String(evidence).slice(0, k), ref: String(evidence).slice(k + 1) }; x.noEvidence = false; }
291
+ }),
292
+ keep: (b) => A.keepBatch(initiative, b),
293
+ onClose: (i) => {
294
+ P.sheet = null;
295
+ const t = P.root.querySelector('[data-fk="card:' + i + '"]');
296
+ if (t && t.isConnected) t.focus({ preventScroll: true });
297
+ },
298
+ };
299
+ P.sheet = openItemSheet(ctx, id, opts);
300
+ },
301
+ menu(anchor, id) {
302
+ const lay = L();
303
+ const it = lay && lay.items[id];
304
+ if (!it) return;
305
+ const items = [];
306
+ for (const s of ['todo', 'doing', 'review', 'done']) if (s !== it.state) items.push({ label: BD.menu.moveTo(STATE_LABEL(s)), onSelect: () => A.moveTo(id, s) });
307
+ items.push({ sep: true });
308
+ items.push({ label: BD.handToAgent, icon: 'bot', onSelect: () => A.openSheet(id, { handoff: true }) });
309
+ items.push({ label: BD.labels.withdraw, danger: true, onSelect: () => A.withdraw(id) });
310
+ menu({ anchor, items, ariaLabel: BD.aria.more(it.title) });
311
+ },
312
+ openAdd() { openAdd(P); },
313
+ };
314
+ return A;
315
+ }
316
+
317
+ /** + Add item: a sheet with a title, an initiative and a stage. The item is planned in that initiative's ledger. */
318
+ function openAdd(P) {
319
+ const inits = (P.out && P.out.initiatives) || [];
320
+ if (!inits.length) { toast({ text: COPY.adding.needInitiative, tone: 'error' }); return; }
321
+ const stagesOf = (id) => ((inits.find((i) => i.id === id) || {}).stages || []);
322
+ const pickStage = (id) => { const ss = stagesOf(id); return (ss.find((s) => s.id === 'build') || ss[0] || {}).id || ''; };
323
+ const first = P.filter === 'init' && inits.some((i) => i.id === P.init) ? P.init : inits[0].id;
324
+ const title = field({ label: COPY.adding.fieldTitle, kind: 'text', attrs: { 'data-bo-input': 'title', maxlength: '200', spellcheck: 'true' } });
325
+ const stageSel = field({
326
+ label: COPY.adding.fieldStage, kind: 'select', value: pickStage(first),
327
+ options: stagesOf(first).map((s) => ({ value: s.id, label: s.label })), attrs: { 'data-bo-input': 'stage' },
328
+ });
329
+ const initSel = field({
330
+ label: COPY.adding.fieldInitiative, kind: 'select', value: first, options: inits.map((i) => ({ value: i.id, label: i.title })),
331
+ attrs: { 'data-bo-input': 'initiative' },
332
+ onCommit: (v) => {
333
+ const sel = stageSel.control;
334
+ sel.replaceChildren(...stagesOf(v).map((s) => el('option', { value: s.id }, s.label)));
335
+ sel.value = pickStage(v);
336
+ },
337
+ });
338
+ let busy = false;
339
+ const submit = async () => {
340
+ if (busy) return;
341
+ const t = title.control.value.trim();
342
+ if (!t) { toast({ text: COPY.adding.needTitle, tone: 'error' }); title.control.focus(); return; }
343
+ busy = true;
344
+ const out = await postEvents(P.ws, [{ ev: 'planned', initiative: initSel.control.value, title: t, stage: stageSel.control.value }]);
345
+ busy = false;
346
+ if (!out || out.ok === false) { toast({ text: (out && out.error) || 'nothing was saved', tone: 'error' }); return; }
347
+ sh.close();
348
+ toast({ text: COPY.added });
349
+ load(P);
350
+ };
351
+ title.control.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } });
352
+ const sh = sheet({
353
+ mode: 'auto', title: COPY.adding.title, sub: COPY.adding.sub, attrs: { 'data-bo-sheet': 'add' },
354
+ body: [title, el('div', { class: 'bd-grid2' }, initSel, stageSel)],
355
+ actions: [
356
+ button({ label: COPY.adding.cancel, variant: 'outline', onClick: () => sh.close(), attrs: { 'data-bo-cancel': '' } }),
357
+ button({ label: COPY.adding.add, variant: 'primary', onClick: submit, attrs: { 'data-bo-confirm': '' } }),
358
+ ],
359
+ onClose: () => { const b = P.root.querySelector('[data-fk="add"]'); if (b && b.isConnected) b.focus({ preventScroll: true }); },
360
+ });
361
+ title.control.focus();
362
+ }
363
+
364
+ /* ── mount ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
365
+
366
+ function dispose(P) {
367
+ P.disposed = true;
368
+ P.drags.destroy();
369
+ if (P.unwatch) P.unwatch();
370
+ if (P.stopSig) P.stopSig();
371
+ if (P.onLedgers) window.removeEventListener('pmos:ledgers', P.onLedgers);
372
+ if (P.resize) P.resize.disconnect();
373
+ if (P.sheet && P.sheet.open) P.sheet.close();
374
+ live.delete(P);
375
+ }
376
+
377
+ function createPane(host, ws) {
378
+ const headEl = el('div', { class: 'bd-head bo-head' });
379
+ const bodyEl = el('div', { class: 'bd-body', 'data-bd-body': '' });
380
+ const root = el('div', { class: 'bd-pane bo-pane', 'data-bo-pane': '', 'data-ws': ws }, headEl, bodyEl);
381
+ const P = {
382
+ host, ws, root, headEl, bodyEl, slug: 'board', filter: 'active', init: null, out: null, raw: null, older: 0, error: null, loaded: false,
383
+ loading: false, again: false, boardCol: 'todo', ver: 0, renderedVer: -1, drags: dragSet(), dragging: false, rendering: false,
384
+ disposed: false, sheet: null, wide: false, cardMenus: true, lastWidth: 0,
385
+ coarse: typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches,
386
+ };
387
+ P.actions = makeActions(P);
388
+ P.unwatch = watchLedgers(ws);
389
+ P.onLedgers = (e) => {
390
+ const d = e && e.detail;
391
+ if (d && (d.changed || []).includes(ws)) load(P);
392
+ };
393
+ window.addEventListener('pmos:ledgers', P.onLedgers);
394
+ // the signature of GET /api/board (the placements and the factory join, which the ledger poll does not cover)
395
+ P.stopSig = watchSig(async () => {
396
+ const j = await pmGet('/api/board?ws=' + enc(ws) + '&only=sig');
397
+ return j && j.ok !== false && typeof j.sig === 'string' ? j.sig : null;
398
+ }, () => load(P));
399
+ if (typeof ResizeObserver === 'function') {
400
+ P.resize = new ResizeObserver((entries) => {
401
+ const w = entries[0] && entries[0].contentRect.width;
402
+ if (!w) return;
403
+ P.lastWidth = w;
404
+ if ((w >= WIDE_BOARD) !== P.wide) bump(P);
405
+ });
406
+ P.resize.observe(root);
407
+ }
408
+ return P;
409
+ }
410
+
411
+ export function mountDestination(host, ctx) {
412
+ if (FIXTURE) return;
413
+ const ws = wsOf((ctx && ctx.tab && ctx.tab.repo) || currentProduct());
414
+ let P = panes.get(host);
415
+ if (P && (P.disposed || P.ws !== ws)) { dispose(P); panes.delete(host); P = null; }
416
+ if (!ws) { ensureComponentsCss(); host.replaceChildren(empty({ text: COPY.noWorkspace, attrs: { 'data-bo-no-ws': '' } })); return; }
417
+ if (!P) {
418
+ ensureComponentsCss(); layer(); ensureSprite();
419
+ P = createPane(host, ws);
420
+ panes.set(host, P); live.add(P);
421
+ setChildren(host, [P.root]);
422
+ render(P);
423
+ load(P);
424
+ return;
425
+ }
426
+ if (host.firstChild !== P.root || host.childNodes.length !== 1) setChildren(host, [P.root]);
427
+ if (P.renderedVer !== P.ver && !P.dragging) render(P);
428
+ }
429
+
430
+ export function disposeDestination(host) {
431
+ const P = panes.get(host);
432
+ if (!P) return;
433
+ panes.delete(host);
434
+ if (!P.disposed) dispose(P);
435
+ }
436
+
437
+ /* for the browser suites: the pane of a host, read-only */
438
+ export function _paneOf(host) { return panes.get(host) || null; }
@@ -0,0 +1,134 @@
1
+ /* pm/dest-initiatives.js — the Initiatives destination (pm-os-absorb Run 2, slice 2D; PRD R9, R15, R17, R20).
2
+ *
3
+ * The shell of the place: a header with the View control (Roadmap, List, and any view another run registered, such as
4
+ * Timeline) and the New initiative button, and under it the view that is chosen. Roadmap and List are built here; a
5
+ * registered view is mounted with initiativesViewCtx(…), the one way to build its ctx, and appears only when
6
+ * initiativesViews() lists it.
7
+ *
8
+ * This module imports the others so that their registrations run when it loads at boot: the initiative sheet (it becomes
9
+ * the opener of every initiative), the Cycle review card and the triage card (Today cards), and the New-initiative form.
10
+ *
11
+ * mountDestination(host, ctx) runs on every render. It keeps ONE root per host and rebuilds a view only when the
12
+ * repository, the workspace or the chosen view changed.
13
+ */
14
+
15
+ import { el } from './dom.js';
16
+ import { button, tabs, empty, ensureComponentsCss } from './primitives.js';
17
+ import { wsOf } from './data.js';
18
+ import { setInitiativeOpener, registerCommand, initiativesViews, initiativesViewCtx, openDestination } from './registry.js';
19
+ import { COPY } from './initiatives-copy.js';
20
+ import { getRoadmap, grantScopes } from './initiatives-api.js';
21
+ import { mountRoadmap } from './roadmap.js';
22
+ import { mountList } from './initiative-list.js';
23
+ import { openSheet } from './initiative-sheet.js';
24
+ import { openNewInitiative } from './new-initiative-sheet.js';
25
+ import './cycle-review.js';
26
+ import './triage-card.js';
27
+
28
+ setInitiativeOpener(openSheet);
29
+
30
+ registerCommand({ id: 'initiatives-open', label: 'Open Initiatives', hint: 'Roadmap and List', cli: 'pmos initiative list', run: () => openDestination('initiatives') });
31
+ registerCommand({
32
+ id: 'initiatives-new', label: COPY.newInitiative, hint: 'Add one to Later', cli: 'pmos initiative new "<title>" --anchor <kr>',
33
+ run: () => { openDestination('initiatives'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-new')); } catch { /* no window */ } },
34
+ });
35
+ registerCommand({ id: 'initiatives-list', label: 'Initiatives as a list', hint: 'By objective', cli: 'pmos initiative list', run: () => { try { window.localStorage.setItem('pmos.initiatives.view', 'list'); } catch { /* no storage */ } openDestination('initiatives'); try { window.dispatchEvent(new CustomEvent('pmos:initiatives-view', { detail: 'list' })); } catch { /* no window */ } } });
36
+
37
+ const BUILT_IN = ['roadmap', 'list'];
38
+ const roots = new WeakMap(); // host -> its persistent root and what it shows
39
+
40
+ function savedView() { try { return window.localStorage.getItem('pmos.initiatives.view'); } catch { return null; } }
41
+ function saveView(v) { try { window.localStorage.setItem('pmos.initiatives.view', v); } catch { /* no storage */ } }
42
+
43
+ const flat = (answer) => {
44
+ if (!answer || !answer.horizons) return [];
45
+ return [...(answer.horizons.now || []), ...(answer.horizons.next || []), ...(answer.horizons.later || []), ...(answer.dormant || [])];
46
+ };
47
+
48
+ export function mountDestination(host, ctx) {
49
+ ensureComponentsCss();
50
+ const state = ctx.state;
51
+ const repo = (ctx.tab && ctx.tab.repo) || state.product || null;
52
+ const ws = (repo && wsOf(repo)) || state.wsId || null;
53
+ let r = roots.get(host);
54
+ if (!r) {
55
+ r = { root: el('div', { class: 'in-dest', 'data-in-dest': '' }), view: savedView() || 'roadmap', key: null, handle: null, extra: null, rows: [], sig: null, grants: [] };
56
+ roots.set(host, r);
57
+ host.replaceChildren(r.root);
58
+ const reloadView = () => { if (r.handle && r.handle.reload) r.handle.reload(); refreshRows(r, ws); };
59
+ r.onChanged = reloadView;
60
+ // The three listeners are kept on r so disposeDestination can take them off: a closed tab must not keep its host alive.
61
+ r.listeners = [
62
+ ['pmos:initiatives-changed', () => { if (roots.get(host) === r) r.onChanged(); }],
63
+ ['pmos:initiatives-new', () => { if (roots.get(host) === r) r.openNew(); }],
64
+ ['pmos:initiatives-view', (e) => { if (roots.get(host) === r && e.detail) { r.view = e.detail; r.draw(r.ctx); } }],
65
+ ];
66
+ for (const [name, fn] of r.listeners) window.addEventListener(name, fn);
67
+ }
68
+ r.ctx = ctx;
69
+ r.openNew = () => openNewInitiative({ repo, ws, anchors: r.rows.map((x) => x.anchor), onAdded: () => r.onChanged() });
70
+ r.draw = (c) => draw(host, r, c, repo, ws);
71
+ if (r.key === keyOf(r, repo, ws)) return; // nothing changed that this place draws: keep the live view (a drag may be running in it)
72
+ r.draw(ctx);
73
+ }
74
+
75
+ const keyOf = (r, repo, ws) => [repo, ws, r.view, initiativesViews().map((v) => v.id).join(',')].join('|');
76
+
77
+ function refreshRows(r, ws) {
78
+ if (!ws) return;
79
+ getRoadmap(ws).then((a) => { if (a && a.ok) { r.rows = flat(a); r.sig = a.sig || null; } });
80
+ }
81
+
82
+ function draw(host, r, ctx, repo, ws) {
83
+ if (r.handle && r.handle.dispose) r.handle.dispose();
84
+ if (r.extra && r.extra.dispose) { try { r.extra.dispose(r.body); } catch { /* the view is gone */ } }
85
+ r.handle = null; r.extra = null;
86
+
87
+ const views = [
88
+ { id: 'roadmap', label: COPY.roadmap },
89
+ { id: 'list', label: COPY.list },
90
+ ...initiativesViews().filter((v) => !BUILT_IN.includes(v.id)).map((v) => ({ id: v.id, label: v.label || v.id })),
91
+ ];
92
+ if (!views.some((v) => v.id === r.view)) r.view = 'roadmap';
93
+ r.key = keyOf(r, repo, ws);
94
+
95
+ const head = el('div', { class: 'in-head' },
96
+ el('h2', { class: 'in-head__title' }, COPY.title),
97
+ tabs({ variant: 'segmented', role: 'tablist', ariaLabel: COPY.views, items: views, value: r.view, attrs: { 'data-in-views': '' }, onChange: (id) => { r.view = id; saveView(id); r.draw(r.ctx); } }),
98
+ button({ label: COPY.newInitiative, icon: 'plus', variant: 'primary', attrs: { 'data-in-new': '' }, onClick: () => r.openNew() }));
99
+ r.body = el('div', { class: 'in-body', 'data-in-body': r.view });
100
+ r.root.replaceChildren(head, r.body);
101
+
102
+ if (!ws) { r.body.replaceChildren(empty({ text: COPY.noRepo, attrs: { 'data-in-empty': 'repo' } })); return; }
103
+
104
+ const open = (rp, slug) => ctx.openInitiative(rp || repo, slug);
105
+ if (r.view === 'roadmap') {
106
+ r.handle = mountRoadmap(r.body, { repo, ws, openInitiative: open, onData: (a) => { r.rows = flat(a); r.sig = a.sig || null; } });
107
+ } else if (r.view === 'list') {
108
+ r.handle = mountList(r.body, { repo, ws, openInitiative: open, grants: () => r.grants });
109
+ refreshRows(r, ws);
110
+ grantScopes().then((g) => { r.grants = g; if (r.handle && r.handle.reload) r.handle.reload(); });
111
+ } else {
112
+ const v = initiativesViews().find((x) => x.id === r.view);
113
+ if (v) {
114
+ refreshRows(r, ws);
115
+ getRoadmap(ws).then((a) => {
116
+ if (r.view !== v.id) return;
117
+ const rows = a && a.ok ? flat(a) : [];
118
+ r.rows = rows;
119
+ v.mount(r.body, initiativesViewCtx({ repo, ws, rows, sig: a && a.sig ? a.sig : null, openInitiative: open, setState: ctx.setState }));
120
+ });
121
+ r.extra = { dispose: (b) => { if (typeof v.dispose === 'function') v.dispose(b); } };
122
+ }
123
+ }
124
+ }
125
+
126
+ export function disposeDestination(host) {
127
+ const r = roots.get(host);
128
+ if (!r) return;
129
+ if (r.handle && r.handle.dispose) r.handle.dispose();
130
+ if (r.extra && r.extra.dispose) { try { r.extra.dispose(r.body); } catch { /* gone */ } }
131
+ for (const [name, fn] of r.listeners || []) window.removeEventListener(name, fn);
132
+ r.listeners = null;
133
+ roots.delete(host);
134
+ }