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,189 @@
1
+ 'use strict';
2
+ /* measurements.js — THE ONE WRITER and THE ONE FOLD of measurements.jsonl: a measured value of a KR or a health
3
+ * budget, with its target, its date and its source (pm-os-absorb, PRD R17, R23, R24; D86).
4
+ *
5
+ * readMeasurements(root) → {
6
+ * ok: true,
7
+ * byAnchor: { [anchor]: [{ value, target, unit, as_of, source, by, at }] }, oldest first; `source` is "path@sha"
8
+ * flags: [{ kind, eid, why }],
9
+ * }
10
+ * latest(result, anchor) → the newest measurement of `anchor` by as_of, or null
11
+ * appendFromUi(root, events, { actor, now }) → { ok, written, lines } | { ok: false, status, error, index }
12
+ * appendFromCli(root, events, { actor, now }) → the same
13
+ * checkEvent(ev) → null | error text the caller-side checks, shared with `pmos measure`
14
+ *
15
+ * `anchor` is the initiative's parent_kr as written (a KR id, or `health:<id>`, D59). A measurement needs a source; an
16
+ * agent may write one (it is evidence, not a PM act), so the actor comes from actor.js, never from the caller.
17
+ * The workspace never computes a value (PRD Non-goals): a group shows a value only from a line here.
18
+ *
19
+ * THE `measured` LINE (the shared contract):
20
+ * { v: 1, eid, ev: 'measured', anchor, value: <number>, target: <number> | null, unit?: '%'|…, as_of: 'YYYY-MM-DD',
21
+ * source: '<path>@<sha>', by, via, at }
22
+ * pmOnly(rec) → false: a measurement is evidence, not a PM act.
23
+ *
24
+ * `source` is a repository-relative path, `@`, and a commit sha of 7 to 40 hex characters: the file the number was
25
+ * read from, at the commit it was read at. The writer does not open it (no read outside the call's own checks), so a
26
+ * wrong path is the writer's caller's error; scripts/check-measurements.js reports a path that is not in the tree.
27
+ *
28
+ * The fold is order-free, like lib/plan-items.js: byte-identical duplicates count once, one eid with two contents keeps
29
+ * the first. The ledger is one per repository: lib/plan-ledgers.js ledgerPath(root, 'measurements'). Nothing is cached.
30
+ */
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const crypto = require('crypto');
34
+ const ledgers = require('./plan-ledgers.js');
35
+ const actorLib = require('./actor.js');
36
+ const { createSinkGuard } = require('./sink.js');
37
+
38
+ const MAX_LINES = 50;
39
+ const EID_RE = /^[0-9a-z]{12}$/;
40
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
41
+ const AT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
42
+ /** A KR id as the OKR files write it, or a health budget or a domain (D59, D67). One line, no control characters. */
43
+ const ANCHOR_RE = /^(?:health:[a-z0-9][a-z0-9-]{0,59}|domain:[a-z0-9][a-z0-9-]{0,59}|[A-Za-z0-9][A-Za-z0-9 .:_-]{0,79})$/;
44
+ const SOURCE_RE = /^([A-Za-z0-9_][A-Za-z0-9_./-]{0,239})@([0-9a-f]{7,40})$/;
45
+ const UNIT_RE = /^[^\s\x00-\x1f]{1,12}$/;
46
+ const WRITER_SET = Object.freeze(['v', 'eid', 'by', 'via', 'at', 'override', 'pm', 'actor', 'nonce']);
47
+ const CALLER_KEYS = Object.freeze(['anchor', 'value', 'target', 'unit', 'as_of', 'source']);
48
+ const STORED_KEYS = Object.freeze(['v', 'eid', 'ev', 'by', 'via', 'at', ...CALLER_KEYS]);
49
+ const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
50
+
51
+ const isStr = (v) => typeof v === 'string';
52
+ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
53
+ const dateOk = (v) => isStr(v) && DATE_RE.test(v) && !Number.isNaN(Date.parse(v + 'T00:00:00Z'))
54
+ && new Date(v + 'T00:00:00Z').toISOString().slice(0, 10) === v;
55
+
56
+ function newEventId() {
57
+ let out = '';
58
+ while (out.length < 12) {
59
+ for (const b of crypto.randomBytes(24)) {
60
+ if (b >= 252) continue;
61
+ out += ALPHABET[b % 36];
62
+ if (out.length === 12) break;
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /** The source as { path, sha }, or null when it is not "<repository-relative path>@<sha>". */
69
+ function parseSource(s) {
70
+ const m = isStr(s) ? SOURCE_RE.exec(s) : null;
71
+ if (!m) return null;
72
+ if (m[1].split('/').some((seg) => seg === '..' || seg === '.' || seg === '')) return null;
73
+ return { path: m[1], sha: m[2] };
74
+ }
75
+
76
+ /** The checks on what a caller sends (the writer runs them; `pmos measure` runs them before it writes). */
77
+ function checkEvent(ev) {
78
+ if (!ev || typeof ev !== 'object' || Array.isArray(ev)) return 'each line must be a JSON object';
79
+ const sent = WRITER_SET.find((k) => has(ev, k));
80
+ if (sent) return `the writer sets "${sent}"; do not send it`;
81
+ if (ev.ev !== 'measured') return 'ev must be measured';
82
+ const extra = Object.keys(ev).filter((k) => k !== 'ev' && !CALLER_KEYS.includes(k));
83
+ if (extra.length) return `a measured line has no field ${extra.join(', ')}`;
84
+ if (!isStr(ev.anchor) || !ANCHOR_RE.test(ev.anchor.trim()) || ev.anchor !== ev.anchor.trim()) return 'anchor must be a KR id, health:<id> or domain:<id>';
85
+ if (typeof ev.value !== 'number' || !Number.isFinite(ev.value)) return 'value must be a number';
86
+ if (has(ev, 'target') && ev.target !== null && (typeof ev.target !== 'number' || !Number.isFinite(ev.target))) return 'target must be a number, or null';
87
+ if (has(ev, 'unit') && !(isStr(ev.unit) && UNIT_RE.test(ev.unit))) return 'unit must be a short word with no spaces, like %';
88
+ if (!dateOk(ev.as_of)) return 'as_of must be YYYY-MM-DD';
89
+ if (!has(ev, 'source') || ev.source === '' || ev.source == null) return 'a measurement needs a source: <path>@<commit sha>';
90
+ if (!parseSource(ev.source)) return 'source must be <repository-relative path>@<commit sha of 7 to 40 hex characters>';
91
+ return null;
92
+ }
93
+
94
+ /** Why a stored line is not a valid line of this ledger, or null. */
95
+ function lineErr(r) {
96
+ if (r.v !== 1) return 'v must be 1';
97
+ if (!isStr(r.eid) || !EID_RE.test(r.eid)) return 'eid must be 12 characters of [0-9a-z]';
98
+ if (!isStr(r.by) || !actorLib.BY_RE.test(r.by)) return 'by must be pm:<slug>, agent:unknown or agent:<session>';
99
+ if (!actorLib.VIA.includes(r.via)) return 'via must be ui or cli';
100
+ if (!isStr(r.at) || !AT_RE.test(r.at)) return 'at must be an ISO UTC time';
101
+ const extra = Object.keys(r).filter((k) => !STORED_KEYS.includes(k));
102
+ if (extra.length) return `a measured line has no field ${extra.join(', ')}`;
103
+ const body = {};
104
+ for (const k of ['ev', ...CALLER_KEYS]) if (has(r, k)) body[k] = r[k];
105
+ return checkEvent(body);
106
+ }
107
+
108
+ function readMeasurements(root) {
109
+ const out = { ok: true, byAnchor: {}, flags: [] };
110
+ const { lines, bad } = ledgers.readLines(root, 'measurements');
111
+ for (const b of bad) out.flags.push({ kind: 'unreadable', eid: null, why: `line ${b.n}: ${b.why}` });
112
+ const kept = [];
113
+ for (const { n, rec } of lines) {
114
+ if (rec.ev !== 'measured') { out.flags.push({ kind: 'invalid', eid: isStr(rec.eid) ? rec.eid : null, why: `line ${n}: ev must be measured` }); continue; }
115
+ const why = lineErr(rec);
116
+ if (why) { out.flags.push({ kind: 'invalid', eid: isStr(rec.eid) ? rec.eid : null, why: `line ${n}: ${why}` }); continue; }
117
+ kept.push({ n, rec });
118
+ }
119
+ kept.sort((a, b) => (Date.parse(a.rec.at) - Date.parse(b.rec.at)) || (a.rec.eid < b.rec.eid ? -1 : a.rec.eid > b.rec.eid ? 1 : 0) || (a.n - b.n));
120
+ const seen = new Map();
121
+ for (const { n, rec } of kept) {
122
+ const text = JSON.stringify(rec);
123
+ if (seen.has(rec.eid)) {
124
+ if (seen.get(rec.eid) !== text) out.flags.push({ kind: 'duplicate-event', eid: rec.eid, why: `line ${n}: the eid ${rec.eid} is used by two different lines; the first one counts` });
125
+ continue;
126
+ }
127
+ seen.set(rec.eid, text);
128
+ (out.byAnchor[rec.anchor] = out.byAnchor[rec.anchor] || []).push({
129
+ value: rec.value, target: Number.isFinite(rec.target) ? rec.target : null, unit: rec.unit || null,
130
+ as_of: rec.as_of, source: rec.source, by: rec.by, at: rec.at,
131
+ });
132
+ }
133
+ for (const rows of Object.values(out.byAnchor)) rows.sort((a, b) => String(a.as_of).localeCompare(String(b.as_of)) || String(a.at).localeCompare(String(b.at)));
134
+ return out;
135
+ }
136
+
137
+ const pmOnly = () => false;
138
+
139
+ function latest(result, anchor) {
140
+ const rows = result && result.byAnchor && Object.prototype.hasOwnProperty.call(result.byAnchor, anchor) ? result.byAnchor[anchor] : null;
141
+ if (!Array.isArray(rows) || !rows.length) return null;
142
+ return rows.reduce((a, b) => (String(b.as_of) >= String(a.as_of) ? b : a));
143
+ }
144
+
145
+ const refuse = (status, error, index) => ({ ok: false, status, error, index: index == null ? null : index });
146
+
147
+ function append(root, events, actor, { now } = {}) {
148
+ if (!actorLib.valid(actor)) return refuse(500, 'measurements: the caller must pass the actor from pm-guard.js or actor.js', null);
149
+ if (!Array.isArray(events) || !events.length) return refuse(400, 'send at least one line', null);
150
+ if (events.length > MAX_LINES) return refuse(413, `send at most ${MAX_LINES} lines at once`, null);
151
+ for (let i = 0; i < events.length; i++) {
152
+ const why = checkEvent(events[i]);
153
+ if (why) return refuse(400, why, i);
154
+ }
155
+ const read = ledgers.readLines(root, 'measurements');
156
+ if (read.bad.some((b) => b.n === 0)) return refuse(500, 'could not read the measurements ledger — nothing was saved: ' + read.bad[0].why.slice(0, 160), null);
157
+ const clock = now == null ? Date.now() : now;
158
+ let last = 0;
159
+ for (const l of read.lines) {
160
+ const t = isStr(l.rec.at) ? Date.parse(l.rec.at) : NaN;
161
+ if (Number.isFinite(t) && t <= clock + 1000 && t > last) last = t;
162
+ }
163
+ const recs = events.map((ev) => {
164
+ last = Math.max(clock, last + 1);
165
+ const rec = { v: 1, eid: newEventId(), ev: 'measured', anchor: ev.anchor, value: ev.value, target: has(ev, 'target') ? ev.target : null };
166
+ if (has(ev, 'unit')) rec.unit = ev.unit;
167
+ Object.assign(rec, { as_of: ev.as_of, source: ev.source, by: actor.by, via: actor.via, at: new Date(last).toISOString() });
168
+ return rec;
169
+ });
170
+ let file;
171
+ try { file = ledgers.ledgerPath(root, 'measurements'); fs.mkdirSync(path.dirname(file), { recursive: true }); }
172
+ catch { return refuse(500, 'could not record — nothing was saved', null); }
173
+ const guard = createSinkGuard({ sink: file, what: 'measurements' });
174
+ let wrote = 0;
175
+ for (const rec of recs) {
176
+ try { guard.appendLine(file, rec); wrote++; }
177
+ catch {
178
+ return refuse(500, wrote ? `could not record — ${wrote} of ${recs.length} lines were saved before the write failed` : 'could not record — nothing was saved', wrote);
179
+ }
180
+ }
181
+ return { ok: true, written: recs.length, lines: recs.map((r) => ({ eid: r.eid, anchor: r.anchor })) };
182
+ }
183
+
184
+ /** A page press (requirePagePress's actor). */
185
+ function appendFromUi(root, events, { actor, now } = {}) { return append(root, events, actor, { now }); }
186
+ /** `pmos measure` (actor.fromCli's actor; an agent may write a measurement). */
187
+ function appendFromCli(root, events, { actor, now } = {}) { return append(root, events, actor, { now }); }
188
+
189
+ module.exports = { MAX_LINES, readMeasurements, latest, pmOnly, checkEvent, parseSource, lineErr, appendFromUi, appendFromCli };
@@ -17,13 +17,14 @@
17
17
  *
18
18
  * The screen model:
19
19
  * { dest, kind, mode, id, dk, title, column }
20
- * dest one of DESTS — which destination this screen belongs to
21
- * kind 'list' | 'detail' | 'terminal' | 'doc' | 'more'
20
+ * dest one of DESTS — which tab this screen belongs to
21
+ * kind 'dest' | 'agent' (the frame) · 'list' | 'more' (the old shell) · 'detail' | 'terminal' | 'doc'
22
22
  * mode a MODES key, for kind 'list'
23
- * id the detail id / session id / viewer id
23
+ * id the frame destination (kind 'dest') / detail id / session id / viewer id
24
24
  * dk the DETAIL kind, for kind 'detail'
25
25
  * title only for 'terminal' and 'doc', where the name is not derivable
26
- * column 'side' | 'term' | 'rail' — determined by `kind` and by nothing else
26
+ * column 'side' | 'term' | 'rail' — determined by `kind` and by nothing else (panelOf is the same for the four
27
+ * panel grid)
27
28
  *
28
29
  * `column` is the whole of the exclusivity guarantee: the stylesheet shows exactly one of the three
29
30
  * column nodes per value, so P1 is a property of the CSS rather than something this reducer has to
@@ -65,87 +66,90 @@ const DETAIL_TITLE = {
65
66
  addprod: 'add repository', new: 'file new',
66
67
  };
67
68
 
68
- const MOBILE_DESTS = ['board', 'scheduled', 'agents', 'memory', 'more'];
69
- // Every MODES key has an entry. The suite ITERATES `MODES` against this map rather than a literal
70
- // list, so adding a mode without giving it a phone path fails the suite instead of going silently
71
- // unreachable — which is the failure P2 exists to catch.
72
- const MOBILE_DEST = { product: 'board', scheduled: 'scheduled', agent: 'agents', memory: 'memory',
73
- workflow: 'more', skills: 'more', knowledge: 'more' };
69
+ /* ── the frame's five phone tabs (pm-os-absorb Run 3, slice 3A; PRD R15) ─────────────────────────────────────────
70
+ * Today · Initiatives · Board · Reports · Agent. The first four are the frame's destinations (a `dest` screen, drawn
71
+ * in the frame's place, panel index 0); Agent is the dock (an `agent` screen, panel 4, the live terminals). Settings
72
+ * is not a tab: it is pushed on top of the current tab as a `dest` screen with id `settings`, so Back returns.
73
+ * The old cockpit's list modes still map to a tab (MOBILE_DEST), for the old shell (fixture mode, ?layout=legacy). */
74
+ const MOBILE_DESTS = ['today', 'initiatives', 'board', 'reports', 'agent'];
75
+ // What each tab and Settings is called. The same words as the desktop bar (public/pm/frame-copy.js COPY.nav), which
76
+ // MOBNAV-P7 holds equal.
77
+ const DEST_TITLE = { today: 'Today', initiatives: 'Initiatives', board: 'Board', reports: 'Reports', agent: 'Agent', settings: 'Settings' };
78
+ // Every old MODES key has a tab. The suite ITERATES this map, so a mode without a phone path fails.
79
+ const MOBILE_DEST = { product: 'initiatives', scheduled: 'reports', agent: 'agent', memory: 'initiatives',
80
+ workflow: 'initiatives', skills: 'initiatives', knowledge: 'initiatives' };
74
81
  const MOBILE_ROOT = {
75
- board: { kind: 'list', mode: 'product' },
76
- scheduled: { kind: 'list', mode: 'scheduled' },
77
- agents: { kind: 'list', mode: 'agent' },
78
- memory: { kind: 'list', mode: 'memory' },
79
- more: { kind: 'more', mode: null },
82
+ today: { kind: 'dest', id: 'today' },
83
+ initiatives: { kind: 'dest', id: 'initiatives' },
84
+ board: { kind: 'dest', id: 'board' },
85
+ reports: { kind: 'dest', id: 'reports' },
86
+ agent: { kind: 'agent', id: null },
80
87
  };
88
+ // The tab every stored value of the retired five-tab nav (board · scheduled · agents · memory · more) and of the
89
+ // three-way switcher before it lands on. Read once from the old key; a new value is never passed through it.
90
+ const LEGACY_DEST = { board: 'initiatives', scheduled: 'reports', agents: 'agent', memory: 'today', more: 'today',
91
+ sessions: 'agent', terminal: 'agent', library: 'today' };
81
92
  // kind → column. There is no second input, and no screen may carry a column its kind does not give.
82
- const MOBILE_COLUMN = { list: 'side', more: 'side', detail: 'rail', terminal: 'term', doc: 'term' };
93
+ const MOBILE_COLUMN = { list: 'side', more: 'side', dest: 'side', detail: 'rail', terminal: 'term', doc: 'term', agent: 'term' };
83
94
  function mobColumnOf(kind) { return MOBILE_COLUMN[kind] || 'side'; }
84
95
 
85
- /* kind → PANEL INDEX, for the four-panel surface (public/pm/).
86
- *
87
- * WHY THIS EXISTS ALONGSIDE `column`, RATHER THAN REPLACING IT. `column` names one of the three
88
- * columns of the retired `public/app.js` layout, and MOBNAV-P1 pins it to exactly those three
89
- * values. The surface that ships now has FOUR panels — repositories · mode list · detail · tabs —
90
- * and two of the old `side` screens belong to DIFFERENT panels there: a `list` screen is panel 2
91
- * (the mode list) while a `more` screen is panel 1, which is the structure/settings panel and is
92
- * therefore the phone's "more" destination in the four-panel design.
93
- *
94
- * So this is an ADDITIVE widening, not an edit: `columnOf` is untouched and still describes the
95
- * three-column layout truthfully for the surface that owns it, and `panelOf` describes the
96
- * four-panel one. Both are pure functions of `kind` alone, so the exclusivity guarantee — one
97
- * visible surface per screen — is the same property stated against a different layout. Putting it
98
- * HERE rather than in pm/mobile.js keeps a single node-walkable source of truth for "which surface
99
- * does this screen show", which is the whole reason the reducer was pulled out of app.js.
100
- */
101
- const MOBILE_PANEL = { more: 0, list: 1, detail: 2, terminal: 3, doc: 3 };
96
+ /* kind → PANEL INDEX: which child of the body grid a phone shows (pm/mobile.css `data-mpanel`). In the frame the body
97
+ * grid holds the place, the dock's rail (never shown on a phone), the detail and the dock, in that order; in the old
98
+ * shell it holds its four panels. `dest` is the frame's place, `agent` the dock. A pure function of `kind` alone, so
99
+ * one visible surface per screen is a property of the CSS. */
100
+ const MOBILE_PANEL = { dest: 0, more: 0, list: 1, detail: 2, terminal: 3, doc: 3, agent: 3 };
102
101
  function mobPanelOf(kind) {
103
- return Object.prototype.hasOwnProperty.call(MOBILE_PANEL, kind) ? MOBILE_PANEL[kind] : 1;
102
+ return Object.prototype.hasOwnProperty.call(MOBILE_PANEL, kind) ? MOBILE_PANEL[kind] : 0;
104
103
  }
105
104
  function mobDestForMode(k) { return MOBILE_DEST[k] || null; }
106
105
  // Complete a partial screen. The column is derived, never passed in, so a caller cannot put a
107
106
  // detail on the terminal column by writing it down.
108
107
  function mobScreen(s) {
109
- const kind = (s && s.kind) || 'list';
110
- return { dest: (s && s.dest) || 'board', kind, mode: (s && s.mode) || null,
108
+ const kind = (s && s.kind) || 'dest';
109
+ return { dest: (s && s.dest) || 'today', kind, mode: (s && s.mode) || null,
111
110
  id: (s && s.id) || null, dk: (s && s.dk) || null, title: (s && s.title) || null,
112
111
  column: mobColumnOf(kind) };
113
112
  }
114
113
  function mobRootOf(dest) {
115
- const r = MOBILE_ROOT[dest] || MOBILE_ROOT.board;
116
- return mobScreen({ dest: MOBILE_ROOT[dest] ? dest : 'board', kind: r.kind, mode: r.mode });
114
+ const d = MOBILE_ROOT[dest] ? dest : 'today';
115
+ const r = MOBILE_ROOT[d];
116
+ return mobScreen({ dest: d, kind: r.kind, id: r.id });
117
117
  }
118
- /* The title map. `list` and `more` derive; `detail` reads DETAIL_TITLE (declared far below — this
119
- * is only ever called at runtime, long after that const has evaluated); `terminal` and `doc` carry
120
- * a title because a session's name and a document's name are not derivable from a screen. */
118
+ /* The title map. `dest` and `agent` read DEST_TITLE; `list` and `more` derive (the old shell); `detail` reads
119
+ * DETAIL_TITLE; `terminal` and `doc` carry a title because a session's name and a document's name are not derivable. */
121
120
  function mobTitleOf(s) {
122
121
  if (!s) return '';
122
+ if (s.kind === 'dest') return DEST_TITLE[s.id] || DEST_TITLE[s.dest] || '';
123
+ if (s.kind === 'agent') return DEST_TITLE.agent;
123
124
  if (s.kind === 'list') return MODE_TITLE[s.mode] || String(s.mode || '');
124
125
  if (s.kind === 'more') return 'more';
125
126
  if (s.kind === 'detail') return DETAIL_TITLE[s.dk] || 'detail';
126
127
  return s.title || String(s.id || '');
127
128
  }
128
- function mobNavInit(dest) { return { dest: MOBILE_ROOT[dest] ? dest : 'board', stack: [] }; }
129
+ function mobNavInit(dest) { return { dest: MOBILE_ROOT[dest] ? dest : 'today', stack: [] }; }
129
130
  function mobPush(nav, screen) {
130
- const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('board');
131
+ const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
131
132
  return { dest: n.dest, stack: n.stack.concat([mobScreen(Object.assign({ dest: n.dest }, screen))]) };
132
133
  }
133
- // Removes EXACTLY one. Back from a destination root is a no-op — it never exits the app and never
134
- // unwinds into another destination, which is the second half of P3.
134
+ // Removes EXACTLY one. Back from a tab's root is a no-op — it never exits the app and never
135
+ // unwinds into another tab.
135
136
  function mobPop(nav) {
136
- const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('board');
137
+ const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
137
138
  return n.stack.length ? { dest: n.dest, stack: n.stack.slice(0, -1) } : { dest: n.dest, stack: [] };
138
139
  }
139
- // Choosing a destination RESETS it to its root with an empty stack — the prototype's {tab, stack:[]}.
140
+ // Choosing a tab RESETS it to its root with an empty stack.
140
141
  function mobSwitchDest(nav, dest) { return mobNavInit(dest); }
141
142
  function mobCurrentOf(nav) {
142
- const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('board');
143
+ const n = nav && MOBILE_ROOT[nav.dest] ? nav : mobNavInit('today');
143
144
  return n.stack.length ? n.stack[n.stack.length - 1] : mobRootOf(n.dest);
144
145
  }
145
- /* R11 — the legacy key is read ONCE, for migration, and never written again. Every value the old
146
- * three-way switcher could hold, plus anything unrecognised and nothing at all, lands on `board`:
147
- * the destination root is the only screen that is safe to draw without re-reading anything. */
148
- function mobMigrateView(v) { return MOBILE_ROOT[v] ? v : 'board'; }
146
+ /* R11 — a value stored by an older nav is read ONCE, for migration. Every value the retired navs could hold, plus
147
+ * anything unrecognised and nothing at all, lands on a tab's root: the only screen that is safe to draw without
148
+ * re-reading anything. */
149
+ function mobMigrateView(v) {
150
+ if (typeof v === 'string' && Object.prototype.hasOwnProperty.call(LEGACY_DEST, v)) return LEGACY_DEST[v];
151
+ return MOBILE_ROOT[v] ? v : 'today';
152
+ }
149
153
 
150
154
  return { push: mobPush, pop: mobPop, switchDest: mobSwitchDest, init: mobNavInit,
151
155
  current: mobCurrentOf, titleOf: mobTitleOf, columnOf: mobColumnOf, panelOf: mobPanelOf,
@@ -153,5 +157,5 @@ function mobMigrateView(v) { return MOBILE_ROOT[v] ? v : 'board'; }
153
157
  MODE_TITLE,
154
158
  migrateView: mobMigrateView, screen: mobScreen, root: mobRootOf,
155
159
  DESTS: MOBILE_DESTS, DEST_FOR_MODE: MOBILE_DEST, ROOTS: MOBILE_ROOT,
156
- PANEL_OF_KIND: MOBILE_PANEL, DETAIL_TITLE };
160
+ PANEL_OF_KIND: MOBILE_PANEL, DETAIL_TITLE, DEST_TITLE, LEGACY_DEST };
157
161
  });
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+ /* needs-providers.js — the planning layer's needs-you rows, one file per run, folded into server.js deriveNeeds
3
+ * (pm-os-absorb Runs 2 to 5, PRD R12).
4
+ *
5
+ * rowsFor(w, { lead }) → rows every provider's rows for workspace `w`, each tagged { derived: true, ws, provider }
6
+ * sigFor(w) → string one signature over every provider (server.js needsSig), so a count is never stale
7
+ * isProviderRow(n) → boolean server.js keeps these rows out of derived state, next steps and the portfolio,
8
+ * as it keeps Run 1's plan rows out (notPlanNeed)
9
+ * PROVIDERS · RESERVED_KINDS · status()
10
+ *
11
+ * Four runs add needs rows (kind 8 "needs repair", kind 7 cycle rows, kind 4 work to check, kind 1 report to issue).
12
+ * Each adds ONE file, named in PROVIDERS before it exists, instead of a block in deriveNeeds. A PROVIDER exports:
13
+ * module.exports = {
14
+ * kinds: ['pm-line'], // the row kinds it makes; unique across providers
15
+ * rowsFor(w, { lead }) → [{ run_id, kind, initiative_id, question, doc?, today? }],
16
+ * sig(w) → string, // optional: changes whenever its rows may change
17
+ * };
18
+ * `w` is a registered workspace ({ id, root, … }). `lead` is true for the one registered tree that speaks for a
19
+ * repository's ledgers (server.js isPlanNeedsTree): a provider of repository-wide rows returns [] when it is false, or
20
+ * a repository with a linked worktree registered counts every row twice. `run_id` is unique and stable per row and
21
+ * starts with the provider's name: the needs queue de-duplicates by it.
22
+ * `today` (optional) is what a Today card needs, passed through /api/needs unchanged:
23
+ * { kind: 1..8, source: '<provider>', cause, evidence, act: { label, cli? } }
24
+ * A row the page receives never carries a session token: name an agent by its page name (`agent:sess-<n>`).
25
+ *
26
+ * A provider file that is not on disk yet is skipped. One that fails to load, claims a kind another uses, or throws
27
+ * gives ONE row of kind 'provider-error' that names it: a broken source is never an empty list (R10).
28
+ */
29
+ const path = require('path');
30
+
31
+ const PROVIDERS = Object.freeze([
32
+ 'pm-lines-needs', // Run 2 (2B): a PM-only line that is not attested (kind 8)
33
+ 'initiative-needs', // Run 2 (2C): day one, agent proposals, passed check-by (kind 7); a record with no anchor (kind 8)
34
+ 'work-check-needs', // Run 4 (4D): a person's item in Review, a batched Release (kind 4)
35
+ 'report-needs', // Run 5 (5C): a report to issue (kind 1), cycle rows (kind 7), a schedule left no output (kind 8)
36
+ ]);
37
+ /** Kinds that already mean something in the needs queue. A provider may not reuse one. */
38
+ const RESERVED_KINDS = Object.freeze(['question', 'accept', 'outcome', 'keep', 'evidence', 'plan-flag', 'provider-error']);
39
+
40
+ let table = null;
41
+ function load() {
42
+ if (table) return table;
43
+ const mods = [];
44
+ const broken = [];
45
+ const owner = new Map(RESERVED_KINDS.map((k) => [k, '(reserved)']));
46
+ for (const name of PROVIDERS) {
47
+ const full = path.join(__dirname, name + '.js');
48
+ let mod;
49
+ try { mod = require(full); }
50
+ catch (e) {
51
+ if (e && e.code === 'MODULE_NOT_FOUND' && String(e.message).includes(full)) continue; // not built yet
52
+ broken.push({ name, error: `failed to load: ${String((e && e.message) || e).slice(0, 160)}` });
53
+ continue;
54
+ }
55
+ const kinds = mod && Array.isArray(mod.kinds) ? mod.kinds : null;
56
+ if (!kinds || !kinds.length || typeof mod.rowsFor !== 'function') { broken.push({ name, error: 'must export kinds and rowsFor' }); continue; }
57
+ const clash = kinds.find((k) => typeof k !== 'string' || owner.has(k));
58
+ if (clash !== undefined) { broken.push({ name, error: `kind ${JSON.stringify(clash)} is already used by ${owner.get(clash) || 'nobody (not a string)'}` }); continue; }
59
+ for (const k of kinds) owner.set(k, name);
60
+ mods.push({ name, mod, kinds });
61
+ }
62
+ table = { mods, broken };
63
+ return table;
64
+ }
65
+
66
+ const errorRow = (w, name, error) => ({
67
+ run_id: `${name}:error`, kind: 'provider-error', initiative_id: null, derived: true, doc: null, ws: w.id, provider: name,
68
+ question: `The needs source ${name} could not be read: ${error}`,
69
+ });
70
+
71
+ function rowsFor(w, { lead = true } = {}) {
72
+ if (!w) return [];
73
+ const { mods, broken } = load();
74
+ const out = broken.map((b) => errorRow(w, b.name, b.error));
75
+ for (const { name, mod, kinds } of mods) {
76
+ let rows;
77
+ try { rows = mod.rowsFor(w, { lead: !!lead }); }
78
+ catch (e) { out.push(errorRow(w, name, String((e && e.message) || e).slice(0, 160))); continue; }
79
+ for (const r of Array.isArray(rows) ? rows : []) {
80
+ if (!r || typeof r.run_id !== 'string' || !kinds.includes(r.kind)) { out.push(errorRow(w, name, 'a row has no run_id or a kind it did not declare')); break; }
81
+ out.push({ ...r, derived: true, ws: w.id, provider: name });
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /** Each provider's state-health lines (optional `banner(w, { lead })` → [{ source, error }]): a source that is still being
88
+ * read, not a row. Today shows them in its banner. */
89
+ function bannersFor(w, { lead = true } = {}) {
90
+ if (!w) return [];
91
+ const out = [];
92
+ for (const { name, mod } of load().mods) {
93
+ if (typeof mod.banner !== 'function') continue;
94
+ try { for (const b of mod.banner(w, { lead: !!lead }) || []) if (b && b.error) out.push({ source: String(b.source || name), error: String(b.error) }); }
95
+ catch { /* a banner that fails says nothing; its rows still report errors */ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ function sigFor(w) {
101
+ if (!w) return '';
102
+ const { mods, broken } = load();
103
+ const parts = [`broken:${broken.length}`];
104
+ for (const { name, mod } of mods) {
105
+ if (typeof mod.sig !== 'function') { parts.push(`${name}:-`); continue; }
106
+ let s;
107
+ try { s = String(mod.sig(w)); } catch { s = 'unreadable'; }
108
+ parts.push(`${name}:${s}`);
109
+ }
110
+ return parts.join(',');
111
+ }
112
+
113
+ const isProviderRow = (n) => !!n && n.derived === true && typeof n.provider === 'string';
114
+
115
+ function status() {
116
+ const t = load();
117
+ return { loaded: t.mods.map((m) => ({ name: m.name, kinds: m.kinds.slice() })), broken: t.broken.map((b) => ({ ...b })) };
118
+ }
119
+
120
+ module.exports = { PROVIDERS, RESERVED_KINDS, rowsFor, bannersFor, sigFor, isProviderRow, status };