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,218 @@
1
+ /* pm/initiative-list.js — the List: initiatives by objective, then by KR or health budget (pm-os-absorb Run 2, slice 2D;
2
+ * PRD R17, W2 and W5).
3
+ *
4
+ * A group header names the objective and its period (from the OKR file). Under it each KR or health budget has a header
5
+ * that shows a value ONLY when one was measured ("KR 2.1 · 41% on 2 Oct → 55%"): a header with no measurement has no value
6
+ * element at all. Nothing is computed here; the value is the server's measured line, with its source.
7
+ *
8
+ * A row is the title, one meta line ("Build · waiting on you · 7 of 12 · due 14 Nov"), the health reason and, when the PM
9
+ * owes something, the attention dot. Within a group rows follow the horizon (Now, Next, Later), then the rank the server
10
+ * gives, then the next due date.
11
+ *
12
+ * Bulk acts, by selecting (the Select button, or `x` on a row): Move to Next, Move to Later, Change lifecycle. Move to Now
13
+ * and the anchor change are never bulk. Changing the lifecycle of several initiatives mints one lifecycle-bulk grant on the
14
+ * server, and the control is disabled BEFORE a press while /api/pm-modules does not list that scope.
15
+ *
16
+ * mountList(host, env) → { dispose(), reload() }
17
+ * env = { repo, ws, openInitiative(repo, slug), grants: () => string[] }
18
+ */
19
+
20
+ import { el } from './dom.js';
21
+ import { row, iconButton, button, badge, needsDot, menu, empty, toast } from './primitives.js';
22
+ import { watchSig } from './sig-watch.js';
23
+ import { COPY, shortDate } from './initiatives-copy.js';
24
+ import { getList, sigKey, place, placed, leaveNow, setLifecycleBulk } from './initiatives-api.js';
25
+ import { healthSignal, say, confirmSheet, checkBySheet, onContextMenu } from './initiatives-ui.js';
26
+
27
+ const ORDER = { now: 0, next: 1, later: 2 };
28
+ const MAX_BULK = 20;
29
+ const STATES = ['on-hold', 'closed', 'cancelled', 'active'];
30
+
31
+ export function mountList(host, env) {
32
+ const root = el('div', { class: 'in-list-view', 'data-in-view': 'list' });
33
+ host.replaceChildren(root);
34
+ let data = null;
35
+ let error = null;
36
+ let selecting = false;
37
+ const picked = new Set();
38
+ let disposed = false;
39
+ let stop = null;
40
+ let latest = null;
41
+
42
+ const allRows = () => ((data && data.groups) || []).flatMap((g) => g.anchors.flatMap((a) => a.rows));
43
+ const rowOf = (id) => allRows().find((r) => r.id === id) || null;
44
+
45
+ /* ── writes ───────────────────────────────────────────────────────────────────────────────────────────────────── */
46
+
47
+ async function moveMany(idList, horizon) {
48
+ // Chained, so the selection keeps its order at the top of the horizon: the first at the top, each next after it.
49
+ const events = idList.map((id, i) => placed(id, horizon, i ? idList[i - 1] : null, leaveNow(rowOf(id), horizon)));
50
+ const out = await place(env.ws, events);
51
+ if (say(out, COPY.moveDone(idList.length, COPY.horizons[horizon]))) { picked.clear(); selecting = false; }
52
+ await reload();
53
+ }
54
+ const orderedPicks = () => allRows().filter((r) => picked.has(r.id)).map((r) => r.id);
55
+
56
+ /* The state is chosen inside the confirm sheet (it opens on On hold), so the path is Select, Change lifecycle, Confirm: three presses (R17). */
57
+ function bulkLifecycle() {
58
+ const idList = orderedPicks();
59
+ if (!idList.length) return;
60
+ confirmSheet({
61
+ title: COPY.bulkLifecycleTitle(idList.length), body: COPY.lifecycleBody(null, idList.length), reason: true, confirmLabel: COPY.confirm,
62
+ choice: { label: COPY.lifecycleChoose, items: STATES.map((id) => ({ id, label: COPY.lifecycleState[id] })), value: STATES[0] },
63
+ onConfirm: async (reason, state) => {
64
+ const out = await setLifecycleBulk(env.ws, idList, state, reason);
65
+ if (out && out.ok) { toast({ text: COPY.lifecycleDone(COPY.lifecycleState[state], idList.length), timeout: 4000 }); picked.clear(); selecting = false; reload(); }
66
+ return out;
67
+ },
68
+ });
69
+ }
70
+
71
+ function rowMenu(anchor, id) {
72
+ const r = rowOf(id); if (!r) return;
73
+ const items = [{ label: COPY.menu.open, onSelect: () => env.openInitiative(env.repo, id) }, { sep: true }];
74
+ for (const h of ['now', 'next', 'later']) if (r.horizon !== h || r.dormant) items.push({ label: COPY.menu['move' + h[0].toUpperCase() + h.slice(1)], onSelect: async () => { say(await place(env.ws, [placed(id, h, null, leaveNow(r, h))]), COPY.moved[h](r.title)); reload(); } });
75
+ if (r.horizon === 'now') items.push({ sep: true }, {
76
+ label: COPY.menu.setCheckBy,
77
+ onSelect: () => checkBySheet({
78
+ title: COPY.menu.setCheckBy, value: r.check_by,
79
+ onSave: async (date) => {
80
+ const out = await place(env.ws, [placed(id, r.horizon, null, { check_by: date })]);
81
+ if (out && out.ok) { toast({ text: COPY.moved.checkBy(r.title, date ? shortDate(date) : ''), timeout: 4000 }); reload(); }
82
+ return out;
83
+ },
84
+ }),
85
+ });
86
+ menu({ anchor, items, ariaLabel: COPY.aria.more(r.title) });
87
+ }
88
+
89
+ function toggle(id) {
90
+ if (picked.has(id)) picked.delete(id);
91
+ else if (picked.size >= MAX_BULK) { toast({ text: COPY.bulkMax, timeout: 4000 }); return; }
92
+ else picked.add(id);
93
+ render();
94
+ }
95
+
96
+ /* ── drawing ──────────────────────────────────────────────────────────────────────────────────────────────────── */
97
+
98
+ function rowEl(r) {
99
+ const waiting = r.display && r.display.waitingOn === 'pm' ? COPY.waitingYou : r.display && r.display.waitingOn === 'agent' ? COPY.waitingAgent : null;
100
+ const meta = [r.display && r.display.word, waiting, r.progress && r.progress.label, r.next_due ? COPY.due(shortDate(r.next_due)) : null];
101
+ const hs = healthSignal(r.health);
102
+ const trail = [];
103
+ if (r.display && r.display.waitingOn === 'pm') trail.push(el('span', { class: 'in-attn', title: COPY.attention }, needsDot()));
104
+ trail.push(badge({ text: COPY.horizons[r.horizon] || r.horizon, tone: 'outline', attrs: { 'data-in-horizon': r.horizon } }));
105
+ trail.push(iconButton({
106
+ icon: 'ellipsis', size: 'sm', ariaLabel: COPY.aria.more(r.title), expanded: false,
107
+ onClick: (e) => { e.stopPropagation(); rowMenu(e.currentTarget, r.id); },
108
+ attrs: { class: 'in-more', 'aria-haspopup': 'menu', 'data-in-more': r.id },
109
+ }));
110
+ const lead = [];
111
+ if (selecting) {
112
+ const on = picked.has(r.id);
113
+ lead.push(iconButton({
114
+ icon: on ? 'square-check-big' : 'square', size: 'sm', ariaLabel: COPY.aria.select(r.title),
115
+ onClick: (e) => { e.stopPropagation(); toggle(r.id); }, attrs: { 'aria-pressed': on ? 'true' : 'false', 'data-in-select': r.id },
116
+ }));
117
+ }
118
+ return row({
119
+ title: r.title, meta, signals: hs ? [hs] : [], lead, trail,
120
+ onOpen: () => env.openInitiative(env.repo, r.id), openLabel: COPY.aria.open(r.title),
121
+ attrs: { class: 'in-row', 'data-in-row': r.id, 'data-horizon': r.horizon, 'data-selected': picked.has(r.id) ? 'true' : null },
122
+ openAttrs: { 'data-in-open': r.id },
123
+ });
124
+ }
125
+
126
+ const rank = (a, b) => (ORDER[a.horizon] - ORDER[b.horizon]) || ((a.rank || 0) - (b.rank || 0)) || String(a.next_due || '~').localeCompare(String(b.next_due || '~'));
127
+
128
+ function anchorEl(a) {
129
+ const head = [el('span', { class: 'in-anchor__id' }, a.anchor)];
130
+ if (a.title) head.push(el('span', { class: 'in-anchor__title' }, a.title));
131
+ if (a.measurement) head.push(el('span', { class: 'in-anchor__value', 'data-in-measured': '', title: a.measurement.source || null }, COPY.measured(a.measurement)));
132
+ return el('section', { class: 'in-anchor', 'data-in-anchor': a.anchor, 'aria-label': a.anchor },
133
+ el('div', { class: 'in-anchor__head' }, head),
134
+ el('div', { class: 'in-rows', role: 'list' }, [...a.rows].sort(rank).map(rowEl)));
135
+ }
136
+
137
+ function groupEl(g) {
138
+ const first = g.anchors[0];
139
+ const name = g.objective ? g.objective.title : (first && first.kind === 'health' ? COPY.healthBudgets : COPY.noObjective);
140
+ const head = [el('span', { class: 'in-group__name' }, name)];
141
+ if (g.objective && g.objective.period) head.push(el('span', { class: 'in-group__period' }, g.objective.period));
142
+ return el('section', { class: 'in-group', 'data-in-group': g.objective ? g.objective.id : '' },
143
+ el('h3', { class: 'in-group__head' }, head), g.anchors.map(anchorEl));
144
+ }
145
+
146
+ function bulkBar() {
147
+ const n = picked.size;
148
+ const can = env.grants().includes('lifecycle-bulk');
149
+ const kids = [el('span', { class: 'in-bulk__count', 'data-in-selected': '' }, COPY.selectedN(n))];
150
+ kids.push(button({ label: COPY.bulkNext, variant: 'outline', size: 'sm', disabled: !n, onClick: () => moveMany(orderedPicks(), 'next'), attrs: { 'data-in-bulk': 'next' } }));
151
+ kids.push(button({ label: COPY.bulkLater, variant: 'outline', size: 'sm', disabled: !n, onClick: () => moveMany(orderedPicks(), 'later'), attrs: { 'data-in-bulk': 'later' } }));
152
+ kids.push(button({
153
+ label: COPY.bulkLifecycle, variant: 'outline', size: 'sm', disabled: !n || !can,
154
+ onClick: () => bulkLifecycle(), attrs: { 'data-in-bulk': 'lifecycle', title: can ? null : COPY.bulkLifecycleLater },
155
+ }));
156
+ kids.push(button({ label: COPY.done, variant: 'ghost', size: 'sm', onClick: () => { selecting = false; picked.clear(); render(); }, attrs: { 'data-in-bulk': 'done' } }));
157
+ const bar = el('div', { class: 'in-bulk', 'data-in-bulkbar': '' }, kids);
158
+ if (!can) bar.appendChild(el('p', { class: 'in-note in-note--inline', 'data-in-bulk-later': '' }, COPY.bulkLifecycleLater));
159
+ return bar;
160
+ }
161
+
162
+ function render() {
163
+ if (disposed) return;
164
+ const active = document.activeElement && document.activeElement.getAttribute ? (document.activeElement.getAttribute('data-in-open') || document.activeElement.getAttribute('data-in-select')) : null;
165
+ if (!data) {
166
+ root.replaceChildren(error ? el('p', { class: 'in-note in-note--error', role: 'alert' }, COPY.readFailed(error)) : empty({ text: COPY.reading }));
167
+ return;
168
+ }
169
+ const kids = [];
170
+ if (!allRows().length) kids.push(empty({ text: COPY.emptyAll }));
171
+ else {
172
+ kids.push(el('p', { class: 'in-note in-hint', 'data-in-hint': '' }, COPY.menuHintRow));
173
+ kids.push(el('div', { class: 'in-toolbar' }, selecting ? bulkBar() : button({ label: COPY.select, variant: 'ghost', size: 'sm', onClick: () => { selecting = true; render(); }, attrs: { 'data-in-select-mode': '' } })));
174
+ kids.push(...data.groups.map(groupEl));
175
+ }
176
+ root.replaceChildren(...kids);
177
+ if (active) { const t = root.querySelector('[data-in-open="' + CSS.escape(active) + '"], [data-in-select="' + CSS.escape(active) + '"]'); if (t) t.focus({ preventScroll: true }); }
178
+ }
179
+
180
+ /* The row menu is the phone's path (the ⋯ button shows where a touch screen is). On a desk it opens from the context menu:
181
+ * a right click, or the menu key on a focused row. */
182
+ onContextMenu(root, (t) => {
183
+ const node = t && t.closest ? t.closest('[data-in-row]') : null;
184
+ const open = node && node.querySelector('[data-in-open]');
185
+ return open ? { anchor: open, id: node.getAttribute('data-in-row') } : null;
186
+ }, rowMenu);
187
+
188
+ /* `x` on a row selects it (and starts selecting), the keyboard path of the bulk acts. */
189
+ root.addEventListener('keydown', (e) => {
190
+ if (e.key !== 'x' || e.altKey || e.ctrlKey || e.metaKey) return;
191
+ const open = e.target && e.target.closest ? e.target.closest('[data-in-open]') : null;
192
+ if (!open) return;
193
+ e.preventDefault(); e.stopPropagation();
194
+ selecting = true;
195
+ toggle(open.getAttribute('data-in-open'));
196
+ });
197
+
198
+ /* ── reading ──────────────────────────────────────────────────────────────────────────────────────────────────── */
199
+
200
+ function adopt(answer) { if (disposed) return; data = answer; error = null; render(); }
201
+ let reads = 0; // only the newest read is drawn: an older answer that comes back late is dropped
202
+ async function reload() {
203
+ const mine = ++reads;
204
+ const a = await getList(env.ws);
205
+ if (disposed || mine !== reads) return;
206
+ if (a && a.ok) adopt(a);
207
+ else if (!data) { error = (a && a.error) || 'the server did not answer'; render(); }
208
+ }
209
+
210
+ render();
211
+ reload().then(() => {
212
+ if (disposed) return;
213
+ stop = watchSig(async () => { const a = await getList(env.ws); if (a && a.ok) { latest = a; return sigKey(a); } return null; },
214
+ () => { if (latest) adopt(latest); }, { everyMs: 10000 });
215
+ });
216
+
217
+ return { reload, dispose() { disposed = true; if (stop) stop(); } };
218
+ }
@@ -0,0 +1,261 @@
1
+ /* pm/initiative-sheet.js — the initiative sheet (pm-os-absorb Run 2, slice 2D; PRD R13, R14, R16, X7).
2
+ *
3
+ * About 70% of the width on a desktop and the whole screen on a phone. Top to bottom:
4
+ * line 1 the title, the horizon, the health reason (only when not on track), and a meta line: lane · anchor · id
5
+ * line 2 the stage stepper, then "Waiting on you: approval [Open gate ▸]" or "Waiting on agent: build [Hand to agent ▸]"
6
+ * tabs Breakdown (the Run 1 pane, embedded: Tree · Board, and Timeline once Run 4 registers it), Files, History
7
+ * ⋯ horizon, check-by, lifecycle and the anchor (both PM-only: the server's guard answers), Copy CLI, Export breakdown
8
+ * Nothing on this sheet says "plan". Everything it shows is read from GET /api/initiatives/<id>; every write goes through
9
+ * initiatives-api.js. The Breakdown tab mounts with a tab id of the form `initiative:<slug>`, so it never meets a panel-4
10
+ * tab, and the sheet releases it (unmountBreakdown) when it closes or the tab changes.
11
+ *
12
+ * If the sheet cannot be read (a server without the Initiatives routes), the legacy detail pane opens instead, so an
13
+ * "open" from Today or the Breakdown never goes nowhere.
14
+ */
15
+
16
+ import { el } from './dom.js';
17
+ import { sheet, button, iconButton, tabs, row, badge, menu, toast, empty, field, ensureComponentsCss } from './primitives.js';
18
+ import { icon, ensureSprite } from './icons.js';
19
+ import { mountBreakdown, unmountBreakdown } from './panel-breakdown.js';
20
+ import { openDoc } from './panel-initiatives.js';
21
+ import { openNewSession } from './panel-newsess.js';
22
+ import { wsOf, readDoc, saveDoc } from './data.js';
23
+ import { state, setState } from './state.js';
24
+ import { dayOf } from './breakdown-copy.js';
25
+ import { COPY, shortDate } from './initiatives-copy.js';
26
+ import { getSheet, getExport, place, placed, leaveNow, setLifecycle } from './initiatives-api.js';
27
+ import { healthSignal, say, confirmSheet, checkBySheet, horizonLabel } from './initiatives-ui.js';
28
+
29
+ /** The pre-Run-2 detail pane: the fallback, and the home of the legacy gate buttons that "Open gate" reaches. */
30
+ export function legacyOpen(repo, slug) {
31
+ const repos = typeof window !== 'undefined' && window.pmApp && window.pmApp.mods ? window.pmApp.mods.repos : null;
32
+ if (repos && typeof repos.selectRepo === 'function' && repo) repos.selectRepo(repo);
33
+ setState({ sel: slug, detail: { kind: 'init' }, col2: true });
34
+ }
35
+
36
+ export const changed = () => { try { window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ } };
37
+
38
+ const who = (by) => {
39
+ const s = String(by || '');
40
+ if (s.startsWith('pm:')) return 'You';
41
+ const m = /^agent:(sess-\d+)/.exec(s);
42
+ return m ? 'Agent · ' + m[1] : s.startsWith('agent:') ? 'Agent' : s;
43
+ };
44
+
45
+ /** The shape of an anchor: a KR id ("KR 2.1") or health:<id>. The same shape lib/measurements.js reads. */
46
+ const ANCHOR_RE = /^(?:health:[a-z0-9][a-z0-9-]{0,59}|[A-Za-z0-9][A-Za-z0-9 .:_-]{0,79})$/;
47
+
48
+ /** The record with its frontmatter `parent_kr` line set to `next`, or null when the frontmatter has no such line. The body is never touched, and a function replacement keeps `$&` and `$'` in `next` literal. */
49
+ function withAnchor(text, next) {
50
+ const fm = /^---\r?\n[\s\S]*?\r?\n---/.exec(text);
51
+ if (!fm || !/^parent_kr:.*$/m.test(fm[0])) return null;
52
+ return fm[0].replace(/^parent_kr:.*$/m, () => 'parent_kr: ' + next) + text.slice(fm[0].length);
53
+ }
54
+
55
+ let current = null;
56
+
57
+ /** Open the sheet of one initiative. `repo` is the repository's name (as the registry's openInitiative gets it). */
58
+ export function openSheet(repo, slug) {
59
+ const ws = wsOf(repo) || state.wsId;
60
+ if (!ws || !slug) { legacyOpen(repo, slug); return null; }
61
+ if (current && current.open) current.close();
62
+ ensureComponentsCss();
63
+ ensureSprite();
64
+ let data = null;
65
+ let tab = 'breakdown';
66
+ let closed = false;
67
+ const tabId = 'initiative:' + slug;
68
+ const crumb = el('div', { class: 'in-sheet__crumb' });
69
+ const stepperHost = el('div', { class: 'in-sheet__stepper-host' });
70
+ const waitingHost = el('div', { class: 'in-sheet__waiting-host' });
71
+ const tabsHost = el('div', { class: 'in-sheet__tabs' });
72
+ const panel = el('div', { class: 'in-sheet__panel', 'data-in-panel': '' });
73
+ const s = sheet({
74
+ mode: 'auto', title: slug, sub: '', body: [crumb, stepperHost, waitingHost, tabsHost, panel],
75
+ attrs: { class: 'in-sheet', 'data-in-sheet': 'initiative', 'data-in-id': slug },
76
+ onClose: () => { closed = true; unmountBreakdown(panel); if (current === s) current = null; },
77
+ });
78
+ current = s;
79
+
80
+ const reload = async () => {
81
+ const out = await getSheet(ws, slug);
82
+ if (closed) return;
83
+ if (!out || !out.ok || !out.initiative) {
84
+ s.close();
85
+ legacyOpen(repo, slug);
86
+ return;
87
+ }
88
+ data = out;
89
+ draw();
90
+ };
91
+ const after = (out, okText) => { if (say(out, okText)) { changed(); reload(); } };
92
+
93
+ /* ── the ⋯ menu ───────────────────────────────────────────────────────────────────────────────────────────────── */
94
+
95
+ function openMore(anchor) {
96
+ const I = data.initiative;
97
+ const items = [];
98
+ for (const h of ['now', 'next', 'later']) if (I.horizon !== h) items.push({ label: COPY.menu['move' + h[0].toUpperCase() + h.slice(1)], onSelect: async () => after(await place(ws, [placed(slug, h, null, leaveNow(I, h))]), COPY.moved[h](I.title)) });
99
+ if (I.horizon === 'now') items.push({
100
+ label: COPY.menu.setCheckBy,
101
+ onSelect: () => checkBySheet({
102
+ title: COPY.sheet.checkByTitle, value: I.check_by,
103
+ onSave: async (date) => {
104
+ const out = await place(ws, [placed(slug, I.horizon, null, { check_by: date })]);
105
+ if (out && out.ok) { toast({ text: COPY.moved.checkBy(I.title, date ? shortDate(date) : ''), timeout: 4000 }); changed(); reload(); }
106
+ return out;
107
+ },
108
+ }),
109
+ });
110
+ items.push({ sep: true });
111
+ const stopped = ['On hold', 'Closed', 'Cancelled'].includes(I.display && I.display.word);
112
+ const states = stopped ? [['active', 'lifecycleReopen']] : [['on-hold', 'lifecycleHold'], ['closed', 'lifecycleClose'], ['cancelled', 'lifecycleCancel']];
113
+ for (const [st, key] of states) items.push({ label: COPY[key], onSelect: () => lifecycle(st) });
114
+ items.push({ label: COPY.sheet.changeAnchor, onSelect: () => anchorSheet() });
115
+ items.push({ sep: true });
116
+ items.push({ label: COPY.sheet.copyCli, onSelect: () => copyCli() });
117
+ items.push({ label: COPY.sheet.exportBreakdown, onSelect: () => exportBreakdown() });
118
+ menu({ anchor, items, ariaLabel: COPY.sheet.menuMore(I.title) });
119
+ }
120
+
121
+ function lifecycle(st) {
122
+ const state = COPY.lifecycleState[st];
123
+ confirmSheet({
124
+ title: COPY.lifecycleTitle(state, 1), body: COPY.lifecycleBody(state, 1), reason: true,
125
+ onConfirm: async (reason) => {
126
+ const out = await setLifecycle(ws, slug, st, reason);
127
+ if (out && out.ok) { toast({ text: COPY.lifecycleDone(state, 1), timeout: 4000 }); changed(); reload(); }
128
+ return out;
129
+ },
130
+ });
131
+ }
132
+
133
+ function anchorSheet() {
134
+ const I = data.initiative;
135
+ const f = field({ label: COPY.sheet.anchorLabel, kind: 'text', value: I.anchor || '', placeholder: COPY.sheet.anchorHint, attrs: { 'data-in-anchor-input': '' } });
136
+ const err = el('p', { class: 'in-error', role: 'alert', 'data-in-error': '' });
137
+ err.hidden = true;
138
+ const rec = (data.files || []).find((x) => x.kind === 'record');
139
+ const file = rec ? rec.path : 'planning/initiatives/' + slug + '.md';
140
+ const fail = (why) => { err.textContent = COPY.refused(why); err.hidden = false; };
141
+ const sh = sheet({
142
+ mode: 'auto', title: COPY.sheet.anchorTitle, body: [el('p', { class: 'in-note' }, COPY.sheet.anchorBody), f, err], attrs: { 'data-in-sheet': 'anchor' },
143
+ actions: [
144
+ button({ label: COPY.cancel, variant: 'outline', onClick: () => sh.close() }),
145
+ button({
146
+ label: COPY.sheet.save, variant: 'primary', attrs: { 'data-in-save': '' },
147
+ onClick: async () => {
148
+ const next = f.control.value.trim();
149
+ if (!next) { fail(COPY.newForm.noAnchor); return; }
150
+ if (!ANCHOR_RE.test(next)) { fail(COPY.sheet.anchorShape); return; }
151
+ let text;
152
+ try { text = await readDoc(file, ws); } catch { fail(COPY.sheet.anchorNoRecord); return; }
153
+ const edited = withAnchor(text, next);
154
+ if (edited === null) { fail(COPY.sheet.anchorNoRecord); return; }
155
+ const out = await saveDoc({ ws, path: file, base: text, text: edited });
156
+ if (out && out.ok !== false && !out.error) { sh.close(); toast({ text: COPY.sheet.anchorSaved(next), timeout: 4000 }); changed(); reload(); } else fail((out && out.error) || 'The server did not answer.');
157
+ },
158
+ }),
159
+ ],
160
+ });
161
+ }
162
+
163
+ async function copyCli() {
164
+ const cmd = (data.cli && (data.cli.now || data.cli.export)) || 'pmos initiative now ' + slug;
165
+ try { await navigator.clipboard.writeText(cmd); toast({ text: COPY.sheet.copied(cmd), timeout: 4000 }); }
166
+ catch { toast({ text: COPY.sheet.copyFailed, tone: 'error', timeout: 6000 }); }
167
+ }
168
+
169
+ async function exportBreakdown() {
170
+ const out = await getExport(ws, slug);
171
+ const text = out && (out.markdown || out.text || out.export);
172
+ if (!out || !out.ok || typeof text !== 'string') { toast({ text: COPY.sheet.exportFailed((out && out.error) || 'no text came back'), tone: 'error', timeout: 6000 }); return; }
173
+ try { await navigator.clipboard.writeText(text); toast({ text: COPY.sheet.exported, timeout: 4000 }); }
174
+ catch { toast({ text: COPY.sheet.copyFailed, tone: 'error', timeout: 6000 }); }
175
+ }
176
+
177
+ /* ── drawing ──────────────────────────────────────────────────────────────────────────────────────────────────── */
178
+
179
+ function drawStepper() {
180
+ const steps = data.stepper || [];
181
+ stepperHost.replaceChildren(el('ol', { class: 'in-stepper', 'aria-label': COPY.sheet.stages, 'data-in-stepper': '' }, steps.map((st) => el('li', {
182
+ class: 'in-stepper__step', 'data-state': st.state, 'data-step': st.id, 'aria-current': st.state === 'current' ? 'step' : null,
183
+ }, st.state === 'done' ? icon('check') : null, el('span', null, st.label)))));
184
+ }
185
+
186
+ function drawWaiting() {
187
+ const w = data.waiting || {};
188
+ const I = data.initiative;
189
+ const kids = [];
190
+ if (w.on === 'pm' || w.on === 'agent') {
191
+ kids.push(el('span', { class: 'in-waiting__text', 'data-in-waiting': w.on }, w.on === 'pm' ? COPY.sheet.waitingYou(w.act) : COPY.sheet.waitingAgent(w.act)));
192
+ const gate = w.action && w.action.kind === 'gate';
193
+ const label = gate ? COPY.sheet.openGate : COPY.sheet.handToAgent;
194
+ kids.push(button({
195
+ label, variant: 'primary', iconEnd: 'chevron-right', attrs: { 'data-in-action': gate ? 'gate' : 'hand' },
196
+ onClick: () => { s.close(); if (gate) legacyOpen(repo, slug); else openNewSession(slug, {}); },
197
+ }));
198
+ } else {
199
+ kids.push(el('span', { class: 'in-waiting__text', 'data-in-waiting': 'none' }, (I.display && I.display.word && ['Accepted', 'Handed off', 'On hold', 'Closed', 'Cancelled'].includes(I.display.word)) ? I.display.word : COPY.sheet.nothingOwed));
200
+ }
201
+ waitingHost.replaceChildren(el('div', { class: 'in-waiting' }, kids));
202
+ }
203
+
204
+ function drawCrumb() {
205
+ const I = data.initiative;
206
+ const hs = healthSignal(I.health);
207
+ const more = iconButton({
208
+ icon: 'ellipsis', ariaLabel: COPY.sheet.menuMore(I.title), expanded: false, onClick: (e) => openMore(e.currentTarget),
209
+ attrs: { 'aria-haspopup': 'menu', 'data-in-sheet-more': '' },
210
+ });
211
+ s.setHead(I.title, [I.lane, I.anchor, I.id].filter(Boolean).join(' · '));
212
+ crumb.replaceChildren(
213
+ badge({ text: horizonLabel(I.horizon), tone: 'outline', attrs: { 'data-in-horizon': I.horizon } }),
214
+ hs || el('span', { class: 'in-sheet__spacer' }),
215
+ more,
216
+ );
217
+ }
218
+
219
+ function drawTabs() {
220
+ tabsHost.replaceChildren(tabs({
221
+ variant: 'segmented', full: true, ariaLabel: COPY.sheet.tabs, value: tab,
222
+ items: [{ id: 'breakdown', label: COPY.sheet.breakdown }, { id: 'files', label: COPY.sheet.files }, { id: 'history', label: COPY.sheet.history }],
223
+ onChange: (id) => {
224
+ if (id === tab) return;
225
+ if (tab === 'breakdown') unmountBreakdown(panel);
226
+ tab = id;
227
+ drawTabs(); drawPanel();
228
+ const t = tabsHost.querySelector('[data-tab="' + id + '"]');
229
+ if (t) t.focus({ preventScroll: true });
230
+ },
231
+ }));
232
+ }
233
+
234
+ function drawPanel() {
235
+ if (!data) return;
236
+ if (tab === 'breakdown') { mountBreakdown(panel, { id: tabId, ws, slug }, { embedded: true }); return; }
237
+ if (tab === 'files') {
238
+ const files = data.files || [];
239
+ panel.replaceChildren(files.length ? el('div', { class: 'in-sheet__rows', role: 'list' }, files.map((f) => row({
240
+ title: f.label, meta: [f.path], openLabel: COPY.aria.open(f.label),
241
+ onOpen: () => { s.close(); openDoc(slug, f.path, f.label); },
242
+ attrs: { 'data-in-file': f.kind }, openAttrs: { 'data-in-file-open': f.kind },
243
+ }))) : empty({ text: COPY.sheet.noFiles }));
244
+ return;
245
+ }
246
+ const hist = data.history || [];
247
+ panel.replaceChildren(hist.length ? el('div', { class: 'in-sheet__rows', role: 'list' }, hist.map((h) => row({
248
+ title: h.text, meta: [dayOf(h.at), who(h.by)], attrs: { 'data-in-history': '' },
249
+ }))) : empty({ text: COPY.sheet.noHistory }));
250
+ }
251
+
252
+ function draw() {
253
+ drawCrumb(); drawStepper(); drawWaiting();
254
+ if (!tabsHost.firstChild) drawTabs();
255
+ drawPanel();
256
+ }
257
+
258
+ panel.replaceChildren(empty({ text: COPY.sheet.reading }));
259
+ reload();
260
+ return s;
261
+ }
@@ -0,0 +1,59 @@
1
+ /* pm/initiatives-api.js — how the Initiatives destination reads and writes (pm-os-absorb Run 2, slice 2D).
2
+ *
3
+ * Every call goes through pmGet and pmPost (http.js), so every press carries the per-launch nonce and the page sends no
4
+ * `by`, `via` or `at`: the server's writer sets them from the call path. The routes are 2C's (lib/routes-initiatives.js);
5
+ * the shapes of their answers are the goldens in fixtures/initiatives-api/. A call never throws: it answers
6
+ * `{ ok: false, error }` and the caller shows the error.
7
+ *
8
+ * GET /api/initiatives?ws=&view=roadmap|list the Roadmap and the List
9
+ * GET /api/initiatives/<id>?ws= the sheet
10
+ * GET /api/initiatives/<id>/export?ws= the breakdown as text
11
+ * POST /api/placements { ws, events: [placed…] } every move, reorder and check-by
12
+ * POST /api/initiatives { ws, title, lane, anchor, intent?, from? } the New-initiative form and a triage card (one body, no events)
13
+ *
14
+ * Every GET of /api/initiatives answers 202 { ok: false, pending: true } until the repository's first derivation lands (2C,
15
+ * about 260 ms a record); a read waits and asks again, so a caller sees the answer or a real error, never the 202.
16
+ * POST /api/lifecycle { ws, initiative_id, state, reason } one initiative (the guarded route of 2A)
17
+ * { ws, ids: [≤20], state, reason } several: one lifecycle-bulk grant (2A, 2B)
18
+ */
19
+
20
+ import { pmGet, pmPost } from './http.js';
21
+
22
+ const enc = encodeURIComponent;
23
+
24
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
+ /** A GET that waits out the first derivation: asks again every 1.2 s while the answer is `pending`, 40 times at most. */
26
+ async function read(path) {
27
+ let a = await pmGet(path);
28
+ for (let k = 0; k < 40 && a && a.pending; k++) { await sleep(1200); a = await pmGet(path); }
29
+ return a && a.pending ? { ok: false, error: 'the workspace is still reading the repository' } : a;
30
+ }
31
+ export const getRoadmap = (ws) => read('/api/initiatives?ws=' + enc(ws) + '&view=roadmap');
32
+ export const getList = (ws) => read('/api/initiatives?ws=' + enc(ws) + '&view=list');
33
+ export const getSheet = (ws, id) => read('/api/initiatives/' + enc(id) + '?ws=' + enc(ws));
34
+ export const getExport = (ws, id) => read('/api/initiatives/' + enc(id) + '/export?ws=' + enc(ws));
35
+
36
+ /** The signature of an answer as one string, for sig-watch.js: the three ledgers it folds. */
37
+ export const sigKey = (answer) => (answer && answer.sig ? JSON.stringify(answer.sig) : null);
38
+
39
+ /** One placed line: horizon, `after` (null = top of the horizon), and a check-by only when the caller says so. */
40
+ export const placed = (initiative, horizon, after, extra) => ({ ev: 'placed', initiative, horizon, after: after || null, ...(extra || {}) });
41
+ export const place = (ws, events) => pmPost('/api/placements', { ws, events });
42
+ /** The extra of a placed line that leaves Now: a check-by belongs to Now (C8), so a card that moves to Next or Later clears its date. Null when there is nothing to clear. */
43
+ export const leaveNow = (row, horizon) => (row && row.check_by && horizon !== 'now' ? { check_by: null } : null);
44
+
45
+ /** Keep (adopt) or drop an agent's proposal. The proposal is named by the eid of its line, in `proposal` (`eid` is a writer field); adopting is a PM-only act. */
46
+ export const adopt = (ws, proposal) => pmPost('/api/placements', { ws, events: [{ ev: 'adopted', proposal }] });
47
+ export const drop = (ws, proposal) => pmPost('/api/placements', { ws, events: [{ ev: 'dropped', proposal }] });
48
+
49
+ /** A new initiative: from the form, or from a triaged capture (`from` is the capture's id). It lands in Later. */
50
+ export const createInitiative = (ws, fields) => pmPost('/api/initiatives', { ws, ...fields });
51
+
52
+ export const setLifecycle = (ws, id, state, reason) => pmPost('/api/lifecycle', { ws, initiative_id: id, state, reason: reason || '' });
53
+ export const setLifecycleBulk = (ws, ids, state, reason) => pmPost('/api/lifecycle', { ws, ids, state, reason: reason || '' });
54
+
55
+ /** The grant scopes a press can mint now (GET /api/pm-modules answers them as `grantScopes`); [] when unknown. */
56
+ export async function grantScopes() {
57
+ const j = await pmGet('/api/pm-modules?want=dest-initiatives.js');
58
+ return Array.isArray(j && j.grantScopes) ? j.grantScopes : [];
59
+ }