create-pmos 0.51.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 +5 -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 +117 -6
  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 +20 -4
  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,197 @@
1
+ 'use strict';
2
+ /* routes-timeline.js — GET /api/timeline?ws=<id>: the rows of Initiatives › Timeline (pm-os-absorb Run 4, slice 4C;
3
+ * PRD R20 A, D86 point 4).
4
+ *
5
+ * → { ok: true, ws, view: 'timeline', sig: { placements, items, records }, now, today,
6
+ * range: { start, end, clipped } | null, whole months, 'YYYY-MM-DD'; null when no initiative has a date;
7
+ * at most 24 months, `clipped` true when a date falls outside them
8
+ * horizons: { now: [row…], next: [row…], later: [row…] } } each in its placed order, first is top
9
+ *
10
+ * row = { id, title, lane, horizon, rank, placed, dormant, dated, `placed`: the reader's order holds it (a line, or
11
+ * a day-one row 2C seeds), so a `placed … after:
12
+ * <this row>` finds it
13
+ * span: { start, due } | null, the initiative's own span, over every dated item
14
+ * stages: [{ id, label, start, due, done, total }], one per stage that holds an item, in stage order, from
15
+ * planTree.layout (an unknown stage is "Other", as in the
16
+ * Breakdown); start and due are null for an undated stage
17
+ * milestones: ['YYYY-MM-DD'…], the dates of milestone items, for the ◆
18
+ * progress: { done, total } }
19
+ *
20
+ * A READ ONLY ROUTE. Nothing here writes a ledger, and no date is computed from anything but the items' own start and due
21
+ * (lib/item-spans.js, the one engine the Breakdown's Timeline and the report's strip also use). An initiative bar is never
22
+ * written: the page reorders rows through POST /api/placements (Run 2, slice 2C), and that is the only write.
23
+ *
24
+ * WORK LANES ONLY (D86 point 4). A row exists for an initiative whose lane has `group: 'work'` in scripts/lane-defs.js,
25
+ * read through the engine, never from a list in this file. A held, closed or cancelled initiative has no row.
26
+ *
27
+ * TODAY is the local date of the machine the server runs on, which is the PM's (the workspace is a local app). The range
28
+ * holds every date and today, but never more than MAX_MONTHS months: the window then starts a few months before today (or at
29
+ * the first date, if later) and a date outside it is cut off, which `range.clipped` says.
30
+ *
31
+ * HORIZON AND ORDER come from lib/placements.js readPlacements and horizonOf, so an initiative without a placement reads
32
+ * Later, as it does on the Roadmap. An initiative the ledger has not placed follows the placed ones of its horizon, in id
33
+ * order.
34
+ *
35
+ * `sig` is made of the ledgerSig of every ledger this route reads (placements, plan-items) and one signature of the
36
+ * records it lists (their names, sizes and times, and lifecycle.jsonl). The page watches it with sig-watch.js watchSig.
37
+ *
38
+ * 400 without `ws`; 404 for a workspace that is not registered (getWs would answer the home workspace for it); 405 for any
39
+ * method but GET. The workflow board is NOT read: it is capped and derived in a child process, and this route needs only
40
+ * the records, the two ledgers and the stage names.
41
+ */
42
+ const fs = require('fs');
43
+ const path = require('path');
44
+ const crypto = require('crypto');
45
+ const ledgers = require('./plan-ledgers.js');
46
+ const placements = require('./placements.js');
47
+ const planItems = require('./plan-items.js');
48
+ const planTree = require('./plan-tree.js');
49
+ const itemSpans = require('./item-spans.js');
50
+ const consoleState = require('./console-state');
51
+
52
+ const MIN_MONTHS = 3;
53
+ const MAX_MONTHS = 24;
54
+ const LEAD_MONTHS = 6; // a clipped window opens this many months before today
55
+ const OTHER_ID = 'other';
56
+ const OTHER_LABEL = 'Other'; // the Breakdown's word for items whose stage the lane does not have (breakdown-copy.js)
57
+ const pad = (n) => String(n).padStart(2, '0');
58
+ const ymd = (d) => `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
59
+ const localYmd = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
60
+
61
+ /** The whole months that hold every date and today: the first of the first month to the last day of the last. More than
62
+ * MAX_MONTHS of them are cut to a window that holds today and opens LEAD_MONTHS before it (or at the first date). */
63
+ function rangeOf(dates, today) {
64
+ const all = dates.concat([today]).sort();
65
+ const a = new Date(all[0] + 'T00:00:00Z');
66
+ const b = new Date(all[all.length - 1] + 'T00:00:00Z');
67
+ const t = new Date(today + 'T00:00:00Z');
68
+ const idx = (d) => d.getUTCFullYear() * 12 + d.getUTCMonth();
69
+ let first = idx(a);
70
+ let months = idx(b) - first + 1;
71
+ if (months > MAX_MONTHS) {
72
+ first = Math.min(Math.max(first, idx(t) - LEAD_MONTHS), idx(t));
73
+ months = MAX_MONTHS;
74
+ } else if (months < MIN_MONTHS) months = MIN_MONTHS;
75
+ const start = new Date(Date.UTC(Math.floor(first / 12), first % 12, 1));
76
+ const last = new Date(Date.UTC(Math.floor(first / 12), (first % 12) + months, 0));
77
+ const out = { start: ymd(start), end: ymd(last) };
78
+ out.clipped = all.some((d) => d < out.start || d > out.end);
79
+ return out;
80
+ }
81
+
82
+ /** One signature for the records this route lists: their names, sizes and times, and the lifecycle ledger. */
83
+ function recordsSig(root, engine) {
84
+ const L = engine.layout(root);
85
+ const h = crypto.createHash('sha1');
86
+ let names = [];
87
+ try { names = fs.readdirSync(L.initiatives).filter((n) => n.endsWith('.md')).sort(); } catch { /* no records */ }
88
+ for (const n of names) {
89
+ try { const st = fs.statSync(path.join(L.initiatives, n)); h.update(`${n}:${st.size}:${st.mtimeMs};`); } catch { /* gone mid-scan */ }
90
+ }
91
+ try { const st = fs.statSync(path.join(L.runsDir, 'lifecycle.jsonl')); h.update(`lc:${st.size}:${st.mtimeMs}:${st.ino}`); } catch { /* none */ }
92
+ return h.digest('hex');
93
+ }
94
+
95
+ /** The initiatives that get a row: [{ id, title, lane }] in id order. */
96
+ function workInitiatives(root, engine) {
97
+ const { profile, defs } = engine.lanesFor(root);
98
+ const fallback = profile === 'pm-artifacts' ? 'prd' : 'feature';
99
+ const stopped = engine.readLifecycle(root);
100
+ const out = [];
101
+ for (const id of engine.listInitiatives(root)) {
102
+ const file = planTree.recordPath(root, id);
103
+ if (!file) continue;
104
+ let fm = {};
105
+ try { fm = engine.parseLeadingFrontmatter(fs.readFileSync(file, 'utf8')) || {}; } catch { fm = {}; }
106
+ const declared = String(fm.lane || '').trim();
107
+ const lane = declared && defs[declared] ? declared : fallback;
108
+ if (!defs[lane] || defs[lane].group !== 'work') continue;
109
+ const lc = stopped.get(id);
110
+ if (lc && lc.state !== 'active') continue;
111
+ out.push({ id, title: String(fm.title || id), lane });
112
+ }
113
+ return out;
114
+ }
115
+
116
+ /** One initiative's row, without its horizon. `live` is its items from the plan-items fold, proposed ones left out. The
117
+ * stage rows come from planTree.layout, the Breakdown's own layout: an item whose stage the lane does not know is in
118
+ * "Other", as there, so the rows' totals add up to the row's progress. */
119
+ function rowOf(root, init, fold, live, now) {
120
+ const lay = planTree.layout({ root, initiative: init.id, fold, ladder: null, now });
121
+ const spans = itemSpans.spansOf(live, { stages: lay.stages.map((s) => s.id) });
122
+ const byItem = new Map(spans.map((s) => [s.id, s]));
123
+ const groups = lay.stages.map((s) => ({ id: s.id, label: s.label, ids: new Set(s.items) }));
124
+ if (lay.other.items.length) groups.push({ id: OTHER_ID, label: OTHER_LABEL, ids: new Set(lay.other.items) });
125
+ const stageRows = [];
126
+ for (const g of groups) {
127
+ const mine = live.filter((it) => g.ids.has(it.id));
128
+ if (!mine.length) continue;
129
+ const dated = mine.map((it) => byItem.get(it.id)).filter((sp) => sp && !sp.milestone);
130
+ stageRows.push({
131
+ id: g.id, label: g.label,
132
+ start: dated.length ? dated.reduce((m, sp) => (sp.start < m ? sp.start : m), dated[0].start) : null,
133
+ due: dated.length ? dated.reduce((m, sp) => (sp.due > m ? sp.due : m), dated[0].due) : null,
134
+ done: mine.filter((it) => it.state === 'done').length, total: mine.length,
135
+ });
136
+ }
137
+ const span = itemSpans.initiativeSpan(spans);
138
+ return {
139
+ id: init.id, title: init.title, lane: init.lane, span, dated: !!span, stages: stageRows,
140
+ milestones: spans.filter((sp) => sp.milestone).map((sp) => sp.due).sort(),
141
+ progress: { done: lay.progress.done, total: lay.progress.total },
142
+ };
143
+ }
144
+
145
+ function build(w, now) {
146
+ const root = w.root;
147
+ const loaded = consoleState.loadEngine();
148
+ if (!loaded.ok) return { ok: false, status: 500, error: loaded.error };
149
+ const engine = loaded.engine;
150
+ const sig = { placements: ledgers.ledgerSig(root, 'placements'), items: ledgers.ledgerSig(root, 'plan-items'), records: recordsSig(root, engine) };
151
+ const inits = workInitiatives(root, engine);
152
+ const fold = planItems.readFold(root, { now });
153
+ const placed = placements.readPlacements(root, { now, records: inits.map((i) => ({ id: i.id, lane: i.lane })) });
154
+ const rows = new Map();
155
+ for (const init of inits) {
156
+ const live = Object.values(fold.items).filter((it) => it.initiative === init.id && !it.proposed);
157
+ const row = rowOf(root, init, fold, live, now);
158
+ const p = placed.byInit[init.id] || null;
159
+ row.horizon = placements.horizonOf(placed, init.id);
160
+ row.dormant = !!(p && p.dormant);
161
+ row.placed = ((placed.order && placed.order[row.horizon]) || []).includes(init.id);
162
+ rows.set(init.id, row);
163
+ }
164
+ const horizons = { now: [], next: [], later: [] };
165
+ for (const h of placements.HORIZONS) {
166
+ const seen = new Set();
167
+ for (const id of (placed.order && placed.order[h]) || []) {
168
+ const row = rows.get(id);
169
+ if (!row || row.horizon !== h || seen.has(id)) continue;
170
+ seen.add(id); horizons[h].push(row);
171
+ }
172
+ for (const row of rows.values()) if (row.horizon === h && !seen.has(row.id)) horizons[h].push(row);
173
+ horizons[h].forEach((row, i) => { row.rank = i; });
174
+ }
175
+ const dates = [];
176
+ for (const row of rows.values()) if (row.span) dates.push(row.span.start, row.span.due);
177
+ const today = localYmd(new Date(now));
178
+ return { ok: true, ws: w.id, view: 'timeline', sig, now: new Date(now).toISOString(), today, range: dates.length ? rangeOf(dates, today) : null, horizons };
179
+ }
180
+
181
+ module.exports = {
182
+ prefixes: ['/api/timeline'],
183
+ handle: async (req, res, url, ctx) => {
184
+ if (url.pathname !== '/api/timeline') return false;
185
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET /api/timeline only reads; the page reorders rows through /api/placements' }, 405); return true; }
186
+ const id = String(url.searchParams.get('ws') || '');
187
+ if (!id) { ctx.json(res, { ok: false, error: 'ws is required: the id of a registered workspace' }, 400); return true; }
188
+ const w = ctx.getWs(id);
189
+ if (!w || w.id !== id || !w.root) { ctx.json(res, { ok: false, error: 'no such workspace' }, 404); return true; }
190
+ let out;
191
+ try { out = build(w, Date.now()); }
192
+ catch (e) { out = { ok: false, status: 500, error: 'the timeline could not be read: ' + String((e && e.message) || 'error').slice(0, 160) }; }
193
+ ctx.json(res, out, out.ok ? 200 : out.status || 500);
194
+ return true;
195
+ },
196
+ _rangeOf: rangeOf,
197
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ /* routes-today.js — GET /api/today?ws=<id> (pm-os-absorb Run 3, slice 3B, PRD R12 and R24).
3
+ *
4
+ * The ranked list of what needs the PM, the same list `pmos today` prints and Today draws (lib/today.js). Read-only:
5
+ * it answers a GET and writes nothing, so it takes no press (seams §5.1). A workspace id the server does not know reads
6
+ * as the home workspace, the same as GET /api/needs.
7
+ *
8
+ * { ok, ws, count, cards: [{ id, run_id, ws, kind 1..8, source, row_kind, initiative_id, cause, evidence,
9
+ * act: { label, cli? }, detail? }], banner: [{ source, error }], next, stall_days, sig }
10
+ *
11
+ * `banner` holds the sources that could not be read. An unreadable source is said here and never turned into an empty
12
+ * list. `sig` changes whenever the list changes.
13
+ */
14
+ const path = require('path');
15
+ const today = require('./today.js');
16
+
17
+ module.exports = {
18
+ prefixes: ['/api/today'],
19
+ handle: async (req, res, url, ctx) => {
20
+ if (url.pathname !== '/api/today') return false;
21
+ if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
22
+ const w = ctx.getWs(url.searchParams.get('ws'));
23
+ if (!w) { ctx.json(res, { ok: false, error: 'no workspace is registered' }, 404); return true; }
24
+ let sessions = [];
25
+ try { sessions = ctx.sessionList(); } catch { /* a session list that cannot be read adds no session card */ }
26
+ // ctx.getWs answers the home workspace for an id it does not hold, so a root counts as registered only when the answer is that root
27
+ const registered = (root) => { const x = ctx.getWs(today.wsIdOf(root)); return !!x && path.resolve(x.root) === path.resolve(root); };
28
+ const out = today.todayFor(w, { stateDir: ctx.stateDirFor(w), sessions, registered });
29
+ ctx.json(res, out, out.ok ? 200 : 500);
30
+ return true;
31
+ },
32
+ };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+ /* today-cli.js — `pmos today`: what needs you, ranked as Today shows it (pm-os-absorb Run 3, slice 3B, PRD R24).
3
+ *
4
+ * pmos today [--ws <id>] [--json]
5
+ * pmos today --root <dir> [--json]
6
+ *
7
+ * The same list as GET /api/today (lib/today.js). With a workspace server running it asks the server, so a waiting
8
+ * session is in the list. With none, it reads the repository itself (--root, or the git root of the current directory) and
9
+ * says that sessions were not read. It writes nothing. Exit codes: 0 done (an empty list is done), 1 the list could not be
10
+ * read, 2 usage.
11
+ */
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+ const today = require('./today.js');
16
+ const { parseArgs } = require('./schedule-cli');
17
+
18
+ const COMMANDS = { today: [] };
19
+ const USAGE = [
20
+ 'usage: pmos today [--ws <id>] [--json]',
21
+ ' pmos today --root <dir> [--json] [--wait]',
22
+ 'Prints what needs you, ranked as Today shows it: the kind first (1 a gate is owed, 2 a session is waiting, 3 a step was',
23
+ 'returned to you, 4 work to check, 5 a breakdown to keep, 6 a capture, 7 a cycle review, 8 needs repair), then the card.',
24
+ 'With a workspace server running it asks the server. Without one it reads the repository and does not read sessions.',
25
+ 'Without a server the Cycle review and missing-anchor cards need the initiative records read, which takes time: they show',
26
+ 'as pending. Add --wait to read them now (about a quarter of a second per record).',
27
+ ];
28
+ const BOOL = new Set(['json', 'help', 'wait']);
29
+ const VALUE = new Set(['ws', 'root']);
30
+
31
+ function isDir(d) { try { return fs.statSync(d).isDirectory(); } catch { return false; } }
32
+
33
+ function repoRoot(cwd) {
34
+ try { return execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || cwd; }
35
+ catch { return cwd; }
36
+ }
37
+
38
+ function print(say, out, { sessions }) {
39
+ say(`${out.count} ${out.count === 1 ? 'needs' : 'need'} you`);
40
+ for (const c of out.cards) {
41
+ say(`${c.kind} ${c.cause}${c.evidence ? ' — ' + c.evidence : ''}`);
42
+ say(` ${c.act.label}${c.act.cli ? ' · ' + c.act.cli : ''}${c.initiative_id ? ' · ' + c.initiative_id : ''}`);
43
+ }
44
+ for (const b of out.banner || []) say(`! ${b.source}: ${b.error}`);
45
+ if (!out.count) say(out.next ? `Nothing needs you. Next: ${out.next.label}.` : 'Nothing needs you.');
46
+ if (!sessions) say('No server is running, so waiting sessions were not read.');
47
+ }
48
+
49
+ async function run(argv, { say, call }) {
50
+ const rest = []; const bools = {};
51
+ for (const a of argv.slice(1)) {
52
+ if (a.startsWith('--') && BOOL.has(a.slice(2))) bools[a.slice(2)] = true;
53
+ else if (a === '-h') bools.help = true;
54
+ else rest.push(a);
55
+ }
56
+ const { pos, flags } = parseArgs(rest);
57
+ const f = { ...flags, ...bools };
58
+ if (f.help) { USAGE.forEach((l) => say(l)); return 0; }
59
+ const bad = Object.keys(f).find((k) => !BOOL.has(k) && !VALUE.has(k));
60
+ if (bad || pos.length) { say(`✗ ${bad ? '--' + bad : pos[0]} is not an option of pmos today`); USAGE.forEach((l) => say(l)); return 2; }
61
+ const empty = ['ws', 'root'].find((k) => k in f && (typeof f[k] !== 'string' || !f[k]));
62
+ if (empty) { say(`✗ --${empty} needs a value`); USAGE.forEach((l) => say(l)); return 2; }
63
+ if (f.ws && f.root) { say('✗ --ws and --root name two different things: use one'); USAGE.forEach((l) => say(l)); return 2; }
64
+ const root = path.resolve(f.root ? f.root : repoRoot(process.cwd()));
65
+ if (f.root && !isDir(root)) { say(`✗ ${root} is not a directory`); return 1; }
66
+ let out = null;
67
+ let sessions = false;
68
+ if (!f.root) {
69
+ // ask the server about the repository of this directory, unless --ws names one
70
+ let ws = f.ws || null;
71
+ if (!ws) {
72
+ const list = await call('GET', '/api/workspaces');
73
+ const mine = list && Array.isArray(list.workspaces) ? list.workspaces.find((x) => x && x.root && path.resolve(x.root) === root) : null;
74
+ ws = mine ? mine.id : null;
75
+ }
76
+ if (ws) { out = await call('GET', '/api/today?ws=' + encodeURIComponent(ws)); sessions = !!(out && out.ok); }
77
+ if (f.ws) {
78
+ // a named workspace is never swapped for a local read; the server answers the home workspace for an id it does not hold
79
+ if (!out) { say(`✗ the workspace ${f.ws} could not be reached: no workspace server answered. Start it, or give --root <dir>.`); return 1; }
80
+ if (!out.ok) { say('✗ ' + out.error); return 1; }
81
+ if (out.ws !== f.ws) { say(`✗ no workspace has the id ${f.ws}`); return 1; }
82
+ }
83
+ }
84
+ if (!out || !out.ok) {
85
+ // a local read: nothing in this process would read a background derivation, so none starts; --wait derives now
86
+ const rowsLib = require('./initiative-rows.js');
87
+ const was = rowsLib.setBackground(false);
88
+ try {
89
+ if (f.wait) rowsLib.deriveNow(root);
90
+ out = today.todayFor({ id: 'local', root }, {});
91
+ } finally { rowsLib.setBackground(was); }
92
+ }
93
+ if (!out.ok) { say('✗ ' + out.error); return 1; }
94
+ if (f.json) say(JSON.stringify(out));
95
+ else print(say, out, { sessions });
96
+ return 0;
97
+ }
98
+
99
+ module.exports = { run, COMMANDS, USAGE };