create-pmos 0.133.0 → 0.135.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 +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +35 -2
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+ /* stage-tools.js — the records the PM edits per initiative and stage (problem statement, RICE, launch checklist, kill
3
+ * conditions, learnings), and the sprint burndown from the Jira snapshot (initiative stage-tools-records, PRD R1, R2, R4, R6).
4
+ *
5
+ * TOOLS · LEDGER = 'initiative-tools.jsonl'
6
+ * checkData(tool, data) → { ok: true, data } | { ok: false, error } the shape of one save; unknown fields dropped
7
+ * readLedger(file) → Map<initiative, Map<tool, { data, at, by }>> the last valid line per initiative and tool
8
+ * recordsOf(folded, initiative) → { problem, rice, launch, kill, learnings } each { data, at, by } or null; rice adds score
9
+ * appendTool(file, { initiative, tool, data }, actor, { now }) → the line written
10
+ * riceScore(data) → reach × impact × confidence/100 ÷ effort, one decimal
11
+ * riceRank(folded, ids) → [{ id, score, rank, reach, impact, confidence, effort, at }] highest score first
12
+ * buildDeliverOn(boardsFile, board) → false only when the board's own line says buildDeliver: false
13
+ * burndown(tickets, { sprint, today }) → { available: true, sprint, committed, done, left, ... } | { available: false, reason }
14
+ *
15
+ * THE LEDGER is one append-only file per repository, in that repository's state folder (the PM's choice, 2026-10-11): a
16
+ * save appends { v: 1, at, by, ev: 'tool', initiative, tool, data }. Nothing is rewritten. `by` is the guard's actor, never
17
+ * a field of the body. A line that does not parse, or whose data fails the shape check, is skipped by the read.
18
+ * The factory never writes this file (D65): the only writer is the page route, behind pm-guard requirePagePress.
19
+ */
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const LEDGER = 'initiative-tools.jsonl';
24
+ const TOOLS = Object.freeze(['problem', 'rice', 'launch', 'kill', 'learnings']);
25
+ const IMPACTS = Object.freeze([0.25, 0.5, 1, 2, 3]);
26
+ const KILL_STATES = Object.freeze(['watching', 'tripped', 'cleared']);
27
+ const LIMITS = Object.freeze({ launch: 40, kill: 20, learnings: 60 });
28
+ const TEXT_MAX = { problem: 4000, item: 500 };
29
+ const ITEM_ID = /^[A-Za-z0-9][\w-]{0,39}$/;
30
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
31
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
32
+ const num = (v) => typeof v === 'number' && Number.isFinite(v);
33
+ const textOk = (v, max) => typeof v === 'string' && v.trim().length >= 1 && v.length <= max && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(v);
34
+
35
+ function checkItems(tool, items, one) {
36
+ if (!Array.isArray(items)) return { ok: false, error: `${tool} needs items: a list` };
37
+ if (items.length > LIMITS[tool]) return { ok: false, error: `${tool} holds at most ${LIMITS[tool]} items` };
38
+ const out = []; const seen = new Set();
39
+ for (let i = 0; i < items.length; i++) {
40
+ const it = items[i];
41
+ if (!isObj(it)) return { ok: false, error: `${tool} item ${i + 1} is not an object` };
42
+ if (!ITEM_ID.test(String(it.id || ''))) return { ok: false, error: `${tool} item ${i + 1} needs an id (letters, digits, - or _)` };
43
+ if (seen.has(it.id)) return { ok: false, error: `${tool} item id ${it.id} is used twice` };
44
+ seen.add(it.id);
45
+ if (!textOk(it.text, TEXT_MAX.item)) return { ok: false, error: `${tool} item ${i + 1} needs text of 1 to ${TEXT_MAX.item} characters on one line` };
46
+ if (/[\r\n]/.test(it.text)) return { ok: false, error: `${tool} item ${i + 1} text must be one line` };
47
+ const r = one(it, i);
48
+ if (typeof r === 'string') return { ok: false, error: r };
49
+ out.push({ id: it.id, text: it.text.trim(), ...r });
50
+ }
51
+ return { ok: true, data: { items: out } };
52
+ }
53
+
54
+ /** The shape of one save. Unknown fields are dropped, so a record carries only what the read promises. */
55
+ function checkData(tool, data) {
56
+ if (!TOOLS.includes(tool)) return { ok: false, error: `tool must be one of ${TOOLS.join(', ')}` };
57
+ if (!isObj(data)) return { ok: false, error: 'data must be an object' };
58
+ if (tool === 'problem') {
59
+ if (!textOk(data.text, TEXT_MAX.problem)) return { ok: false, error: `problem needs text of 1 to ${TEXT_MAX.problem} characters` };
60
+ return { ok: true, data: { text: data.text.trim() } };
61
+ }
62
+ if (tool === 'rice') {
63
+ const { reach, impact, confidence, effort } = data;
64
+ if (!num(reach) || reach < 0) return { ok: false, error: 'rice reach must be a number of 0 or more' };
65
+ if (!IMPACTS.includes(impact)) return { ok: false, error: `rice impact must be one of ${IMPACTS.join(', ')}` };
66
+ if (!num(confidence) || confidence < 0 || confidence > 100) return { ok: false, error: 'rice confidence must be a percentage from 0 to 100' };
67
+ if (!num(effort) || effort <= 0) return { ok: false, error: 'rice effort must be a number above 0 (person-months)' };
68
+ return { ok: true, data: { reach, impact, confidence, effort } };
69
+ }
70
+ if (tool === 'launch') return checkItems(tool, data.items, (it, i) => (typeof it.done === 'boolean' ? { done: it.done } : `launch item ${i + 1} needs done: true or false`));
71
+ if (tool === 'kill') return checkItems(tool, data.items, (it, i) => (KILL_STATES.includes(it.status) ? { status: it.status } : `kill item ${i + 1} needs status: ${KILL_STATES.join(', ')}`));
72
+ return checkItems(tool, data.items, (it, i) => (typeof it.date === 'string' && DATE_RE.test(it.date) ? { date: it.date } : `learnings item ${i + 1} needs date: YYYY-MM-DD`));
73
+ }
74
+
75
+ function readLedger(file) {
76
+ const out = new Map();
77
+ let text = '';
78
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return out; }
79
+ for (const raw of text.split('\n')) {
80
+ if (!raw.trim()) continue;
81
+ let e;
82
+ try { e = JSON.parse(raw); } catch { continue; }
83
+ if (!isObj(e) || e.ev !== 'tool' || typeof e.initiative !== 'string' || !e.initiative) continue;
84
+ const c = checkData(e.tool, e.data);
85
+ if (!c.ok) continue;
86
+ if (!out.has(e.initiative)) out.set(e.initiative, new Map());
87
+ out.get(e.initiative).set(e.tool, { data: c.data, at: typeof e.at === 'string' ? e.at : null, by: typeof e.by === 'string' ? e.by : null });
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function riceScore(d) {
93
+ if (!d || !num(d.effort) || d.effort <= 0) return null;
94
+ return Math.round((d.reach * d.impact * (d.confidence / 100) / d.effort) * 10) / 10;
95
+ }
96
+
97
+ function recordsOf(folded, initiative) {
98
+ const mine = folded.get(initiative) || new Map();
99
+ const out = {};
100
+ for (const t of TOOLS) {
101
+ const r = mine.get(t);
102
+ out[t] = r ? { ...r, data: t === 'rice' ? { ...r.data, score: riceScore(r.data) } : r.data } : null;
103
+ }
104
+ return out;
105
+ }
106
+
107
+ function appendTool(file, { initiative, tool, data }, actor, { now } = {}) {
108
+ const line = { v: 1, at: new Date(now == null ? Date.now() : now).toISOString(), by: (actor && actor.by) || 'pm:unknown', ev: 'tool', initiative, tool, data };
109
+ fs.mkdirSync(path.dirname(file), { recursive: true });
110
+ fs.appendFileSync(file, JSON.stringify(line) + '\n');
111
+ return line;
112
+ }
113
+
114
+ function riceRank(folded, ids) {
115
+ const want = Array.isArray(ids) && ids.length ? new Set(ids) : null;
116
+ const rows = [];
117
+ for (const [id, tools] of folded) {
118
+ if (want && !want.has(id)) continue;
119
+ const r = tools.get('rice');
120
+ if (!r) continue;
121
+ rows.push({ id, score: riceScore(r.data), ...r.data, at: r.at });
122
+ }
123
+ rows.sort((a, b) => (b.score - a.score) || a.id.localeCompare(b.id));
124
+ let rank = 0; let last = null;
125
+ rows.forEach((r, i) => { if (r.score !== last) { rank = i + 1; last = r.score; } r.rank = rank; });
126
+ return rows;
127
+ }
128
+
129
+ /** The per-board "Build and deliver" switch, from the board's own lines in boards.jsonl (kind 'board'). The last line that
130
+ * names it wins. Missing file, missing board or a board that never set it: on. */
131
+ function buildDeliverOn(boardsFile, board) {
132
+ if (!boardsFile || !board) return true;
133
+ let text = '';
134
+ try { text = fs.readFileSync(boardsFile, 'utf8'); } catch { return true; }
135
+ let on = true;
136
+ for (const raw of text.split('\n')) {
137
+ if (!raw.trim()) continue;
138
+ let e;
139
+ try { e = JSON.parse(raw); } catch { continue; }
140
+ if (isObj(e) && e.kind === 'board' && e.board === board && typeof e.buildDeliver === 'boolean') on = e.buildDeliver;
141
+ }
142
+ return on;
143
+ }
144
+
145
+ // ── burndown ──────────────────────────────────────────────────────────────────────────────────────
146
+ const DAY = 86400000;
147
+ const dayNum = (d) => Date.UTC(+d.slice(0, 4), +d.slice(5, 7) - 1, +d.slice(8, 10)) / DAY;
148
+ const dayStr = (n) => new Date(n * DAY).toISOString().slice(0, 10);
149
+ const r1 = (x) => Math.round(x * 10) / 10;
150
+
151
+ /** The sprint to draw: the named one, else the active one, else the latest that has dates. Sprints are keyed by id when
152
+ * Jira gave one, else by name. */
153
+ function pickSprint(tickets, name) {
154
+ const by = new Map();
155
+ for (const t of tickets) {
156
+ const s = t && t.sprint;
157
+ if (!s || !s.name) continue;
158
+ const k = s.id || s.name;
159
+ const cur = by.get(k) || { name: s.name, state: s.state, id: s.id || null, start: null, end: null, completed: null };
160
+ for (const f of ['start', 'end', 'completed']) if (s[f] && !cur[f]) cur[f] = s[f];
161
+ by.set(k, cur);
162
+ }
163
+ const all = [...by.values()];
164
+ if (name) return all.find((s) => s.name === name) || null;
165
+ return all.find((s) => s.state === 'active') || all.filter((s) => s.start).sort((a, b) => (b.start > a.start ? 1 : -1))[0] || all[0] || null;
166
+ }
167
+
168
+ /**
169
+ * Remaining points per day over the sprint, from the tickets' current sprint membership. Committed is the points of every
170
+ * ticket in the sprint; a ticket counts as burned on its resolved date (or, when Done with no date, on the sprint's
171
+ * completion date for a closed sprint). Scope added or removed mid-sprint is not visible: the snapshot has no history.
172
+ */
173
+ function burndown(tickets, { sprint: name, today } = {}) {
174
+ const list = Array.isArray(tickets) ? tickets : [];
175
+ const s = pickSprint(list, name);
176
+ if (!s) return { available: false, reason: name ? `no ticket is in the sprint ${name}` : 'no ticket is in a sprint' };
177
+ if (!s.start || !s.end) return { available: false, reason: `the Jira read has no dates for ${s.name}: the next Jira status check adds them`, sprint: s };
178
+ const inSprint = list.filter((t) => t.sprint && (s.id ? t.sprint.id === s.id : t.sprint.name === s.name));
179
+ const pointed = inSprint.filter((t) => num(t.points));
180
+ if (!pointed.length) return { available: false, reason: `no ticket in ${s.name} has story points`, sprint: s, tickets: inSprint.length, unpointed: inSprint.length };
181
+ const t0 = dayNum(s.start); const t1 = dayNum(s.end);
182
+ if (t1 < t0) return { available: false, reason: `${s.name} ends before it starts`, sprint: s };
183
+ const now = dayNum(today || new Date().toISOString().slice(0, 10));
184
+ const burnDay = (t) => {
185
+ if (t.category !== 'done') return null;
186
+ if (t.resolved) return dayNum(t.resolved);
187
+ return s.completed ? dayNum(s.completed) : now;
188
+ };
189
+ const committed = pointed.reduce((a, t) => a + t.points, 0);
190
+ const span = Math.max(1, t1 - t0);
191
+ const last = Math.min(t1, now);
192
+ const series = [];
193
+ for (let d = t0; d <= t1; d++) {
194
+ const ideal = r1(committed * (1 - (d - t0) / span));
195
+ if (d > last) { series.push({ date: dayStr(d), remaining: null, ideal }); continue; }
196
+ const burned = pointed.reduce((a, t) => { const b = burnDay(t); return b != null && b <= d ? a + t.points : a; }, 0);
197
+ series.push({ date: dayStr(d), remaining: r1(committed - burned), ideal });
198
+ }
199
+ const done = pointed.filter((t) => t.category === 'done').reduce((a, t) => a + t.points, 0);
200
+ const left = r1(committed - done);
201
+ const daysLeft = Math.max(0, t1 - now);
202
+ return {
203
+ available: true, sprint: s, committed: r1(committed), done: r1(done), left, daysLeft,
204
+ needPerDay: daysLeft > 0 ? r1(left / daysLeft) : null, series, tickets: inSprint.length, unpointed: inSprint.length - pointed.length,
205
+ };
206
+ }
207
+
208
+ module.exports = { LEDGER, TOOLS, IMPACTS, KILL_STATES, LIMITS, checkData, readLedger, recordsOf, appendTool, riceScore, riceRank, buildDeliverOn, burndown, pickSprint };
@@ -3,6 +3,7 @@
3
3
  // The workflow engine already decides which layout a repo has (scripts/workflow-state.js layout()): the PMOS repo keeps its
4
4
  // ledgers under planning/runs, a kit tenant keeps them under pmos/state. Readers must ask here and never hard-code planning/.
5
5
  const path = require('path');
6
+ const { workspaceLib } = require('../workspace-lib.js');
6
7
 
7
8
  function pathsFor(root) {
8
9
  const base = path.resolve(root || process.cwd());
@@ -33,7 +34,7 @@ function planItemToFact(it) {
33
34
  }
34
35
 
35
36
  function planItemFacts(text) {
36
- const P = require('../../workspace/lib/plan-items.js');
37
+ const P = require(workspaceLib('plan-items.js')); // a checkout's workspace/lib, or the package's own lib
37
38
  const parsed = P.parseText(text);
38
39
  const folded = P.fold(parsed.lines || parsed);
39
40
  const list = Array.isArray(folded.items) ? folded.items : Object.values(folded.items || {});
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+ /* workspace-lib.js — where a workspace/lib/ module is, for a script that runs in a checkout AND as a copy in a workspace
3
+ * package (incident workspace-package-script-closure).
4
+ *
5
+ * require(workspaceLib('plan-items.js'))
6
+ *
7
+ * Some scripts share a fold with the workspace (the plan items, the item steps). In a checkout that module is at
8
+ * workspace/lib/<name>. In a package the script is a copy that scripts/vendor-workspace-engine.js put in lib/engine/,
9
+ * and this file is lib/engine/workspace-lib.js, so the module is the bundle's own lib/<name>, one folder up. A plain relative
10
+ * require of ../workspace/lib/x.js resolves in the first place only, and the vendor step refuses it.
11
+ *
12
+ * Both places are tried, the checkout first. When neither exists the checkout path is returned, and require() names it.
13
+ */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ function workspaceLib(name) {
18
+ const n = String(name || '');
19
+ if (!/^[a-z0-9][a-z0-9-]*\.js$/.test(n)) throw new Error(`workspaceLib: '${n}' must be a file name in workspace/lib/, such as 'plan-items.js'`);
20
+ const candidates = [path.join(__dirname, '..', 'workspace', 'lib', n), path.join(__dirname, '..', n)];
21
+ return candidates.find((p) => fs.existsSync(p)) || candidates[0];
22
+ }
23
+
24
+ module.exports = { workspaceLib };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+ /* lib/work-evidence.js — "Looks done?" proof for work done outside PMOS (initiative console3-looks-done).
3
+ *
4
+ * evidenceFor(item, { events, notes, now }) → string | null one sentence, at most 240 characters
5
+ * withEvidence(items, { events, notes, now }) → items offline items gain `evidence` when they have none
6
+ *
7
+ * Pure: no I/O, no model. An offline item has a `person`. Proof is a calendar event that has ended and names the person, or
8
+ * a note that is not private and names the person (its title, meeting title or summary lines; the body is never read), both
9
+ * after the item was created. The PM confirms; nothing closes the item. */
10
+
11
+ const MAX = 240;
12
+ const MIN_FIRST = 3;
13
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+ const day = (t) => new Date(t).toISOString().slice(0, 10);
15
+ const clip = (s) => (s.length > MAX ? s.slice(0, MAX - 1).trimEnd() + '…' : s);
16
+
17
+ /** A matcher for a person: the full name, or the first name as a whole word (3 letters or more). */
18
+ function matcher(person) {
19
+ const name = String(person || '').trim().replace(/\s+/g, ' ');
20
+ if (!name) return null;
21
+ const parts = [esc(name)];
22
+ const first = name.split(' ')[0];
23
+ if (first.length >= MIN_FIRST) parts.push(esc(first));
24
+ return new RegExp(`(^|[^\\p{L}\\p{N}])(?:${parts.join('|')})(?![\\p{L}\\p{N}])`, 'iu');
25
+ }
26
+
27
+ const ms = (t) => { const v = Date.parse(t || ''); return Number.isNaN(v) ? null : v; };
28
+
29
+ function calendarProof(re, since, now, events) {
30
+ let best = null;
31
+ for (const e of events || []) {
32
+ const start = ms(e.at || e.start);
33
+ if (start === null || start < since) continue;
34
+ const end = ms(e.end) === null ? start : ms(e.end);
35
+ if (end > now) continue;
36
+ if (!re.test([e.title, ...(e.attendees || [])].join('\n'))) continue;
37
+ if (!best || start > best.start) best = { start, title: String(e.title || '').trim() };
38
+ }
39
+ return best && best.title ? `You had "${best.title}" on ${day(best.start)}.` : null;
40
+ }
41
+
42
+ function noteProof(re, since, now, notes) {
43
+ let best = null;
44
+ for (const n of notes || []) {
45
+ if (!n || n.private) continue;
46
+ const at = ms(n.updated_at) === null ? ms(n.created_at) : ms(n.updated_at);
47
+ if (at === null || at < since || at > now) continue;
48
+ const hay = [n.title, n.meeting && n.meeting.title, ...(n.summary_lines || [])].filter(Boolean).join('\n');
49
+ if (!re.test(hay)) continue;
50
+ if (!best || at > best.at) best = { at, title: String(n.title || (n.meeting && n.meeting.title) || 'Untitled note').trim() };
51
+ }
52
+ return best ? `Your note "${best.title}" from ${day(best.at)} mentions them.` : null;
53
+ }
54
+
55
+ function evidenceFor(item, { events = [], notes = [], now = Date.now() } = {}) {
56
+ if (!item || item.kind !== 'offline' || item.done) return null;
57
+ if (typeof item.evidence === 'string' && item.evidence.trim()) return item.evidence;
58
+ const re = matcher(item.person);
59
+ if (!re) return null;
60
+ const since = ms(item.created_at) === null ? 0 : ms(item.created_at);
61
+ const parts = [calendarProof(re, since, now, events), noteProof(re, since, now, notes)].filter(Boolean);
62
+ return parts.length ? clip(parts.join(' ')) : null;
63
+ }
64
+
65
+ function withEvidence(items, ctx = {}) {
66
+ return (items || []).map((it) => {
67
+ if (!it || it.kind !== 'offline') return it;
68
+ const ev = evidenceFor(it, ctx);
69
+ return ev && ev !== it.evidence ? { ...it, evidence: ev } : it;
70
+ });
71
+ }
72
+
73
+ module.exports = { evidenceFor, withEvidence, matcher, MAX };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.133.0",
3
+ "version": "0.135.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.133.0",
9
+ "version": "0.135.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.133.0",
3
+ "version": "0.135.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -12,10 +12,12 @@
12
12
  * Not registered with the registry (Today has no view registry): dest-today.js imports mountBriefing/disposeBriefing. */
13
13
  import { el } from './dom.js';
14
14
  import { createIdleGate, watchIdle, applyDayPart } from './c3-daypart.js';
15
- import { LENSES, TAG_STYLE, PART_STYLE, cardView, dayPartFor, nextCards, swipeDir, sizeFor, caseModelOf, rawOption } from './briefing-model.js';
15
+ import { LENSES, TAG_STYLE, PART_STYLE, PARTS, partByKey, partFor, cardView, nextCards, swipeDir, sizeFor, caseModelOf, rawOption } from './briefing-model.js';
16
16
  import { BX, caseFile, toast } from './c3-brief.js';
17
17
  import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
18
18
  import { listenButton } from './shell-readaloud.js';
19
+ import { onBriefPage } from './c3-brief-data.js';
20
+ import { button } from './primitives.js';
19
21
 
20
22
  const NS = 'http://www.w3.org/2000/svg';
21
23
  const roots = new WeakMap(); // host -> state
@@ -149,16 +151,23 @@ function hero(v) {
149
151
 
150
152
  /* ── one card: the mockup's cardHtml (line 2324): .fc > .ff (.tg + .cf) and .fb (.fh + .case) ──────────────────────── */
151
153
 
152
- const tagStyle = (v) => TAG_STYLE[v.tag] || TAG_STYLE['Just in'];
154
+ /* the tag's style, coloured by the card's tone when the engine sends one (briefing-model.js TONE_COLOR) */
155
+ const tagStyle = (v) => { const T = TAG_STYLE[v.tag] || TAG_STYLE['Just in']; return v.toneColor ? { ...T, color: v.toneColor } : T; };
153
156
  const pct = (x) => Math.round(Math.max(0, Math.min(1, x)) * 100);
154
157
 
155
158
  function eyebrow(v) {
156
159
  const T = tagStyle(v);
157
160
  if (v.eyebrow) return [el('span', { class: 'tg', style: '--tc:var(--muted-foreground)' }, c3ic(v.eyebrow.icon, 'sm'), v.eyebrow.label), v.pov ? el('span', { class: 'tag pov' }, v.pov) : null];
158
- const tg = el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(v.icon || T.icon, 'sm'), v.tag);
161
+ const tg = el('span', { class: 'tg' + (T.filled ? ' f' : ''), title: T.q }, c3ic(v.icon || T.icon, 'sm'), v.kind || v.tag);
159
162
  return v.pov ? [tg, el('span', { class: 'tag pov' }, v.pov)] : tg;
160
163
  }
161
164
 
165
+ /* One number tile. Its colour is a token name ('var(--warning)' gives class k-warning), set by console3.css, so no inline style is needed. */
166
+ const kpTile = (v, l, c) => {
167
+ const m = /^var\(--([a-z-]+)\)$/.exec(String(c || '')); const tone = m ? ({ success: 'foreground', purple: 'acc' }[m[1]] || m[1]) : 'acc'; // the colour vocabulary has no green or purple tile: success reads neutral, purple takes the accent
168
+ return el('div', { class: 'kp k-' + tone }, el('b', null, v), el('span', null, l));
169
+ };
170
+
162
171
  /* the mockup's .kp tiles (line 765): numbers the card carries, only the ones it carries */
163
172
  function kpis(v) {
164
173
  const t = v.terms || {};
@@ -166,7 +175,15 @@ function kpis(v) {
166
175
  if (t.impact != null) a.push([pct(t.impact) + '%', 'impact', 'var(--acc)']);
167
176
  if (t.urgency != null) a.push([pct(t.urgency) + '%', 'urgency', 'var(--warning)']);
168
177
  if (t.score != null) a.push([String(Math.round(t.score * 100) / 100), 'score', 'var(--success)']);
169
- return a.slice(0, 3).map((k) => el('div', { class: 'kp', style: '--c:' + k[2] }, el('b', null, k[0]), el('span', null, k[1])));
178
+ return a.slice(0, 3).map((k) => kpTile(k[0], k[1], k[2]));
179
+ }
180
+
181
+ /* The front's right side (the mockup's UI.deck): the number tiles the card carries (.kp). A card that carries no number keeps its picture (.hero), so the
182
+ * front is never empty. The tiles are a row of their own, not a second slot, so a card draws one or the other. */
183
+ function cfh(v) {
184
+ const tiles = v.kpisX ? v.kpisX.map((k) => kpTile(k.v, k.l, k.c)) : kpis(v);
185
+ if (tiles.length) return el('div', { class: 'cfh', 'data-kp': String(tiles.length) }, el('div', { class: 'chips2 kps' }, ...tiles));
186
+ return el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': v.hero.kind }, hero(v)));
170
187
  }
171
188
 
172
189
  function front(v) {
@@ -179,8 +196,7 @@ function front(v) {
179
196
  el('div', null, eyebrow(v)),
180
197
  el('div', { class: 'cf' },
181
198
  el('div', { class: 'cfa' }, el('h2', null, v.headline), v.sub ? el('div', { class: 'cfs' }, v.sub) : v.why ? el('div', { class: 'cfs', 'data-bf-why': '' }, 'why shown: ' + v.why) : null),
182
- el('div', { class: 'cfh' }, el('div', { class: 'hero', 'data-hero': v.hero.kind }, hero(v))),
183
- el('div', { class: 'cfk' }, ...(v.kpisX ? v.kpisX.map((k) => el('div', { class: 'kp', style: '--c:' + (k.c || 'var(--acc)') }, el('b', null, k.v), el('span', null, k.l))) : kpis(v))),
199
+ cfh(v),
184
200
  el('div', { class: 'cff' }, el('div', { class: 'cfx' }, ...facts), rec)));
185
201
  }
186
202
 
@@ -232,7 +248,7 @@ function card(S, v, i, n) {
232
248
  }, front(v), el('span', { class: 'flipg', 'aria-hidden': 'true' }, c3ic('undo', 'sm')), bk);
233
249
  bk.bind(c);
234
250
  /* named by its own headline (aria-labelledby) rather than a long aria-label: the card's name is its text, and the position is its description */
235
- const hd = c.querySelector('h2'); if (hd) { hd.id = 'fc-h-' + String(v.key || i).replace(/\W+/g, '-') + '-' + i; c.setAttribute('aria-labelledby', hd.id); } else c.setAttribute('aria-label', v.tag + ': ' + v.headline);
251
+ const hd = c.querySelector('h2'); if (hd) { hd.id = 'fc-h-' + String(v.key || i).replace(/\W+/g, '-') + '-' + i; c.setAttribute('aria-labelledby', hd.id); } else c.setAttribute('aria-label', (v.kind || v.tag) + ': ' + v.headline);
236
252
  return c;
237
253
  }
238
254
 
@@ -244,9 +260,10 @@ function flipTo(S, c, force) {
244
260
  if (!c.classList.contains('on') && !c.classList.contains('back')) { deck.scrollTo({ left: c.offsetLeft - deck.children[0].offsetLeft, behavior: reduced() ? 'auto' : 'smooth' }); return; }
245
261
  const toBack = force !== undefined ? force : !c.classList.contains('back');
246
262
  deck.querySelectorAll('.fc.back').forEach((o) => { if (o !== c) { o.classList.remove('back'); o.setAttribute('aria-expanded', 'false'); } });
247
- /* R2: the card has one fixed size and both faces are in it, so the flip is a CSS transition of rotateY (console3.css .flipcard, about 350 ms).
248
- * Nothing is measured and nothing scrolls: no sibling and nothing below the deck moves. prefers-reduced-motion turns the transition off. */
263
+ /* The flip is a CSS transition of rotateY (console3.css .flipcard, about 350 ms). The front has a fixed size; the back grows to fit its content (console3 foundation),
264
+ * so the options and the slide are never clipped. Sibling cards keep their boxes. prefers-reduced-motion turns the transition off. */
249
265
  c.classList.toggle('back', toBack); c.setAttribute('aria-expanded', String(toBack));
266
+ if (toBack && c.scrollIntoView) c.scrollIntoView({ block: 'nearest', behavior: reduced() ? 'auto' : 'smooth' }); // the back grows to its content: bring its foot into view
250
267
  S.flip = toBack ? c.dataset.bfCard || '1' : null;
251
268
  }
252
269
 
@@ -323,15 +340,37 @@ async function load(S, lens) {
323
340
  else { const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); } // another lens arrived: update the switch, keep the open card
324
341
  }
325
342
 
326
- /* the stance bar (the mockup's stanceBar, line 2332): the lens icon and its name and line. The lens follows the clock; there is no switch (console3 R4). */
343
+ /* the stance bar (the mockup's stanceBar): the greeting for the part of the day, the lens and its line, the five-part day switch and the read-aloud
344
+ * button. The part follows the clock; a tap on the switch previews another part (its lens, its cards) and the preview drops when the clock moves
345
+ * to another part. The name is the PM's first name when PMOS holds one (GET /api/brief-page `me`); without one the greeting stands alone. */
327
346
  function stance(S) {
328
347
  const P = PART_STYLE[S.lens] || PART_STYLE.Plan;
348
+ const part = partByKey(S.preview || S.clockPart);
349
+ const seg = el('div', { class: 'seg dp', role: 'group', 'aria-label': 'Part of the day', title: 'Follows the clock. Tap to preview another part of the day.' },
350
+ ...PARTS.map((x) => {
351
+ const b = button({ label: x.label, variant: 'ghost', size: 'sm', onClick: () => setPart(S, x.k), attrs: { 'aria-pressed': String(x.k === part.k), 'data-pg-dp': x.k, title: x.label + ' · ' + x.at + (x.k === S.clockPart ? ' (now)' : ''), 'aria-label': x.label } });
352
+ b.replaceChildren(c3ic(x.icon, 'sm')); // the icon alone; the label is the aria-label
353
+ return b;
354
+ }));
329
355
  return el('div', { class: 'stance', 'data-bf': 'lens-bar' },
330
- el('span', { class: 'ki' }, c3ic(P.icon)),
331
- el('div', null, el('b', { title: 'Follows the clock.' }, S.lens), el('span', { class: 'dim' }, P.sub)),
356
+ el('span', { class: 'ki' }, c3ic(part.icon)),
357
+ el('div', null, el('b', { title: 'Follows the clock: this is the part of the day it is now.' }, part.greet + (S.me ? ', ' + S.me : '')), el('span', { class: 'dim' }, S.lens + ': ' + P.sub)),
358
+ seg,
332
359
  listenButton(() => { const x = S.data[S.lens]; return x && x.ok ? x.cards[S.i] || x.cards[0] : null; })); // the mockup's speaker (data-a=listen) reads the card on top
333
360
  }
334
361
 
362
+ /* Preview one part of the day: its lens, its cards. Nothing is saved; the clock's own part comes back by tapping it, or when the clock moves on. */
363
+ function setPart(S, k) {
364
+ const part = partByKey(k);
365
+ S.preview = k === S.clockPart ? null : k;
366
+ const lens = part.lens;
367
+ const changed = lens !== S.lens;
368
+ S.lens = lens; S.i = 0; S.flip = null;
369
+ if (changed) load(S, lens);
370
+ draw(S);
371
+ if (S.ctx.onLens) S.ctx.onLens(lens);
372
+ }
373
+
335
374
  /* the mockup's running line (.tick > .tkt, lines 539 to 545): the engine's ticker strings, twice so the marquee loops */
336
375
  function ticker(items) {
337
376
  const one = (hide) => items.map((t) => el('span', { class: 'tki', 'aria-hidden': hide ? 'true' : null }, el('b', { style: 'color:var(--acc)' }, c3ic('bolt', 'sm')), String(t)));
@@ -340,7 +379,12 @@ function ticker(items) {
340
379
 
341
380
  function draw(S) {
342
381
  const d = S.data[S.lens];
343
- const kids = [stance(S)];
382
+ /* a page's headline deck (ctx.bare, the Calendar tab): no lens row, and nothing at all until cards of its own scope arrive. ctx.scope
383
+ * is then required of the answer, so a server that does not know the scope never fills the page with another page's cards. */
384
+ const bare = !!(S.ctx && S.ctx.bare);
385
+ if (bare && (!d || !d.ok || d.scope !== S.ctx.scope || !(d.cards || []).length)) { S.root.replaceChildren(); S.root.hidden = true; S.flip = null; return; }
386
+ if (bare) S.root.hidden = false;
387
+ const kids = bare ? [] : [stance(S)];
344
388
  if (!d) kids.push(el('p', { class: 'dim mono', style: small, role: 'status' }, 'Reading the repository'));
345
389
  else if (!d.ok) kids.push(el('p', { class: 'dim mono', style: small, role: 'status', 'data-bf': 'error' }, 'The briefing could not be read: ' + d.error));
346
390
  else {
@@ -370,16 +414,18 @@ export function mountBriefing(host, ctx) {
370
414
  ensureC3Css();
371
415
  const root = el('section', { class: 'c3 brief', 'data-board': 'briefing', 'aria-label': 'Briefing' });
372
416
  host.replaceChildren(root);
373
- const clock = dayPartFor(new Date());
374
- S = { host, root, ctx: ctx || {}, lens: clock, clock, zoom: 'week', i: 0, flip: null, data: {}, dead: false };
417
+ const clockPart = partFor(new Date()).k;
418
+ const clock = partByKey(clockPart).lens;
419
+ S = { host, root, ctx: ctx || {}, lens: clock, clock, clockPart, preview: null, me: null, zoom: 'week', i: 0, flip: null, data: {}, dead: false };
375
420
  S.stopPhone = c3Phone(root);
376
421
  roots.set(host, S);
377
422
  draw(S);
378
423
  if (!host.closest('[data-daypart]')) applyDayPart(root); // a page without the console3 shell still gets the day part tokens
379
424
  /* The clock moves the lens, but never under the reader: a new part waits for 60 s without pointer or key activity and for every card to be turned back. */
380
- const gate = createIdleGate({ current: S.lens, part: (ms) => dayPartFor(new Date(ms)), busy: () => !!S.flip || !!S.root.querySelector('.fc.back'),
381
- apply: (l) => { if (S.dead) return; S.clock = l; S.lens = l; S.i = 0; S.flip = null; load(S, l); draw(S); if (S.ctx.onLens) S.ctx.onLens(l); } });
425
+ const gate = createIdleGate({ current: S.clockPart, part: (ms) => partFor(new Date(ms)).k, busy: () => !!S.flip || !!S.root.querySelector('.fc.back'),
426
+ apply: (k) => { if (S.dead) return; const l = partByKey(k).lens; S.clockPart = k; S.preview = null; S.clock = l; S.lens = l; S.i = 0; S.flip = null; load(S, l); draw(S); if (S.ctx.onLens) S.ctx.onLens(l); } });
382
427
  S.idle = watchIdle(gate);
428
+ if (!(S.ctx && S.ctx.bare)) onBriefPage((j) => { const me = j && j.ok !== false && j.me ? String(j.me) : null; if (me === S.me || S.dead) return; S.me = me; const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); }); // the greeting's name
383
429
  if (S.ctx.onLens) S.ctx.onLens(S.lens); // the page may follow the lens from the start (Reflect: "Carries over", "What I could stop")
384
430
  // the shown lens first, then the others for their preview line
385
431
  load(S, S.lens).then(() => Promise.all(LENSES.filter((l) => l !== S.lens).map((l) => load(S, l))));
@@ -15,6 +15,26 @@ export const TAG_STYLE = {
15
15
  'Small win': { key: 'small', icon: 'up', color: 'var(--success)', filled: false, q: 'Small win: progress you can bank in one gesture.' },
16
16
  'Big win': { key: 'big', icon: 'flag', color: 'var(--success)', filled: true, q: 'Big win: a milestone worth telling others.' },
17
17
  };
18
+ /* A card's tone (scripts/signals/kinds.js) colours it: the engine names open kinds ("Deadline moved", "Blocked", "Risk"), so the
19
+ * colour comes from the tone, not from the kind. Only health tones have a colour of their own (the colour vocabulary); a positive
20
+ * or neutral card, or one without a tone, keeps its tag's colour. */
21
+ export const TONES = ['critical', 'warning', 'positive', 'neutral'];
22
+ export const TONE_COLOR = { critical: 'var(--destructive)', warning: 'var(--warning)' };
23
+
24
+ /* The five parts of the day (console3 Briefing stance switch). The lens is the part's signal ranking: evening and night both read Reflect. */
25
+ export const PARTS = [
26
+ { k: 'morning', lens: 'Plan', icon: 'sun', label: 'Morning', at: '08:00', greet: 'Good morning' },
27
+ { k: 'midday', lens: 'Unblock', icon: 'bolt', label: 'Midday', at: '12:30', greet: 'Good afternoon' },
28
+ { k: 'afternoon', lens: 'Close', icon: 'check', label: 'Afternoon', at: '16:00', greet: 'Good afternoon' },
29
+ { k: 'evening', lens: 'Reflect', icon: 'moon', label: 'Evening', at: '18:30', greet: 'Good evening' },
30
+ { k: 'night', lens: 'Reflect', icon: 'star', label: 'Night', at: '22:00', greet: 'Good night' },
31
+ ];
32
+ export const partByKey = (k) => PARTS.find((p) => p.k === k) || PARTS[0];
33
+ /** The part of the day for a clock time (morning 5 to 11, midday 11 to 14, afternoon 14 to 17, evening 17 to 21, night 21 to 5). */
34
+ export function partFor(date) {
35
+ const h = (date instanceof Date ? date : new Date(date)).getHours();
36
+ return partByKey(h >= 5 && h < 11 ? 'morning' : h >= 11 && h < 14 ? 'midday' : h >= 14 && h < 17 ? 'afternoon' : h >= 17 && h < 21 ? 'evening' : 'night');
37
+ }
18
38
  /* The stance bar of each lens: icon and the line under its name (console3.html line 2206). */
19
39
  export const PART_STYLE = {
20
40
  Plan: { icon: 'sun', sub: 'Decide what gates the day' },
@@ -27,21 +47,24 @@ export const PART_STYLE = {
27
47
  * (pm-work-view.js SPRITE, `c3i-*`). The five starting tags map to the icons TAG_STYLE gives them. No model call, no network. */
28
48
  export const NEUTRAL_ICON = 'info';
29
49
  export const ICON_RULES = [
30
- [/\bsmall win\b|\bprogress\b|\bimproved?\b/, 'up'],
50
+ [/\bsmall win\b|\bquick win\b|\bprogress\b|\bimproved?\b|\bcapacity\b|\bfree\b/, 'up'],
31
51
  [/\bbig win\b|\bwin\b|\bmilestone\b|\blaunch|\bship(ped)?\b|\brelease/, 'flag'],
32
52
  [/\bcritical\b|\brisks?\b|\bblock|\bincident|\boutage|\boverdue\b|\bescalat|\bregress|\bfail/, 'alert'],
33
53
  [/\bsecurity\b|\bcompliance\b|\bprivacy\b|\bpolicy\b/, 'shield'],
34
54
  [/\bdeadline|\bdue\b|\bmoved\b|\bslip|\bdelay|\bschedule|\bwaiting\b|\bwaits?\b/, 'clock'],
35
- [/\bdecision|\bapprov|\btrade-?off|\bchoose|\bchoice/, 'scale'],
55
+ [/\bdecision|\bapprov|\btrade-?off|\bchoose|\bchoice|\bconflict|\bmismatch|\bdisagree/, 'scale'],
36
56
  [/\bopportunit|\bidea\b|\binsight|\bsuggest/, 'spark'],
37
57
  [/\bjira\b|\bticket|\bissue|\bnew from\b|\binbound\b/, 'inbox'],
38
58
  [/\bmeeting|\bcalendar|\bevent\b|\bagenda/, 'cal'],
39
- [/\bmail|\bemail|\breply\b|\bmessage/, 'mail'],
40
- [/\bperson|\bpeople|\bstakeholder|\bowner\b|\bteam\b|\bcustomer/, 'user'],
41
- [/\bokr\b|\bgoal|\bobjective|\bkey result|\btarget\b/, 'target'],
59
+ [/\bmail|\bemail|\breply\b|\bmessage|\bpromise|\btold\b/, 'mail'],
60
+ [/\bperson|\bpeople|\bstakeholder|\bowner\b|\bowns?\b|\bnobody\b|\bteam\b|\bcustomer/, 'user'],
61
+ [/\bokr\b|\bgoal|\bobjective|\bkey result|\btarget\b|\banchor/, 'target'],
42
62
  [/\bmetric|\bkpi\b|\btrend|\breport|\bnumber|\bburn/, 'report'],
43
63
  [/\bpull request|\bpr\b|\bmerge|\bbranch|\bcommit|\bcode\b|\brepo/, 'branch'],
44
64
  [/\bdoc\b|\bdocs\b|\bspec\b|\bprd\b|\bnote/, 'doc'],
65
+ [/\bplan\b|\bplans\b|\broadmap|\bforecast|\btoo much\b|\boverload|\bload\b/, 'cal'],
66
+ [/\bquestion|\bunknown|\bhypothes|\bevidence|\bresearch/, 'search'],
67
+ [/\bsort\b|\btriage|\border\b|\brank/, 'filter'],
45
68
  [/\bjust in\b|\bnew\b|\bnews\b|\bupdate|\bsignal|\bchanged?\b/, 'bolt'],
46
69
  ];
47
70
  /** The icon name for a card of this kind and signal type. Pure. Unknown or empty gives the neutral icon. */
@@ -88,6 +111,10 @@ export function cardView(card) {
88
111
  kind: String(c.kind || c.tag || tag),
89
112
  icon: iconFor(c.kind || c.tag || tag, c.signal || c.signalType || c.type),
90
113
  tag, mark: TAG_MARK[tag], pinned: !!c.pinned,
114
+ tone: TONES.includes(c.tone) ? c.tone : null,
115
+ toneColor: TONE_COLOR[c.tone] || null,
116
+ /* what a card of this kind must say (the engine's template); null on cards that carry none */
117
+ template: c.template && typeof c.template === 'object' && c.template.text ? { text: String(c.template.text), gaps: Array.isArray(c.template.gaps) ? c.template.gaps.map(String) : [] } : null,
91
118
  headline: String(c.headline || ''),
92
119
  hero: { kind, points: Array.isArray(h.points) ? h.points.map(num).filter((x) => x !== null) : null },
93
120
  facts: (Array.isArray(c.facts) ? c.facts : []).filter(Boolean).map(String).slice(0, 3),
@@ -144,6 +171,7 @@ export const rawOption = (m, id) => ((m && (m.opts || m.options)) || []).find((o
144
171
  export const NO_ROUTE = 'No channel connected, and no route on this page carries it out. The suggestion stays text; nothing is sent or changed.';
145
172
 
146
173
  /** The case of one engine card (v = cardView(card)). Pure; every string comes from a card field. */
174
+ const GAP_WORDS = { what: 'what happened', impact: 'the impact', when: 'by when', who: 'who it waits on', move: 'the move' };
147
175
  const OUTWARD = /\b(send|tell|warn|reply|replies|email|message|notify|invite|ask)\b/i;
148
176
  export function caseModelOf(v, o = {}) {
149
177
  const reg = v && v.key && registered('card', v.key);
@@ -156,9 +184,10 @@ export function caseModelOf(v, o = {}) {
156
184
  }));
157
185
  const dl = c.deadline;
158
186
  return {
159
- id: 'card:' + (c.key || ''), tc: (TAG_STYLE[c.tag] || TAG_STYLE['Just in']).color,
187
+ id: 'card:' + (c.key || ''), tc: c.toneColor || (TAG_STYLE[c.tag] || TAG_STYLE['Just in']).color,
160
188
  situation: c.waitsFor ? 'Waiting on ' + c.waitsFor + '.' : c.sub || c.headline, evidence: ev, options: opts,
161
- confidence: c.conf != null ? c.conf : null, unsure: c.unsure || '',
189
+ confidence: c.conf != null ? c.conf : null,
190
+ unsure: [c.unsure, c.template && c.template.gaps.length ? 'Not known yet: ' + c.template.gaps.map((g) => GAP_WORDS[g] || g).join(', ') + '.' : ''].filter(Boolean).join(' '),
162
191
  ifYouWait: dl ? (dl.days == null ? 'No deadline is known: ' + dl.text : dl.text) : '', why: c.why || '',
163
192
  outward: !sim, outboxReason: NO_ROUTE,
164
193
  };