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,125 @@
1
+ /* pm/cycle-review.js — the Cycle review card of Today, kind 7 (pm-os-absorb Run 2, slice 2D; PRD R12, R26, R27).
2
+ *
3
+ * ONE renderer for two row kinds. The server's initiative provider makes `cycle-review` (day one, or a new cycle: what
4
+ * reads Now and is unconfirmed, the agents' proposals, the check-by dates that passed). The reports provider makes
5
+ * `cycle-kill-check` (the monthly kill-condition check). Both rows open the same sheet, so the PM reviews the cycle in one
6
+ * place. The kill-check row adds its own line: its cause and the file the check reads.
7
+ *
8
+ * The sheet has three lists:
9
+ * - Now, unconfirmed: Confirm (a placed line into Now, written by the PM's press) or Move back (to Later). One initiative
10
+ * for each press: Move to Now is never bulk (R17), so there is no "Confirm all";
11
+ * - Agent proposals: Keep (adopt: written as the PM's line) or Drop;
12
+ * - Check-by dates passed: Set check-by, or Move to Next.
13
+ * Each press is one write through initiatives-api.js; the sheet reads the Roadmap again and redraws.
14
+ */
15
+
16
+ import { el } from './dom.js';
17
+ import { button, card, row, sheet, toast, ensureComponentsCss } from './primitives.js';
18
+ import { registerTodayCard } from './registry.js';
19
+ import { COPY, shortDate } from './initiatives-copy.js';
20
+ import { getRoadmap, place, placed, leaveNow, adopt, drop } from './initiatives-api.js';
21
+ import { say, checkBySheet } from './initiatives-ui.js';
22
+ import { changed } from './initiative-sheet.js';
23
+
24
+ const refreshToday = (ws) => { try { window.dispatchEvent(new CustomEvent('pmos:ledgers', { detail: { changed: [ws] } })); } catch { /* no window */ } };
25
+
26
+ /** What the sheet lists, from one Roadmap answer. */
27
+ export function cycleItems(data) {
28
+ const today = String((data && data.now) || '').slice(0, 10) || new Date().toISOString().slice(0, 10);
29
+ const now = (data && data.horizons && data.horizons.now) || [];
30
+ return {
31
+ unconfirmed: now.filter((r) => !r.confirmed),
32
+ proposals: (data && data.proposals) || [],
33
+ passed: now.filter((r) => r.check_by && r.check_by < today),
34
+ };
35
+ }
36
+
37
+ export function openCycleReview({ row: needsRow, ws, repo }) {
38
+ ensureComponentsCss();
39
+ const list = el('div', { class: 'in-cycle', 'data-in-cycle': '' }, el('p', { class: 'in-note' }, COPY.cycle.reading));
40
+ const head = [];
41
+ if (needsRow && needsRow.kind === 'cycle-kill-check') {
42
+ const t = needsRow.today || {};
43
+ head.push(el('p', { class: 'in-note', 'data-in-kill': '' }, [needsRow.question, t.evidence ? ' · ' + t.evidence : ''].join('')));
44
+ } else if (needsRow && needsRow.today && needsRow.today.cause) head.push(el('p', { class: 'in-note' }, needsRow.today.cause));
45
+ const s = sheet({ mode: 'auto', title: COPY.cycle.sheetTitle, body: [...head, list], attrs: { 'data-in-sheet': 'cycle-review' }, actions: [button({ label: COPY.sheet.close, variant: 'outline', onClick: () => s.close(), attrs: { 'data-in-close': '' } })] });
46
+
47
+ let data = null;
48
+ const after = async (pending, text) => { const out = await pending; if (say(out, text)) { changed(); refreshToday(ws); } await load(); };
49
+
50
+ const sectionTitle = (text, hint) => [el('h3', { class: 'in-cycle__title' }, text), el('p', { class: 'in-note' }, hint)];
51
+
52
+ function draw() {
53
+ const { unconfirmed, proposals, passed } = cycleItems(data);
54
+ const kids = [];
55
+ if (!unconfirmed.length && !proposals.length && !passed.length) kids.push(el('p', { class: 'in-note', 'data-in-nothing': '' }, COPY.cycle.nothing));
56
+ if (unconfirmed.length) {
57
+ kids.push(...sectionTitle(COPY.cycle.confirmNow, COPY.cycle.confirmNowHint));
58
+ for (const r of unconfirmed) {
59
+ const idx = ((data.horizons.now || []).findIndex((x) => x.id === r.id));
60
+ const prev = idx > 0 ? data.horizons.now[idx - 1].id : null;
61
+ kids.push(row({
62
+ title: r.title, meta: [r.display && r.display.word, r.anchor], attrs: { 'data-in-cycle-row': r.id, 'data-kind': 'unconfirmed' },
63
+ trail: [
64
+ button({ label: COPY.cycle.confirm, variant: 'primary', size: 'sm', attrs: { 'data-in-confirm-one': r.id }, onClick: () => after(place(ws, [placed(r.id, 'now', prev)]), COPY.cycle.confirmed(r.title)) }),
65
+ button({ label: COPY.cycle.moveBack, variant: 'outline', size: 'sm', attrs: { 'data-in-moveback': r.id }, onClick: () => after(place(ws, [placed(r.id, 'later', null, leaveNow(r, 'later'))]), COPY.cycle.movedBack(r.title)) }),
66
+ ],
67
+ }));
68
+ }
69
+ }
70
+ if (proposals.length) {
71
+ kids.push(...sectionTitle(COPY.cycle.proposals, COPY.cycle.proposalsHint));
72
+ for (const p of proposals) {
73
+ kids.push(row({
74
+ title: p.title || p.initiative, meta: [COPY.cycle.proposalLine(COPY.horizons[p.horizon] || p.horizon, p.by)], proposed: true,
75
+ attrs: { 'data-in-cycle-row': p.initiative, 'data-kind': 'proposal', 'data-eid': p.eid },
76
+ trail: [
77
+ button({ label: COPY.cycle.keep, variant: 'primary', size: 'sm', attrs: { 'data-in-keep': p.eid }, onClick: () => after(adopt(ws, p.eid), COPY.cycle.kept) }),
78
+ button({ label: COPY.cycle.drop, variant: 'outline', size: 'sm', attrs: { 'data-in-drop': p.eid }, onClick: () => after(drop(ws, p.eid), COPY.cycle.dropped) }),
79
+ ],
80
+ }));
81
+ }
82
+ }
83
+ if (passed.length) {
84
+ kids.push(...sectionTitle(COPY.cycle.checkBys, COPY.cycle.checkBysHint));
85
+ for (const r of passed) {
86
+ const idx = (data.horizons.now || []).findIndex((x) => x.id === r.id);
87
+ const prev = idx > 0 ? data.horizons.now[idx - 1].id : null;
88
+ kids.push(row({
89
+ title: r.title, meta: [COPY.checkBy(shortDate(r.check_by))], attrs: { 'data-in-cycle-row': r.id, 'data-kind': 'check-by' },
90
+ trail: [
91
+ button({
92
+ label: COPY.cycle.setCheckBy, variant: 'outline', size: 'sm', attrs: { 'data-in-set-check-by': r.id },
93
+ onClick: () => checkBySheet({ title: COPY.sheet.checkByTitle, value: r.check_by, onSave: async (d) => { const out = await place(ws, [placed(r.id, 'now', prev, { check_by: d })]); if (out && out.ok) { toast({ text: COPY.moved.checkBy(r.title, d ? shortDate(d) : ''), timeout: 4000 }); changed(); refreshToday(ws); load(); } return out; } }),
94
+ }),
95
+ button({ label: COPY.cycle.moveNext, variant: 'outline', size: 'sm', attrs: { 'data-in-move-next': r.id }, onClick: () => after(place(ws, [placed(r.id, 'next', null, leaveNow(r, 'next'))]), COPY.moved.next(r.title)) }),
96
+ ],
97
+ }));
98
+ }
99
+ }
100
+ list.replaceChildren(...kids);
101
+ }
102
+
103
+ async function load() {
104
+ const a = await getRoadmap(ws);
105
+ if (a && a.ok) { data = a; draw(); } else list.replaceChildren(el('p', { class: 'in-error', role: 'alert' }, COPY.readFailed((a && a.error) || 'the server did not answer')));
106
+ }
107
+ load();
108
+ return s;
109
+ }
110
+
111
+ /** The one renderer, for both row kinds. */
112
+ export function renderCycleCard(row_, ctx) {
113
+ ensureComponentsCss();
114
+ const t = row_.today || {};
115
+ const open = () => openCycleReview({ row: row_, ws: ctx.ws, repo: ctx.repo });
116
+ return card({
117
+ title: row_.question || COPY.cycle.cardKind, meta: [COPY.cycle.cardKind, t.cause, t.evidence],
118
+ trail: [button({ label: (t.act && t.act.label) || COPY.cycle.review, variant: 'primary', attrs: { 'data-in-cycle-open': row_.kind }, onClick: open })],
119
+ onOpen: open, openLabel: COPY.cycle.review + ': ' + (row_.question || ''),
120
+ attrs: { class: 'in-today-card', 'data-today-row': row_.kind, 'data-provider': row_.provider || null },
121
+ });
122
+ }
123
+
124
+ registerTodayCard('cycle-review', renderCycleCard);
125
+ registerTodayCard('cycle-kill-check', renderCycleCard);
@@ -25,16 +25,16 @@
25
25
  * never a plausible-looking guess. `— not tracked` is the surface's job, not this file's.
