create-pmos 0.52.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) 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/README.md +2 -0
  16. package/workspace/REMOTE.md +24 -63
  17. package/workspace/bin/pmos.js +19 -3
  18. package/workspace/cut-stub.js +40 -0
  19. package/workspace/first-paint-audit.js +145 -0
  20. package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
  21. package/workspace/fixtures/initiatives-api/list.json +176 -0
  22. package/workspace/fixtures/initiatives-api/post-403.json +5 -0
  23. package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
  24. package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
  25. package/workspace/fixtures/initiatives-api/sheet.json +112 -0
  26. package/workspace/fixtures/report-api/draft.json +177 -0
  27. package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
  28. package/workspace/fixtures/today-api/provider-rows.json +204 -0
  29. package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
  30. package/workspace/guide/board.md +81 -0
  31. package/workspace/guide/breakdown.md +154 -0
  32. package/workspace/guide/command-palette.md +75 -0
  33. package/workspace/guide/index.md +28 -17
  34. package/workspace/guide/initiatives-roadmap.md +130 -0
  35. package/workspace/guide/initiatives-screen.md +89 -0
  36. package/workspace/guide/initiatives-timeline.md +57 -0
  37. package/workspace/guide/integrated-work.md +94 -0
  38. package/workspace/guide/knowledge-graph.md +3 -68
  39. package/workspace/guide/launcher.md +51 -0
  40. package/workspace/guide/layout.md +102 -32
  41. package/workspace/guide/live-notes.md +3 -3
  42. package/workspace/guide/pm-only-acts.md +89 -0
  43. package/workspace/guide/pm-only-lines.md +61 -0
  44. package/workspace/guide/proactive-engine.md +21 -5
  45. package/workspace/guide/reports-page.md +80 -0
  46. package/workspace/guide/reports.md +73 -0
  47. package/workspace/guide/timeline.md +70 -0
  48. package/workspace/guide/today.md +85 -0
  49. package/workspace/guide/validate-with-typesafe.md +1 -1
  50. package/workspace/lib/actor.js +91 -0
  51. package/workspace/lib/board-cache.js +32 -2
  52. package/workspace/lib/board-cli.js +108 -0
  53. package/workspace/lib/board-fold.js +232 -0
  54. package/workspace/lib/breakdown-export.js +75 -0
  55. package/workspace/lib/brief.js +20 -2
  56. package/workspace/lib/capture.js +98 -0
  57. package/workspace/lib/engine/plan-items-stage.js +271 -0
  58. package/workspace/lib/engine/workflow-state.js +28 -2
  59. package/workspace/lib/factory-join.js +158 -0
  60. package/workspace/lib/goals.js +165 -0
  61. package/workspace/lib/grant-mint.js +113 -0
  62. package/workspace/lib/health-rules-factory.js +92 -0
  63. package/workspace/lib/health-rules-plan.js +164 -0
  64. package/workspace/lib/health.js +79 -0
  65. package/workspace/lib/initiative-cli.js +222 -0
  66. package/workspace/lib/initiative-display.js +183 -0
  67. package/workspace/lib/initiative-needs.js +127 -0
  68. package/workspace/lib/initiative-rows.js +210 -0
  69. package/workspace/lib/initiative-writer.js +141 -0
  70. package/workspace/lib/issues.js +206 -0
  71. package/workspace/lib/item-cli.js +195 -0
  72. package/workspace/lib/item-spans.js +137 -0
  73. package/workspace/lib/launcher-cli.js +207 -0
  74. package/workspace/lib/measure-cli.js +100 -0
  75. package/workspace/lib/measurements.js +189 -0
  76. package/workspace/lib/mobile-nav.js +57 -53
  77. package/workspace/lib/needs-providers.js +120 -0
  78. package/workspace/lib/placements.js +440 -0
  79. package/workspace/lib/plan-items.js +922 -0
  80. package/workspace/lib/plan-ledgers.js +85 -0
  81. package/workspace/lib/plan-rules.js +72 -0
  82. package/workspace/lib/plan-tree.js +380 -0
  83. package/workspace/lib/pm-guard.js +174 -0
  84. package/workspace/lib/pm-lines-needs.js +331 -0
  85. package/workspace/lib/pmos-verbs.js +69 -0
  86. package/workspace/lib/proactive-cli.js +58 -2
  87. package/workspace/lib/proactive.js +9 -6
  88. package/workspace/lib/qr.js +293 -0
  89. package/workspace/lib/remote-access.js +93 -1
  90. package/workspace/lib/report-cli.js +152 -0
  91. package/workspace/lib/report-draft.js +578 -0
  92. package/workspace/lib/report-needs.js +213 -0
  93. package/workspace/lib/route-modules.js +128 -0
  94. package/workspace/lib/routes-board.js +91 -0
  95. package/workspace/lib/routes-capture.js +52 -0
  96. package/workspace/lib/routes-initiatives.js +371 -0
  97. package/workspace/lib/routes-reports.js +163 -0
  98. package/workspace/lib/routes-timeline.js +197 -0
  99. package/workspace/lib/routes-today.js +32 -0
  100. package/workspace/lib/today-cli.js +99 -0
  101. package/workspace/lib/today.js +333 -0
  102. package/workspace/lib/triage.js +163 -0
  103. package/workspace/lib/verify-work-cli.js +81 -0
  104. package/workspace/lib/work-check-needs.js +99 -0
  105. package/workspace/package-lock.json +1 -1
  106. package/workspace/package.json +4 -5
  107. package/workspace/public/icons/apple-touch-icon.png +0 -0
  108. package/workspace/public/icons/icon-192.png +0 -0
  109. package/workspace/public/icons/icon-512.png +0 -0
  110. package/workspace/public/index.html +40 -19
  111. package/workspace/public/pm/app.js +330 -30
  112. package/workspace/public/pm/bd-board.js +113 -0
  113. package/workspace/public/pm/bd-drag.js +59 -0
  114. package/workspace/public/pm/bd-model.js +152 -0
  115. package/workspace/public/pm/bd-timeline.js +233 -0
  116. package/workspace/public/pm/bd-tree.js +244 -0
  117. package/workspace/public/pm/board-copy.js +57 -0
  118. package/workspace/public/pm/breakdown-api.js +58 -0
  119. package/workspace/public/pm/breakdown-copy.js +138 -0
  120. package/workspace/public/pm/components.css +3135 -0
  121. package/workspace/public/pm/cycle-review.js +125 -0
  122. package/workspace/public/pm/data.js +40 -187
  123. package/workspace/public/pm/dest-board.js +438 -0
  124. package/workspace/public/pm/dest-initiatives.js +134 -0
  125. package/workspace/public/pm/dest-reports.js +270 -0
  126. package/workspace/public/pm/dest-settings.js +217 -0
  127. package/workspace/public/pm/dest-today.js +182 -0
  128. package/workspace/public/pm/dialogs.js +88 -0
  129. package/workspace/public/pm/display-word.js +56 -0
  130. package/workspace/public/pm/dock.js +175 -0
  131. package/workspace/public/pm/fixture.js +8 -227
  132. package/workspace/public/pm/frame-copy.js +99 -0
  133. package/workspace/public/pm/handoff-box.js +82 -0
  134. package/workspace/public/pm/http.js +88 -0
  135. package/workspace/public/pm/icons.js +53 -0
  136. package/workspace/public/pm/initiative-list.js +218 -0
  137. package/workspace/public/pm/initiative-sheet.js +261 -0
  138. package/workspace/public/pm/initiatives-api.js +59 -0
  139. package/workspace/public/pm/initiatives-copy.js +215 -0
  140. package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
  141. package/workspace/public/pm/initiatives-timeline.js +410 -0
  142. package/workspace/public/pm/initiatives-ui.js +123 -0
  143. package/workspace/public/pm/item-sheet.js +251 -0
  144. package/workspace/public/pm/keep-card.js +204 -0
  145. package/workspace/public/pm/lifecycle.js +7 -1
  146. package/workspace/public/pm/mobile.css +49 -20
  147. package/workspace/public/pm/mobile.js +157 -29
  148. package/workspace/public/pm/modes-live.js +7 -883
  149. package/workspace/public/pm/modes.js +32 -1024
  150. package/workspace/public/pm/new-initiative-sheet.js +74 -0
  151. package/workspace/public/pm/next-step-ui.js +3 -2
  152. package/workspace/public/pm/palette-copy.js +51 -0
  153. package/workspace/public/pm/palette.js +312 -0
  154. package/workspace/public/pm/panel-breakdown.js +702 -0
  155. package/workspace/public/pm/panel-cockpit.js +63 -295
  156. package/workspace/public/pm/panel-detail.js +25 -172
  157. package/workspace/public/pm/panel-initiatives.js +1 -19
  158. package/workspace/public/pm/panel-repos.js +31 -81
  159. package/workspace/public/pm/panel-tabs.js +200 -111
  160. package/workspace/public/pm/panel-trees.js +5 -2
  161. package/workspace/public/pm/portfolio-report.js +61 -0
  162. package/workspace/public/pm/primitives.js +656 -0
  163. package/workspace/public/pm/print.css +33 -0
  164. package/workspace/public/pm/provider-cards.js +136 -0
  165. package/workspace/public/pm/registry.js +194 -0
  166. package/workspace/public/pm/release-card.js +110 -0
  167. package/workspace/public/pm/report-view.js +309 -0
  168. package/workspace/public/pm/reports-copy.js +114 -0
  169. package/workspace/public/pm/roadmap.js +271 -0
  170. package/workspace/public/pm/sig-watch.js +41 -0
  171. package/workspace/public/pm/state.js +8 -4
  172. package/workspace/public/pm/status-banner.js +13 -5
  173. package/workspace/public/pm/tab-doc.js +2 -4
  174. package/workspace/public/pm/tab-term.js +12 -2
  175. package/workspace/public/pm/tab-web.js +3 -2
  176. package/workspace/public/pm/timeline-copy.js +48 -0
  177. package/workspace/public/pm/today-cards.js +244 -0
  178. package/workspace/public/pm/today-copy.js +126 -0
  179. package/workspace/public/pm/today-goals.js +172 -0
  180. package/workspace/public/pm/tokens.css +16 -18
  181. package/workspace/public/pm/topbar.js +102 -0
  182. package/workspace/public/pm/triage-card.js +60 -0
  183. package/workspace/public/pm/undo-hold.js +103 -0
  184. package/workspace/public/pm/view-addrepo.js +2 -1
  185. package/workspace/public/pm/view-livenotes.js +3 -1
  186. package/workspace/public/pm/view-today.js +7 -162
  187. package/workspace/public/vendor/Sortable.min.js +2 -0
  188. package/workspace/public/vendor/lucide-sprite.js +79 -0
  189. package/workspace/replaced-tests.json +2877 -0
  190. package/workspace/run-tests.js +56 -5
  191. package/workspace/server.js +446 -40
  192. package/workspace/sheet-answer.js +61 -0
  193. package/workspace/skills/measure.skill +41 -0
  194. package/workspace/skills/morning-brief.skill +15 -9
  195. package/workspace/skills/report-biweekly.skill +23 -25
  196. package/workspace/skills/tracker-refresh.skill +50 -0
  197. package/workspace/test-files.json +21 -3
  198. package/workspace/test-lists/base.json +6 -0
  199. package/workspace/test-lists/run2-a.json +6 -0
  200. package/workspace/test-lists/run2-b.json +13 -0
  201. package/workspace/test-lists/run2-c.json +19 -0
  202. package/workspace/test-lists/run2-d.json +22 -0
  203. package/workspace/test-lists/run3-0.json +5 -0
  204. package/workspace/test-lists/run3-a.json +5 -0
  205. package/workspace/test-lists/run3-b.json +6 -0
  206. package/workspace/test-lists/run3-c.json +15 -0
  207. package/workspace/test-lists/run3-d.json +6 -0
  208. package/workspace/test-lists/run4-a.json +8 -0
  209. package/workspace/test-lists/run4-b.json +6 -0
  210. package/workspace/test-lists/run4-c.json +13 -0
  211. package/workspace/test-lists/run4-d.json +9 -0
  212. package/workspace/test-lists/run5-a.json +6 -0
  213. package/workspace/test-lists/run5-b.json +6 -0
  214. package/workspace/test-lists/run5-c.json +15 -0
  215. package/workspace/test-lists/workspace-launcher.json +5 -0
  216. package/workspace/test-lists/workspace-phone-app.json +5 -0
  217. package/workspace/test-run.js +27 -1
  218. package/workspace/third-party/README.md +44 -0
  219. package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
  220. package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
  221. package/workspace/design-parity.js +0 -673
  222. package/workspace/public/pm/tab-task.js +0 -86
  223. package/workspace/public/pm/view-guide.js +0 -141
  224. package/workspace/public/pm/view-knowledge.js +0 -225
  225. package/workspace/public/pm/view-memory.js +0 -122
  226. package/workspace/public/pm/view-portfolio.js +0 -153
  227. package/workspace/public/pm/view-reports.js +0 -190
  228. package/workspace/public/pm/view-scheduled.js +0 -161
  229. package/workspace/public/pm/view-skills.js +0 -216