26
26
  *
27
27
  * ── What is derived here, and by what rule ──────────────────────────────────────────────────
28
- * PMOS has no task entity, so a phase's tasks are DERIVED (PRD, "Tasks"):
28
+ * PMOS has no task entity. The phases below are folded from the workflow ladder; the tasks they once carried (derived
29
+ * from the rungs) are cut with the Task tab (R29), and the work an initiative holds is the Breakdown's:
29
30
  * · the phase SET comes from the initiative's lane;
30
31
  * · each phase folds a fixed set of the workflow engine's twelve rungs, so every rung's evidence
31
32
  * lands in exactly one phase and none is dropped;
32
- * · a task's status comes from that phase's rungs — `done` where they passed, `blocked` where
33
- * one is blocking, `doing` where one is current, else `todo`. A phase whose rungs carry no
34
- * evidence at all reads `todo`, NEVER `done`;
35
- * · an `## Tasks` section authored in the initiative record overrides the derivation entirely.
36
- * Nothing is invented per initiative: every task's instruction, expectation and evaluation is the
37
- * engine's own text for the rung it belongs to.
33
+ * · a phase's status comes from its rungs — `done` where they passed, `blocked` where one is
34
+ * blocking, `current` where one is current, else `pending`. A phase whose rungs carry no
35
+ * evidence at all reads `pending`, NEVER `done`.
36
+ * Nothing is invented per initiative: every phase's text is the engine's own text for the rung it
37
+ * belongs to.
38
38
  *
39
39
  * ── The one thing this file refuses to do ───────────────────────────────────────────────────
40
40
  * Linking an artifact records a DECLARATION (`{artifact_path, kind, target}` appended to
@@ -53,6 +53,7 @@
53
53
 
54
54
  import { state, setState, subscribe, FIXTURE } from './state.js';
55
55
  import { C } from './fixture.js';
56
+ import { pressFetch } from './http.js';
56
57
 
57
58
  /* ── transport ──────────────────────────────────────────────────────────────────────────────
58
59
  * Relative URLs only. The server's origin/host allowlist 403s a page served from anywhere else,
@@ -81,8 +82,8 @@ async function getJson(path, params) {
81
82
  async function postJson(path, body) {
82
83
  let res;
83
84
  try {
84
- res = await fetch(path, {
85
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
85
+ res = await pressFetch(path, {
86
+ method: 'POST', body: JSON.stringify(body), // pm/http.js: carries the per-launch nonce, and renews a stale one
86
87
  });
87
88
  } catch (e) { return { ok: false, error: 'could not reach the workspace server — ' + msg(e) }; }
88
89
  try { return await res.json(); }
@@ -192,62 +193,7 @@ function decidingRung(rungs) {
192
193
  || rungs[rungs.length - 1] || null;
193
194
  }
194
195
 
195
- /** Task statuses per phase status — the PRD's rule, spread over the three derived tasks. */
196
- const TASK_STATUS = {
197
- done: ['done', 'done', 'done'],
198
- current: ['done', 'doing', 'todo'],
199
- blocked: ['blocked', 'todo', 'todo'],
200
- pending: ['todo', 'todo', 'todo'],
201
- };
202
-
203
- /**
204
- * The three derived tasks for one phase. Each field is the engine's own text for the rungs this
205
- * phase folds — the instruction is what the engine says to DO, the expectation is what it says it
206
- * LOOKED FOR, the evaluation names the rule that decides. A field with no source is null and the
207
- * task pane renders it empty; it is never filled with a sentence that sounds like evidence.
208
- */
209
- function derivedTasks(phase, rungs, ctx) {
210
- const s = phaseRungStatus(rungs);
211
- const lead = decidingRung(rungs) || {};
212
- const titles = s === 'blocked'
213
- ? ['resolve the ' + phase + ' blocker', 'produce the ' + phase + ' output', 'record the result']
214
- : ['read the inputs for ' + phase, 'produce the ' + phase + ' output', 'record the result'];
215
- const evidence = rungs.filter((r) => r.evidence).map((r) => r.key + ': ' + r.evidence).join(' · ') || null;
216
- const inputs = [ctx.record, ctx.brief, ctx.doc, ctx.prd, ctx.rubric].filter(Boolean)
217
- .filter((v, i, a) => a.indexOf(v) === i).join(' · ') || null;
218
- // D77 — what each rung read and the sentence that decides it, from the engine's own basis.
219
- const basis = rungs.map((r) => ({ key: r.key, b: r.basis || null })).filter((x) => x.b);
220
- const sentences = basis.map((x) => x.b.rule).filter((v, i, a) => v && a.indexOf(v) === i);
221
- const readLine = basis.filter((x) => x.b.file)
222
- .map((x) => x.b.file + (x.b.first_seen ? ' (first seen ' + x.b.first_seen + ')' : '')).filter((v, i, a) => a.indexOf(v) === i);
223
- const rules = rungs.map((r) => r.rule).filter(Boolean);
224
- // The produced artifact: a file a rung of this phase read, never guessed. A document or record is
225
- // preferred over a ledger for the second task; the third task shows the ledger the result lands in.
226
- const files = basis.filter((x) => x.b.file && x.b.source === 'file').map((x) => x.b.file);
227
- const ledgers = basis.filter((x) => x.b.file && x.b.source === 'ledger').map((x) => x.b.file);
228
- const produced = files[files.length - 1] || pathIn(lead.evidence) || pathIn(lead.next) || null;
229
- const recorded = ledgers[ledgers.length - 1] || null;
230
- const def = ctx.laneDef || null;
231
- return titles.map((title, i) => ({
232
- title,
233
- status: (TASK_STATUS[s] || TASK_STATUS.pending)[i],
234
- // The engine's own action for the phase — except on the first task, whose job IS the reading,
235
- // so it names the files rather than repeating the action. Neither is authored prose: one is the
236
- // engine's sentence, the other is a list of paths the record declares.
237
- instruction: (i === 0 && inputs ? 'Read ' + inputs : null) || lead.next || lead.evidence || null,
238
- input: inputs,
239
- // What the lane says finishing means, then what the engine found. Both are data, not prose written here.
240
- expect: [def ? def.done : null, evidence ? 'Found: ' + evidence : null].filter(Boolean).join(' — ') || null,
241
- evalNote: sentences.length
242
- ? sentences.join(' ') + (readLine.length ? ' Decided from: ' + readLine.join(', ') + '.' : '')
243
- : rules.length
244
- ? 'workflow rule ' + rules.join(', ') + ' decides this phase'
245
- : 'the workflow engine derives this phase from ' + rungs.map((r) => r.key).join(', '),
246
- output: i === 1 ? produced : (i === 2 ? recorded : null),
247
- }));
248
- }
249
-
250
- /** The first repo-relative path inside an engine message, or null. Used only for a task's `output`. */
196
+ /** The first repo-relative path inside an engine message, or null. Used for a step's `cmd`. */
251
197
  function pathIn(text) {
252
198
  const m = String(text || '').match(/\b(?:planning|prototype|workspace|okf|skills|build-skills)\/[\w./-]+\.\w+/);
253
199
  return m ? m[0] : null;
@@ -492,21 +438,6 @@ function recordFrom(payload) {
492
438
  const lane = fm.lane || (row && row.lane) || null;
493
439
  const steps = row ? phaseSteps(lane, row.steps, row.profile) : [];
494
440
  const laneDef = (payload.lanes && payload.lanes.defs && lane && payload.lanes.defs[lane]) || null;
495
- const ctx = {
496
- record: payload.record, prd: fm.prd_path || null, rubric: fm.rubric_path || null,
497
- brief: fm.brief_path || null, doc: fm.doc_path || null, laneDef,
498
- };
499
- // Authored tasks win over derivation, per phase (PRD, "Tasks"). Their absence is not a defect.
500
- const authored = payload.tasks || {};
501
- const tasks = {};
502
- const byKey = {};
503
- for (const s of (row && row.steps) || []) byKey[s.key] = s;
504
- for (const st of steps) {
505
- const rungs = (RUNGS[laneOf(lane, row && row.profile)][st.k] || []).map((k) => byKey[k]).filter(Boolean);
506
- tasks[st.k] = Array.isArray(authored[st.k]) && authored[st.k].length
507
- ? authored[st.k]
508
- : derivedTasks(st.k, rungs, ctx);
509
- }
510
441
  const rec = {
511
442
  tag: fm.title || (row && row.title) || '',
512
443
  // the lane this record declares, and the lanes this repository allows — what the header's lane picker offers
@@ -522,7 +453,6 @@ function recordFrom(payload) {
522
453
  branch: scope ? scope.branch : null,
523
454
  answer: answerFor(row),
524
455
  steps,
525
- tasks,
526
456
  sessions: (payload.sessions || []).map(sessionRow),
527
457
  links: linksFor(payload),
528
458
  arts: artsFor(payload),
@@ -905,101 +835,6 @@ function currentProductName() {
905
835
  return productOrder[0] || null;
906
836
  }
907
837
 
908
- /* ── the Focus queue ─────────────────────────────────────────────────────────────────────────
909
- * `/api/console?name=focus` is cross-product (the server accepts and ignores `ws`) and it runs the
910
- * board and ladder derivations for every registered workspace — a cold derive the boot path
911
- * deliberately avoids (see `lightRecord`). So it is fetched LAZILY, on entry to focus mode, never
912
- * at load.
913
- *
914
- * The server's view (lib/focus.js buildFocus) is `{queue, handoffs, delegated, upkeep, badge, …}`;
915
- * modes.js renders the fixture rows' shape `{first, waiting, syn, saw}`. The reshape happens here
916
- * — one adapter, not a second renderer — and a failed read is published as `{readError}`, which
917
- * the mode renders as a failed read, never as "a successful read of nothing".
918
- */
919
-
920
- /** One focus row in the shape the mode's queue renderer reads. The `read:` label is the caller's
921
- * to add (lib/focus.js R4) — the server's string is the content after it. */
922
- function focusRow(r) {
923
- return {
924
- n: r.rank == null ? '' : String(r.rank),
925
- slug: r.subject || '',
926
- // `productName` is the board's own key (the repo name data.js registered), which is what
927
- // `jump` needs; `product` is the server's repo-key path and matches no board.
928
- prod2: r.productName || '',
929
- why: r.why || '',
930
- read: r.read ? 'read: ' + r.read : '',
931
- };
932
- }
933
-
934
- /** The server's focus view, reshaped to what focusList renders. Every row is the payload's — no
935
- * fixture constant is reachable from here. */
936
- function focusFrom(view) {
937
- const h = view.handoffs || {};
938
- const d = view.delegated || {};
939
- const u = view.upkeep || {};
940
-
941
- const syn = [];
942
- for (const s of d.running || []) {
943
- syn.push({
944
- t: (s.name || s.id) + ' · ' + (s.word || s.shown) + (s.productName ? ' — ' + s.productName : ''),
945
- hasBtn: false,
946
- });
947
- }
948
- /* `starts` are the engine's own delegate OFFERS. They render as rows, not buttons: this surface
949
- * has no runner that can feed `initialInput` into a spawn, and a ▶ button PMOS cannot honour is
950
- * worse than no button (the server's own starts_note rule). */
951
- for (const s of d.starts || []) {
952
- syn.push({ t: (s.initiative || '') + ' — ' + (s.action || s.skill || ''), hasBtn: false });
953
- }
954
-
955
- const saw = [];
956
- if (u.memory && u.memory.heading) {
957
- saw.push({ t: u.memory.heading + ' — ' + (u.memory.body || u.memory.state || '') });
958
- }
959
- const bud = u.budgets || {};
960
- if (bud.state === 'error') {
961
- saw.push({ t: 'health budgets — read failed: ' + (bud.error || 'no reason given') });
962
- }
963
- for (const r of bud.rows || []) {
964
- saw.push({
965
- t: r.id + ' — cap ' + (r.cap || 'not declared') + ' · ' + (r.total || 0)
966
- + ' anchored initiative' + (r.total === 1 ? '' : 's'),
967
- });
968
- }
969
- // The server's caveat travels with the counts it qualifies: without it the totals above read as
970
- // the cap test, which is exactly the claim the view refuses to make.
971
- if ((bud.rows || []).length && bud.caveat) saw.push({ t: bud.caveat });
972
-
973
- return {
974
- first: (view.queue || []).map(focusRow),
975
- waiting: (h.items || []).map(focusRow),
976
- // The handoffs body for a tracker that is NOT CONNECTED — a different sentence from an empty
977
- // list, rendered by the mode in place of the calm empty line.
978
- waitingNote: (h.items || []).length ? null : (h.body || null),
979
- syn,
980
- saw,
981
- };
982
- }
983
-
984
- let focusInFlight = false;
985
-
986
- async function loadFocus() {
987
- if (FIXTURE || focusInFlight) return;
988
- focusInFlight = true;
989
- // The in-flight read is STATED. With nothing published yet, the mode would otherwise render its
990
- // calm empty line — a claim of a successful read of nothing while the read is still out. A
991
- // usable earlier payload stays on screen for the refresh instead.
992
- if (!state.focus || state.focus.readError) setState({ focus: { reading: true } });
993
- try {
994
- const view = await getJson('/api/console', { name: 'focus' });
995
- setState({
996
- focus: view && view.ok !== false
997
- ? focusFrom(view)
998
- : { readError: (view && view.error) || 'the focus queue could not be read' },
999
- });
1000
- } finally { focusInFlight = false; }
1001
- }
1002
-
1003
838
  /* ── sessions ───────────────────────────────────────────────────────────────────────────────
1004
839
  * Polled rather than pushed. The WebSocket that carries `sessions` frames belongs to panel 4's
1005
840
  * terminal module, which owns the attach/input protocol; opening a SECOND socket here to read a
@@ -1010,6 +845,22 @@ async function loadFocus() {
1010
845
  let pollTimer = null;
1011
846
  let sessionDigest = null;
1012
847
 
848
+ /* pm-os-absorb Run 1 — the plan-items signatures. An open Breakdown watches its workspace; the poll then asks for that
849
+ * workspace's `evidence` signature too, and `pmos:ledgers` tells every listener which workspaces' signatures moved. */
850
+ const ledgerWatch = new Map(); // ws id -> how many open Breakdowns watch it
851
+ let lastLedgers = null; // null until the first poll: its signatures are the baseline, not a change
852
+ export function watchLedgers(wsId) {
853
+ if (!wsId) return () => {};
854
+ ledgerWatch.set(wsId, (ledgerWatch.get(wsId) || 0) + 1);
855
+ let done = false;
856
+ return () => {
857
+ if (done) return;
858
+ done = true;
859
+ const n = (ledgerWatch.get(wsId) || 1) - 1;
860
+ if (n > 0) ledgerWatch.set(wsId, n); else ledgerWatch.delete(wsId);
861
+ };
862
+ }
863
+
1013
864
  /**
1014
865
  * WHAT THE SURFACE ACTUALLY RENDERS from a session row, as one string.
1015
866
  *
@@ -1040,7 +891,8 @@ function sessionProjection(rows) {
1040
891
 
1041
892
  async function pollSessions() {
1042
893
  if (FIXTURE) return;
1043
- const out = await getJson('/api/sessions');
894
+ const watched = [...ledgerWatch.keys()].slice(0, 4);
895
+ const out = await getJson('/api/sessions' + (watched.length ? '?ledgers=' + watched.map(encodeURIComponent).join(',') : ''));
1044
896
  if (out && Array.isArray(out.sessions)) {
1045
897
  const digest = sessionProjection(out.sessions);
1046
898
  const count = liveCount(out.sessions);
@@ -1051,6 +903,12 @@ async function pollSessions() {
1051
903
  setState({ sessions: out.sessions, runningCount: count });
1052
904
  }
1053
905
  }
906
+ if (out && out.ledgers && typeof out.ledgers === 'object' && typeof window !== 'undefined') {
907
+ const prev = lastLedgers;
908
+ const changed = Object.keys(out.ledgers).filter((ws) => prev && JSON.stringify(out.ledgers[ws]) !== JSON.stringify(prev[ws]));
909
+ lastLedgers = out.ledgers;
910
+ if (changed.length) window.dispatchEvent(new CustomEvent('pmos:ledgers', { detail: { changed, ledgers: out.ledgers } }));
911
+ }
1054
912
  schedulePoll();
1055
913
  }
1056
914
 
@@ -1102,6 +960,9 @@ export async function loadRepos() {
1102
960
  const rows = foldWorkspaces(out && out.workspaces);
1103
961
  productOrder = rows.map((r) => r.name);
1104
962
  for (const r of rows) products.set(r.name, Object.assign(products.get(r.name) || {}, r));
963
+ // The session poll does not read a board, so it does not wait for one. Behind the board loads it first answered only
964
+ // after the (now larger) initiatives surface had been fetched, and a live agent's marker was missing until then.
965
+ pollSessions();
1105
966
  await Promise.all(rows.map((r) => loadBoard(products.get(r.name))));
1106
967
  publishBoards();
1107
968
  const first = currentProductName();
@@ -1121,8 +982,6 @@ export async function loadRepos() {
1121
982
  sel,
1122
983
  });
1123
984
  if (sel) hydrate(sel);
1124
- schedulePoll();
1125
- pollSessions();
1126
985
  return productOrder.map((name) => {
1127
986
  const prod = products.get(name);
1128
987
  return {
@@ -1545,7 +1404,7 @@ export async function searchFiles(query, limit, all) {
1545
1404
  export async function saveDoc({ ws, path, base, text }) {
1546
1405
  if (FIXTURE) return { ok: true };
1547
1406
  const p = products.get(currentProductName());
1548
- return postJson('/api/doc/save', { ws: ws || state.wsId || (p ? p.ws : ''), path, base, text, pm: true });
1407
+ return postJson('/api/doc/save', { ws: ws || state.wsId || (p ? p.ws : ''), path, base, text });
1549
1408
  }
1550
1409
 
1551
1410
  /**
@@ -1636,7 +1495,7 @@ export function runningCount(s) {
1636
1495
  * newly selected initiative, and a repaint when it lands.
1637
1496
  */
1638
1497
  if (!FIXTURE) {
1639
- let lastSel = null, lastProduct = null, lastMode = null;
1498
+ let lastSel = null, lastProduct = null;
1640
1499
  subscribe(() => {
1641
1500
  if (state.product !== lastProduct) {
1642
1501
  lastProduct = state.product;
@@ -1644,12 +1503,6 @@ if (!FIXTURE) {
1644
1503
  if (p && p.ws && state.wsId !== p.ws) setState({ wsId: p.ws });
1645
1504
  if (p && !p.board) loadInitiatives(state.product);
1646
1505
  }
1647
- // The focus view is fetched on ENTRY to focus mode, and re-fetched on every re-entry — the
1648
- // queue is a statement about now, and the mode switch is the moment the PM asks for it.
1649
- if (state.mode !== lastMode) {
1650
- lastMode = state.mode;
1651
- if (state.mode === 'focus') loadFocus();
1652
- }
1653
1506
  const sel = state.sel;
1654
1507
  if (sel && sel !== lastSel) {
1655
1508
  lastSel = sel;