@@ -0,0 +1,182 @@
1
+ /* pm/dest-today.js — the Today destination (pm-os-absorb Run 3, slice 3B, PRD R12, F8).
2
+ *
3
+ * Today lists what waits for the PM, as cards of eight kinds, ranked. The cards come from the registry's Today sources,
4
+ * the same way for every run: todayCards() draws them and todayCount() counts them, so this header and the nav badge add
5
+ * the same sources and cannot disagree. This module also registers the source for the cards the server makes itself
6
+ * (kind 1 acceptance and outcome, kind 2 a waiting session, kind 3 a returned step): `today-core`.
7
+ *
8
+ * mountDestination(host, ctx) runs on every render with the same host. It keeps one root in the host and replaces the root's
9
+ * children only when what it shows changed: the cards' own markup, the scope, the count, the notice, the fold. A card the
10
+ * PM is typing in is rebuilt from the card's own saved state (today-cards.js), and the focus returns to the same control.
11
+ *
12
+ * The legacy Today view (view-today.js) calls mountToday on its own host, so both surfaces draw the one list.
13
+ */
14
+
15
+ import { el } from './dom.js';
16
+ import { empty, tabs, button, toast, ensureComponentsCss } from './primitives.js';
17
+ import { registerTodaySource, todayCards, todayCount, openInitiative } from './registry.js';
18
+ import { repoTuples } from './panel-repos.js';
19
+ import { currentProduct } from './board-read.js';
20
+ import { wsOf } from './data.js';
21
+ import { state, setState, FIXTURE } from './state.js';
22
+ import { COPY } from './today-copy.js';
23
+ import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
24
+ import { goalsSection, goalsSig } from './today-goals.js';
25
+
26
+ const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
27
+ let expanded = false;
28
+
29
+ /* ── the source for the cards the server makes itself ─────────────────────────────────────── */
30
+
31
+ function coreRows(ctx) {
32
+ const out = [];
33
+ for (const repo of (ctx && ctx.repos) || []) {
34
+ const ws = wsOf(repo);
35
+ if (!ws) continue;
36
+ ensureFeed(ws);
37
+ const d = feedOf(ws);
38
+ for (const c of (d && d.cards) || []) if (c && c.source === 'today') out.push({ repo, c });
39
+ }
40
+ return out;
41
+ }
42
+
43
+ async function openSession(c) {
44
+ const id = String(c.run_id || '').replace(/^session:/, '');
45
+ const s = (state.sessions || []).find((x) => x && x.id === id);
46
+ if (!s) { toast({ text: COPY.sessionGone }); return; }
47
+ try { const m = await import('./handoff-box.js'); await m.openSessionTab(c.initiative_id || null, s); } catch (e) { console.error('[pm] could not open the session:', e); }
48
+ }
49
+
50
+ function drawCore({ repo, c }, ctx) {
51
+ const rerender = () => (ctx && ctx.setState ? ctx.setState({}) : setState({}));
52
+ if (c.row_kind === 'accept') return acceptCard(c, { repo, rerender });
53
+ const kind = Number(c.kind);
54
+ return todayCard({
55
+ kind, id: c.id, repo, ws: c.ws, initiative: c.initiative_id, cause: c.cause, evidence: c.evidence, act: c.act,
56
+ attrs: { 'data-today-row': c.row_kind || null },
57
+ onAct: () => {
58
+ if (c.row_kind === 'session') { openSession(c); return; }
59
+ if (c.initiative_id) openInitiative(repo, c.initiative_id); else toast({ text: COPY.noInitiative });
60
+ },
61
+ });
62
+ }
63
+
64
+ if (!FIXTURE) {
65
+ registerTodaySource({
66
+ id: 'today-core', kind: null, rank: 10,
67
+ count: (ctx) => coreRows(ctx).length,
68
+ cards: (ctx) => coreRows(ctx).map((x) => drawCore(x, ctx)),
69
+ });
70
+ }
71
+
72
+ /* ── the board ─────────────────────────────────────────────────────────────────────────────── */
73
+
74
+ const roots = new WeakMap(); // host -> { root, sig, off }
75
+
76
+ function scopeToggle(all) {
77
+ const box = tabs({
78
+ variant: 'segmented', role: 'radiogroup', ariaLabel: COPY.scope.label, value: all ? 'all' : 'repo',
79
+ items: [{ id: 'repo', label: COPY.scope.repo }, { id: 'all', label: COPY.scope.all }],
80
+ onChange: (id) => setState({ todayScope: id === 'all' ? 'all' : 'repo' }),
81
+ attrs: { 'data-today': 'scope' },
82
+ });
83
+ const btns = box.querySelectorAll('button');
84
+ ['repo', 'all'].forEach((id, i) => { if (btns[i]) btns[i].setAttribute('data-today-scope', id); });
85
+ return box;
86
+ }
87
+
88
+ function countText(ctx, n, ready) {
89
+ if (!ready && n === 0) return COPY.reading;
90
+ return ctx.all ? COPY.countAcross(n, ctx.repos.length) : COPY.countIn(n, ctx.repo);
91
+ }
92
+
93
+ function nextLabel(repos) {
94
+ for (const repo of repos) { const d = feedOf(wsOf(repo)); if (d && d.next && d.next.label) return d.next.label; }
95
+ return null;
96
+ }
97
+
98
+ function build(ctx0) {
99
+ const st = ctx0.state || state;
100
+ const sc = scopeCtx(st, repoTuples(), currentProduct());
101
+ const regCtx = { all: sc.all, repos: sc.repos, setState: ctx0.setState || setState, open: (r, slug) => openInitiative(r, slug) };
102
+ const n = todayCount(regCtx);
103
+ const nodes = rankNodes(todayCards(regCtx));
104
+ const ready = sc.repos.length > 0 && sc.repos.every((r) => { const ws = wsOf(r); return !ws || feedOf(ws) !== null || feedErrorOf(ws) !== null; });
105
+ const banner = bannerOf(sc.repos);
106
+ nodes.forEach((node, i) => { if (!expanded && i >= SHOWN) node.hidden = true; });
107
+
108
+ const head = el('div', { class: 'td-head' },
109
+ el('h2', { class: 'td-count', 'data-today': 'count', 'aria-live': 'polite' }, countText(sc, n, ready)),
110
+ scopeToggle(sc.all));
111
+ const health = banner.length ? el('div', { class: 'td-health', 'data-today': 'health', role: 'status' },
112
+ el('p', { class: 'td-health__title' }, COPY.health.title(banner.length)),
113
+ el('ul', { class: 'td-health__list' }, ...banner.map((b) => el('li', null, COPY.health.line(sc.all ? b.repo + ' · ' + b.source : b.source, b.error))))) : null;
114
+ const list = nodes.length
115
+ ? el('div', { class: 'td-list', 'data-today': 'needs', role: 'list', 'aria-label': COPY.aria.list }, ...nodes)
116
+ : el('div', { class: 'td-list', 'data-today': 'needs' }, ready && !banner.some((b) => b.source === COPY.health.todaySource) ? empty({ text: COPY.empty(nextLabel(sc.repos)), attrs: { 'data-today': 'empty' } }) : null);
117
+ const more = nodes.length > SHOWN
118
+ ? button({ label: expanded ? COPY.fewer : COPY.more(nodes.length - SHOWN), variant: 'ghost', attrs: { 'data-today': 'more' }, onClick: () => { expanded = !expanded; setState({}); } })
119
+ : null;
120
+ nodes.forEach((node) => node.setAttribute('role', 'listitem'));
121
+ // today-goals (G6): the PM's goals sit above what is owed; they are joined to the cards of the first repository in scope
122
+ const goalsWs = sc.repos.length ? wsOf(sc.repos[0]) : null;
123
+ const goals = goalsSection({ ws: goalsWs, setState: ctx0.setState || setState });
124
+ return { children: [head, goals, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + goalsSig(goalsWs) + nodes.map((x) => x.outerHTML).join('') };
125
+ }
126
+
127
+ /** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
128
+ function focusKey(root) {
129
+ const a = document.activeElement;
130
+ if (!a || !root.contains(a)) return null;
131
+ const card = a.closest('[data-today-id]');
132
+ const attr = ['data-today-act', 'data-today-more', 'data-today-add-note', 'data-today-accept', 'data-today-return'].find((k) => a.hasAttribute(k));
133
+ if (card && attr) return { id: card.getAttribute('data-today-id'), attr };
134
+ if (a.closest('[data-today-note]')) return { id: card && card.getAttribute('data-today-id'), note: true };
135
+ if (a.getAttribute('data-goals') === 'input') return { goalsInput: true, caret: a.selectionStart };
136
+ if (a.hasAttribute('data-today-scope')) return { scope: a.getAttribute('data-today-scope') };
137
+ if (a.hasAttribute('data-today') && a.getAttribute('data-today') === 'more') return { more: true };
138
+ return null;
139
+ }
140
+ function refocus(root, k) {
141
+ if (!k) return;
142
+ const card = k.id ? [...root.querySelectorAll('[data-today-id]')].find((x) => x.getAttribute('data-today-id') === k.id) : null;
143
+ let t = null;
144
+ if (k.note && card) t = card.querySelector('[data-today-note] input, [data-today-note] textarea, input[data-today-note], textarea[data-today-note]');
145
+ else if (k.attr && card) t = card.querySelector('[' + k.attr + ']');
146
+ else if (k.goalsInput) t = root.querySelector('[data-goals="input"]');
147
+ else if (k.scope) t = root.querySelector('[data-today-scope="' + k.scope + '"]');
148
+ else if (k.more) t = root.querySelector('[data-today="more"]');
149
+ if (t && !t.hidden) { try { t.focus({ preventScroll: true }); if (k.goalsInput && typeof k.caret === 'number') t.setSelectionRange(k.caret, k.caret); } catch { /* not focusable */ } }
150
+ }
151
+
152
+ /** Draw Today into `host`. Safe to call on every render: nothing changes unless what Today shows changed. */
153
+ export function mountToday(host, ctx) {
154
+ if (!host) return;
155
+ ensureComponentsCss();
156
+ let r = roots.get(host);
157
+ if (!r) {
158
+ const root = el('section', { class: 'td-board', 'data-today': 'board', 'data-board': 'today', 'aria-label': COPY.title });
159
+ host.replaceChildren(root);
160
+ r = { root, sig: null, off: null };
161
+ roots.set(host, r);
162
+ // a host the old shell threw away stops listening: its next render makes a new one
163
+ r.off = onFeed(() => {
164
+ if (host.isConnected) (ctx && ctx.setState ? ctx.setState : setState)({});
165
+ else { r.off(); roots.delete(host); }
166
+ });
167
+ }
168
+ if (!r.root.isConnected) host.replaceChildren(r.root);
169
+ const out = build(ctx || {});
170
+ if (out.sig === r.sig) return;
171
+ const k = focusKey(r.root);
172
+ r.sig = out.sig;
173
+ r.root.replaceChildren(...out.children.filter(Boolean));
174
+ refocus(r.root, k);
175
+ }
176
+
177
+ export function mountDestination(host, ctx) { mountToday(host, ctx); }
178
+ export function disposeDestination(host) {
179
+ const r = roots.get(host);
180
+ if (r && r.off) r.off();
181
+ roots.delete(host);
182
+ }
@@ -0,0 +1,88 @@
1
+ /* pm/dialogs.js — the questions the workspace asks, as sheets (pm-os-absorb Run 3, slice 3D; PRD R27, R29).
2
+ *
3
+ * `prompt`, `confirm` and `alert` block the page, cannot be styled, cannot be reached by a test without a browser hook, and on
4
+ * a phone they take the whole screen. Every question the workspace used to ask with one is asked here with the `sheet` primitive
5
+ * instead: the same words, one answer.
6
+ *
7
+ * askConfirm({ title, body, confirmLabel, danger }) → Promise<boolean> true on the confirm press, false on anything else
8
+ * askText({ title, body, label, value, ... }) → Promise<string | null> the text, or null when the PM backed out
9
+ * askFields({ title, body, fields, confirmLabel }) → Promise<object | null> { key: text } for each field, or null
10
+ * askChoice({ title, body, options, value, ... }) → Promise<string | null> the chosen option's value, or null
11
+ *
12
+ * Escape, the backdrop and the close control all answer "no" (false or null). The sheet carries `data-dialog="<kind>"` so a
13
+ * suite can find it, and its confirm control carries `data-dialog-confirm`. There is no browser fallback: when the sheet cannot
14
+ * be drawn (no document) the question is answered "no", so a destructive action never runs unasked.
15
+ */
16
+
17
+ import { sheet, button, field, dialogText, ensureComponentsCss } from './primitives.js';
18
+
19
+ /** The paragraphs of a message: blank lines separate them, single newlines stay line breaks. */
20
+ function paragraphs(text) {
21
+ return String(text || '').split(/\n\s*\n/).filter((p) => p.trim()).map((p) => dialogText(p.trim()));
22
+ }
23
+
24
+ /**
25
+ * The one builder. `kind` names the sheet for suites; `controls` are the fields drawn above the buttons; `read()` returns the
26
+ * answer when the PM confirms. Resolves once, on the first close.
27
+ */
28
+ function ask({ kind, title, sub, body, controls, confirmLabel, cancelLabel, danger, read }) {
29
+ if (typeof document === 'undefined') return Promise.resolve(read ? null : false);
30
+ ensureComponentsCss();
31
+ return new Promise((resolve) => {
32
+ let settled = false;
33
+ let handle = null;
34
+ const finish = (answer) => { if (settled) return; settled = true; resolve(answer); };
35
+ const submit = () => { finish(read ? read() : true); if (handle) handle.close(); };
36
+ const confirmBtn = button({
37
+ label: confirmLabel || 'Confirm', variant: danger ? 'danger' : 'primary', onClick: submit,
38
+ attrs: { 'data-dialog-confirm': '' },
39
+ });
40
+ const cancelBtn = button({
41
+ label: cancelLabel || 'Cancel', variant: 'outline', onClick: () => { finish(read ? null : false); if (handle) handle.close(); },
42
+ attrs: { 'data-dialog-cancel': '' },
43
+ });
44
+ handle = sheet({
45
+ mode: 'auto', title, sub, body: [...paragraphs(body), ...(controls || [])], actions: [cancelBtn, confirmBtn],
46
+ attrs: { 'data-dialog': kind },
47
+ onClose: () => finish(read ? null : false),
48
+ });
49
+ const first = handle.dialog.querySelector('input, textarea, select');
50
+ if (first) { try { first.focus({ preventScroll: true }); if (first.select) first.select(); } catch { /* not focusable yet */ } }
51
+ else { try { confirmBtn.focus({ preventScroll: true }); } catch { /* not focusable yet */ } }
52
+ for (const c of handle.dialog.querySelectorAll('input')) {
53
+ c.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } });
54
+ }
55
+ });
56
+ }
57
+
58
+ export function askConfirm({ title, body, confirmLabel, cancelLabel, danger } = {}) {
59
+ return ask({ kind: 'confirm', title, body, confirmLabel, cancelLabel, danger });
60
+ }
61
+
62
+ export function askFields({ title, sub, body, fields, confirmLabel, cancelLabel, danger } = {}) {
63
+ const made = (fields || []).map((f) => ({
64
+ key: f.key,
65
+ node: field({ label: f.label, value: f.value || '', placeholder: f.placeholder, attrs: { 'data-dialog-field': f.key } }),
66
+ }));
67
+ return ask({
68
+ kind: 'fields', title, sub, body, confirmLabel, cancelLabel, danger,
69
+ controls: made.map((m) => m.node),
70
+ read: () => Object.fromEntries(made.map((m) => [m.key, m.node.control.value])),
71
+ });
72
+ }
73
+
74
+ export function askText({ title, sub, body, label, value, placeholder, confirmLabel, cancelLabel, danger } = {}) {
75
+ return askFields({
76
+ title, sub, body, confirmLabel, cancelLabel, danger,
77
+ fields: [{ key: 'text', label: label || title || 'Answer', value, placeholder }],
78
+ }).then((a) => (a ? a.text : null));
79
+ }
80
+
81
+ export function askChoice({ title, sub, body, label, options, value, confirmLabel, cancelLabel } = {}) {
82
+ const node = field({ label: label || title || 'Choice', kind: 'select', value, options: options || [], attrs: { 'data-dialog-field': 'choice' } });
83
+ return ask({
84
+ kind: 'choice', title, sub, body, confirmLabel, cancelLabel,
85
+ controls: [node],
86
+ read: () => node.control.value,
87
+ });
88
+ }
@@ -0,0 +1,56 @@
1
+ /* pm/display-word.js — the one word an initiative shows in the old shells (pm-os-absorb Run 3, slice 3D; PRD R3).
2
+ *
3
+ * Nobody types a status. The cockpit list, the breadcrumb and the status banner used to carry their own table of words (a
4
+ * `WORD` map in panel-cockpit.js, a `TONE` map in status-banner.js, a rung-to-word map). They now read the one mapping every
5
+ * surface reads, `displayOf` in lib/initiative-display.js (window.PMOSInitiativeDisplay), so a word cannot differ from one
6
+ * surface to the next:
7
+ *
8
+ * wordOf(statusRow, rec) → the word: the stage, or "Accepted" (Run 2 adds the rules: idle, waiting on, held, closed)
9
+ * stageOf(rec) → the stage the initiative is at (the first blocked step, else the current one), or null
10
+ * toneOf(statusRow) → 'd' (destructive), 'w' (warning) or null: how loudly the word is drawn
11
+ *
12
+ * `statusRow` is the derived `{ status, lifecycle }` row the server sends for the initiative (state.statuses[repo][slug]);
13
+ * `rec` is the initiative record the board reads (records()[slug]), whose `steps` are the workflow ladder. Both are read-only
14
+ * here: this file stores nothing and writes nothing. Pure over its two arguments and the display module.
15
+ */
16
+
17
+ import { targetStepIndex } from './fixture.js';
18
+
19
+ const display = () => (typeof window !== 'undefined' ? window.PMOSInitiativeDisplay : null);
20
+ const groups = () => (typeof window !== 'undefined' ? window.PMOSStatusGroups : null);
21
+
22
+ /** The lifecycle state `displayOf` reads, for a derived group key that is a PM-set one. */
23
+ const LIFECYCLE = { 'on-hold': 'held', closed: 'closed', cancelled: 'cancelled' };
24
+
25
+ /** The stage an initiative is at: the first blocked step of its ladder, else the current one. Null when no ladder is read. */
26
+ export function stageOf(rec) {
27
+ const steps = rec && Array.isArray(rec.steps) ? rec.steps : null;
28
+ const ti = steps ? targetStepIndex(steps) : -1;
29
+ const k = ti >= 0 && steps[ti] ? String(steps[ti].k || '') : '';
30
+ return k || null;
31
+ }
32
+
33
+ /** The derived group key of one status row (`unknown` when nothing was derived). */
34
+ export function keyOf(statusRow) {
35
+ const sg = groups();
36
+ return sg ? sg.keyOf(statusRow) : 'unknown';
37
+ }
38
+
39
+ /** The word `displayOf` gives this initiative, or null when the display module is not loaded. */
40
+ export function wordOf(statusRow, rec) {
41
+ const d = display();
42
+ if (!d || typeof d.displayOf !== 'function') return null;
43
+ const key = keyOf(statusRow);
44
+ const row = { stage: stageOf(rec), accepted: key === 'accepted', last_activity: (statusRow && statusRow.last_activity) || null };
45
+ const lifecycle = LIFECYCLE[key] ? { state: LIFECYCLE[key], reason: (statusRow && statusRow.lifecycle && statusRow.lifecycle.reason) || '', at: null } : null;
46
+ const out = d.displayOf(row, { lifecycle, now: Date.now() });
47
+ return out && out.word ? String(out.word) : null;
48
+ }
49
+
50
+ /** How loudly the word is drawn: what needs the PM is destructive, what waits on their acceptance is a warning. */
51
+ export function toneOf(statusRow) {
52
+ const key = keyOf(statusRow);
53
+ if (key === 'needs-you' || key === 'unknown') return 'd';
54
+ if (key === 'awaiting-acceptance') return 'w';
55
+ return null;
56
+ }
@@ -0,0 +1,175 @@
1
+ /* pm/dock.js — the frame's agent dock (pm-os-absorb Run 3, slice 3A; PRD R10).
2
+ *
3
+ * The dock IS panel 4: the host app.js has always kept for the tabs (#pm-tabs), with its live terminals, documents and
4
+ * the Web tab (D69). The frame never re-creates or re-parents it; it only shows it open (on Today) or folds it to a 44px
5
+ * rail (everywhere else), and the PM toggles it with ⌘J or the rail. It never opens by itself: a tab that opens while
6
+ * the dock is a rail waits in the dock until the PM opens it.
7
+ *
8
+ * This module holds the parts of the dock that are new in the frame:
9
+ * createDockRail({ open, resize }) the 44px rail: one control that opens the dock. While the dock is open the rail
10
+ * is a thin grip on the dock's left edge that resizes it (0.53.1)
11
+ * foldButton({ fold }) the control in the open dock's strip that folds it back to the rail
12
+ * focusButton({ toggle }) / paintFocus(b, on)
13
+ * Focus: the dock takes the whole window, and Exit focus gives the page back (0.53.1)
14
+ * sessionMenuButton({ newSession, withOptions, openSkill, ws })
15
+ * "+ Session": New session, Session with options, then the skills to start
16
+ * from (R10: Skills start from the dock's +session menu; the Skills view goes)
17
+ * sessionDefaults() / setSessionDefaults(patch)
18
+ * the default CLI and model that Settings sets and the session menu applies
19
+ *
20
+ * The defaults live in this browser (localStorage), like the dock's other per-window choices; a fixture page reads and
21
+ * writes nothing.
22
+ */
23
+
24
+ import { el } from './dom.js';
25
+ import { FIXTURE } from './state.js';
26
+ import { pmGet } from './http.js';
27
+ import { button, iconButton, menu } from './primitives.js';
28
+ import { COPY } from './frame-copy.js';
29
+
30
+ /* ── the default CLI and model ────────────────────────────────────────────────────────────────────────────────────── */
31
+
32
+ const DEFAULTS_KEY = 'pmos.workspace.pm.session-defaults';
33
+ export const CLIS = Object.freeze(['shell', 'claude']);
34
+ export const TIERS = Object.freeze(['auto', 'fast', 'balanced', 'deep']);
35
+
36
+ /** { cli: 'shell'|'claude', tier: 'auto'|'fast'|'balanced'|'deep' }. An unreadable or unknown value is the default. */
37
+ export function sessionDefaults() {
38
+ let raw = null;
39
+ if (!FIXTURE) { try { raw = JSON.parse(localStorage.getItem(DEFAULTS_KEY) || 'null'); } catch { raw = null; } }
40
+ const r = raw && typeof raw === 'object' ? raw : {};
41
+ return { cli: CLIS.includes(r.cli) ? r.cli : 'shell', tier: TIERS.includes(r.tier) ? r.tier : 'auto' };
42
+ }
43
+
44
+ export function setSessionDefaults(patch) {
45
+ const next = { ...sessionDefaults(), ...(patch || {}) };
46
+ const clean = { cli: CLIS.includes(next.cli) ? next.cli : 'shell', tier: TIERS.includes(next.tier) ? next.tier : 'auto' };
47
+ if (!FIXTURE) { try { localStorage.setItem(DEFAULTS_KEY, JSON.stringify(clean)); } catch { /* private mode: the default stands */ } }
48
+ return clean;
49
+ }
50
+
51
+ /* ── the rail and the fold control ────────────────────────────────────────────────────────────────────────────────── */
52
+
53
+ export function createDockRail({ open, resize } = {}) {
54
+ const b = iconButton({ icon: 'bot', ariaLabel: COPY.dock.open, onClick: () => open && open(), attrs: { 'data-fr': 'dock-open', 'aria-keyshortcuts': 'Meta+J Control+J' } });
55
+ return el('div', { class: 'fr-dock-rail', 'data-fr': 'dock-rail' }, b, resize ? dockGrip(resize) : null);
56
+ }
57
+
58
+ /* ── the dock's width (0.53.1: the PM found the terminal too small) ───────────────────────────────────────────────────
59
+ * The grip sits in the rail's place while the dock is open, so the body keeps its four children and the dock stays the
60
+ * fourth. `resize` = { get() → px | null, set(px | null) }: null is the default width (the CSS clamp). The grip measures
61
+ * from the body's right edge, so the dock is as wide as the distance from the pointer to the window's edge. */
62
+ export const DOCK_MIN = 320;
63
+ export const DOCK_STEP = 32;
64
+ /** The widest the dock may be in a body `bodyW` wide: the page keeps 280px beside it. */
65
+ export const dockMax = (bodyW) => Math.max(DOCK_MIN, Math.round(bodyW) - 280);
66
+ export const clampDock = (px, bodyW) => Math.min(dockMax(bodyW), Math.max(DOCK_MIN, Math.round(px)));
67
+
68
+ function dockGrip(resize) {
69
+ const g = el('div', {
70
+ class: 'fr-dock-grip', 'data-fr': 'dock-grip', role: 'separator', 'aria-orientation': 'vertical',
71
+ 'aria-label': COPY.dock.resize, title: COPY.dock.resize, tabindex: '0',
72
+ });
73
+ grips.set(g, resize);
74
+ g.addEventListener('dblclick', () => resize.set(null));
75
+ g.addEventListener('keydown', (e) => {
76
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
77
+ e.preventDefault();
78
+ const b = g.closest('.fr-body');
79
+ const d = b && b.querySelector(':scope > .fr-dock');
80
+ const now = resize.get() || (d ? d.getBoundingClientRect().width : 0);
81
+ resize.set(clampDock(now + (e.key === 'ArrowLeft' ? DOCK_STEP : -DOCK_STEP), b ? b.getBoundingClientRect().width : window.innerWidth));
82
+ });
83
+ return g;
84
+ }
85
+
86
+ /* The drag is read on the document, once: a press on a grip starts it, and the moves and the release are the window's,
87
+ * so a drag that leaves the grip (it is 6px wide) keeps going. The grip itself is a separator with keys (above). */
88
+ const grips = new WeakMap(); // grip -> resize
89
+ let drag = null; // { g, resize, right }
90
+ document.addEventListener('pointerdown', (e) => {
91
+ const g = e.button === 0 && e.target && e.target.closest ? e.target.closest('[data-fr="dock-grip"]') : null;
92
+ const resize = g && grips.get(g);
93
+ const b = g && g.closest('.fr-body');
94
+ if (!resize || !b) return;
95
+ e.preventDefault();
96
+ drag = { g, resize, body: b, right: b.getBoundingClientRect().right };
97
+ g.setAttribute('data-dragging', '');
98
+ });
99
+ window.addEventListener('pointermove', (e) => {
100
+ if (drag) drag.resize.set(clampDock(drag.right - e.clientX, drag.body.getBoundingClientRect().width));
101
+ });
102
+ const endDrag = () => { if (drag) { drag.g.removeAttribute('data-dragging'); drag = null; } };
103
+ window.addEventListener('pointerup', endDrag);
104
+ window.addEventListener('pointercancel', endDrag);
105
+
106
+ const focusLabels = new WeakMap(); // the Focus control -> its label node
107
+ export function focusButton({ toggle } = {}) {
108
+ const label = el('span', null, COPY.dock.focus);
109
+ const b = button({ label, variant: 'ghost', size: 'sm',
110
+ attrs: { class: 'fr-dock-focus', 'data-fr': 'dock-focus', 'aria-pressed': 'false', title: COPY.dock.focusTitle, 'aria-keyshortcuts': 'Meta+Shift+J Control+Shift+J' },
111
+ onClick: () => toggle && toggle() });
112
+ focusLabels.set(b, label);
113
+ return b;
114
+ }
115
+ /** Show the Focus control's state: pressed while the dock has the whole window. */
116
+ export function paintFocus(b, on) {
117
+ if (!b) return;
118
+ const label = focusLabels.get(b);
119
+ const text = on ? COPY.dock.unfocus : COPY.dock.focus;
120
+ if (label && label.textContent !== text) label.textContent = text;
121
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
122
+ b.title = on ? COPY.dock.unfocusTitle : COPY.dock.focusTitle;
123
+ }
124
+
125
+ export function foldButton({ fold } = {}) {
126
+ return iconButton({ icon: 'chevron-right', ariaLabel: COPY.dock.fold, onClick: () => fold && fold(), attrs: { class: 'fr-dock-fold', 'data-fr': 'dock-fold', 'aria-keyshortcuts': 'Meta+J Control+J' } });
127
+ }
128
+
129
+ /* ── + Session ────────────────────────────────────────────────────────────────────────────────────────────────────── */
130
+
131
+ const skillsByWs = new Map(); // ws -> { at, list | null, error }
132
+ const SKILLS_TTL = 60000;
133
+
134
+ function loadSkills(ws) {
135
+ const key = ws || '';
136
+ const have = skillsByWs.get(key);
137
+ if (have && (have.pending || Date.now() - have.at < SKILLS_TTL)) return have.pending || Promise.resolve(have);
138
+ const entry = { at: Date.now(), list: have ? have.list : null, error: null };
139
+ entry.pending = pmGet('/api/skills' + (ws ? '?ws=' + encodeURIComponent(ws) : '')).then((j) => {
140
+ entry.list = j && Array.isArray(j.skills) ? j.skills : null;
141
+ entry.error = entry.list ? null : ((j && j.error) || COPY.dock.skillsUnread);
142
+ entry.at = Date.now();
143
+ delete entry.pending;
144
+ return entry;
145
+ });
146
+ skillsByWs.set(key, entry);
147
+ return entry.pending;
148
+ }
149
+
150
+ /**
151
+ * The dock's "+ Session" control. A press opens a menu: New session (the one-click start, or the options form when the
152
+ * default CLI is not the shell, because only the form can start another CLI), Session with options, and one item per
153
+ * skill of the repository. A skill opens in its own tab in the dock, where it is read and started.
154
+ */
155
+ export function sessionMenuButton({ newSession, withOptions, openSkill, ws } = {}) {
156
+ if (!FIXTURE) loadSkills(ws);
157
+ const b = button({
158
+ label: COPY.dock.session, variant: 'outline', size: 'sm', icon: 'plus', iconEnd: 'chevron-down',
159
+ attrs: { class: 'fr-session', 'data-fr': 'session', 'aria-haspopup': 'menu', 'aria-label': COPY.dock.sessionMenu },
160
+ onClick: async () => {
161
+ const got = FIXTURE ? null : await loadSkills(ws);
162
+ const d = sessionDefaults();
163
+ const items = [
164
+ { label: COPY.dock.newSession, onSelect: () => (d.cli === 'shell' ? newSession && newSession() : withOptions && withOptions(d)) },
165
+ { label: COPY.dock.withOptions, onSelect: () => withOptions && withOptions(d) },
166
+ { sep: true },
167
+ ];
168
+ const list = got && got.list ? got.list : [];
169
+ if (!list.length) items.push({ label: got && got.error ? COPY.dock.skillsUnread : COPY.dock.noSkills, onSelect: () => {} });
170
+ for (const sk of list) items.push({ label: COPY.dock.skillsHead + ': ' + (sk.label || sk.skill || sk.id), onSelect: () => openSkill && openSkill(sk) });
171
+ menu({ anchor: b, items, ariaLabel: COPY.dock.sessionMenu });
172
+ },
173
+ });
174
+ return b;
175
+ }