create-pmos 0.252.0 → 0.254.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 (54) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/workflow-state.js +6 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/payload/state/log.md +1 -1
  8. package/workspace/briefing-premium-board-test.js +275 -0
  9. package/workspace/briefing-premium-initiative-test.js +304 -0
  10. package/workspace/briefing-premium-products-test.js +164 -0
  11. package/workspace/briefing-premium-test.js +438 -0
  12. package/workspace/briefing-premium-work-test.js +281 -0
  13. package/workspace/fixtures/board-premium-facts.js +38 -0
  14. package/workspace/fixtures/briefing-premium-facts.js +32 -0
  15. package/workspace/fixtures/initiative-premium-facts.js +42 -0
  16. package/workspace/fixtures/my-work-premium-facts.js +31 -0
  17. package/workspace/guide/briefing-premium.md +151 -0
  18. package/workspace/guide/index.md +1 -0
  19. package/workspace/lib/board-premium.js +133 -0
  20. package/workspace/lib/briefing-premium-agent.js +134 -0
  21. package/workspace/lib/briefing-premium-products.js +121 -0
  22. package/workspace/lib/briefing-premium-writes.js +138 -0
  23. package/workspace/lib/briefing-premium.js +581 -0
  24. package/workspace/lib/engine/workflow-state.js +6 -1
  25. package/workspace/lib/headless.js +1 -1
  26. package/workspace/lib/initiative-premium.js +147 -0
  27. package/workspace/lib/my-work-premium.js +151 -0
  28. package/workspace/lib/route-modules.js +4 -0
  29. package/workspace/lib/routes-board-premium.js +61 -0
  30. package/workspace/lib/routes-briefing-premium.js +115 -0
  31. package/workspace/lib/routes-initiative-premium.js +68 -0
  32. package/workspace/lib/routes-my-work-premium.js +31 -0
  33. package/workspace/lib/settings-store.js +3 -0
  34. package/workspace/lib/vendor/scripts/workflow-state.js +6 -1
  35. package/workspace/package-lock.json +2 -2
  36. package/workspace/package.json +1 -1
  37. package/workspace/public/pm/premium-board.js +180 -0
  38. package/workspace/public/pm/premium-initiative.js +336 -0
  39. package/workspace/public/pm/premium-shell.js +184 -0
  40. package/workspace/public/pm/premium-work.js +198 -0
  41. package/workspace/public/pm/premium.css +1212 -0
  42. package/workspace/public/pm/premium.js +272 -0
  43. package/workspace/public/premium-board.html +73 -0
  44. package/workspace/public/premium-initiative.html +46 -0
  45. package/workspace/public/premium-work.html +84 -0
  46. package/workspace/public/premium.html +69 -0
  47. package/workspace/public/vendor/geist-mono-var.woff2 +0 -0
  48. package/workspace/public/vendor/geist-var.woff2 +0 -0
  49. package/workspace/server.js +5 -3
  50. package/workspace/test-files.json +14 -0
  51. package/workspace/test-lists/briefing-premium.json +19 -0
  52. package/workspace/third-party/README.md +12 -0
  53. package/workspace/third-party/geist-OFL.LICENSE +93 -0
  54. package/workspace/third-party/geist-mono-OFL.LICENSE +93 -0
@@ -0,0 +1,581 @@
1
+ 'use strict';
2
+ /* lib/briefing-premium.js — the premium Briefing's page model, built from repository state (initiative briefing-premium, R2 to R9).
3
+ *
4
+ * build(facts) → model PURE. The hero, the ordered rows, the Today timeline, the dock and the header.
5
+ * readSources({ root, now }) → facts what the route reads. A source that cannot be read is named in `missing`, never guessed.
6
+ * modelOf({ root }) → { state, model, as_of } the route's answer: a kept snapshot, refreshed in the background.
7
+ *
8
+ * facts = { now (ms), asOf (ms), initiatives[], runs[], merges[], evals[], meetings[]|null, tripwires[]|null, missing[] }
9
+ * initiatives one per record: { id, title, lane, stage, accepted, stopped, anchor, last_activity (s), blockers[], next, steps[{key,status,next,rule}] }
10
+ * runs agent runs: { initiative_id, created_at, human_corrections }
11
+ * merges commits to the trunk: { at (ms), title }
12
+ * evals review verdicts: { initiative_id, verdict, recorded_at }
13
+ * meetings today's calendar rows: { start (ms), end (ms), title, with, initiative }, or null when the calendar cannot be read
14
+ * tripwires dated kill conditions: { id, name, date, decided }, or null when the file cannot be read
15
+ *
16
+ * THE RULE THE WHOLE FILE FOLLOWS: a sentence, a count or a "if you skip" line is made only from a record that holds it. A clause with
17
+ * no record is left out; nothing is softened or rounded into a claim. Every clause carries `from`, the records it came from, so a test
18
+ * can assert the trace rather than read it. The hero, the list and the dock read ONE list of rows (R8), so they cannot disagree.
19
+ */
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { execFile } = require('child_process');
23
+ const W = require('./briefing-premium-writes.js');
24
+
25
+ const DAY = 86400000;
26
+ const MAX_ROWS = 7;
27
+ const WORDS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve'];
28
+ const num = (n) => (n >= 0 && n <= 12 ? WORDS[n] : String(n));
29
+ const plural = (n, one, many) => (n === 1 ? one : many);
30
+ const pad = (n) => String(n).padStart(2, '0');
31
+ const hhmm = (ms) => { const d = new Date(ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()); };
32
+ const dayKey = (ms) => { const d = new Date(ms); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); };
33
+ const startOfDay = (ms) => { const d = new Date(ms); d.setHours(0, 0, 0, 0); return d.getTime(); };
34
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
35
+ const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
36
+ const dateLine = (ms) => { const d = new Date(ms); return WEEKDAYS[d.getDay()] + ' ' + d.getDate() + ' ' + MONTHS[d.getMonth()]; };
37
+ /* A short name for a sentence: the title up to its first dash, colon or bracket, without a leading article. */
38
+ const joinNames = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
39
+ const shortName = (t) => clip(String(t || '').split(/\s[—–-]\s|:|\(/)[0].replace(/^(the|a|an)\s+/i, ''), 48);
40
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
41
+ const shortDate = (ms) => { const d = new Date(ms); return d.getDate() + ' ' + MON[d.getMonth()]; };
42
+ const longDate = (ms) => shortDate(ms) + ' ' + new Date(ms).getFullYear();
43
+ const isoShort = (iso) => { const t = Date.parse(iso + 'T12:00:00'); return Number.isFinite(t) ? shortDate(t) : iso; };
44
+ const mdClean = (s) => String(s || '').replace(/\*\*/g, '').replace(/`/g, '').replace(/\s+/g, ' ').trim();
45
+ const clip = (s, n) => { s = String(s || '').replace(/\s+/g, ' ').trim(); return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s; };
46
+
47
+ /* What each blocked step means for the PM. The verb-first title and the consequence come from the rule the step enforces
48
+ * (scripts/workflow-state.js), so they state what the rule does, not a guess about the work. */
49
+ const STEP = {
50
+ prototype: { title: 'Get a prototype started', skip: 'the prototype lane does not start, and the initiative stays blocked.' },
51
+ spec: { title: 'Put the spec ahead of the build', skip: 'the build stays ahead of the spec, so the order check keeps failing.' },
52
+ contract: { title: 'Agree the contract', skip: 'the contract stays unagreed, so the run has no done-criteria to check.' },
53
+ anchor: { title: 'Anchor the initiative', skip: 'the initiative stays unanchored, and unanchored work is rejected.' },
54
+ route: { title: 'Route the initiative', skip: 'the initiative has no lane, so no lane rule can run.' },
55
+ };
56
+ const stepOf = (key) => STEP[key] || { title: 'Unblock the ' + key + ' step', skip: 'the ' + key + ' step stays blocked.' };
57
+
58
+ /* The options a decision offers. They name the choices the repository's own rules give (D41 for a tripwire, D60 for a prototype); none is marked
59
+ * Recommended, because no recommendation is on record. `draft` is the sentence the draft entry shows once the option is chosen. */
60
+ const OPT_TRIPWIRE = [
61
+ { k: 'reshape', t: 'Reshape the bet', d: 'Change what the condition guards, and say why. It goes in pm-decisions.md.', draft: 'Recorded as a draft decision: reshape the bet.' },
62
+ { k: 'extend', t: 'Extend the date', d: 'Move it and say why. It goes in pm-decisions.md so it is not a silent default.', draft: 'Recorded as a draft decision: extend the date. The new date goes here.' },
63
+ { k: 'retire', t: 'Retire the condition', d: 'Stop watching it, and say why.', draft: 'Recorded as a draft decision: retire the condition.' },
64
+ ];
65
+ const OPT_PROTOTYPE = [
66
+ { k: 'promote', t: 'Promote it', d: 'Derive the feature PRD and rubric from it (D60). The prototype itself never ships.', draft: 'Recorded as a draft disposition: promote.' },
67
+ { k: 'iterate', t: 'Iterate', d: 'Another round of specimens, then look again.', draft: 'Recorded as a draft disposition: iterate.' },
68
+ { k: 'kill', t: 'Kill it', d: 'Stop the initiative and say why.', draft: 'Recorded as a draft disposition: kill.' },
69
+ ];
70
+ const NO_REC = 'No option is chosen for you: no recommendation is on record.';
71
+ const fmRows = (fm) => (fm ? [['Lane', fm.lane], ['Anchor', fm.parent_kr], ['Status', fm.status && clip(fm.status, 260)]].filter((r) => r[1]) : []);
72
+
73
+ /* A record the builder cannot read must cost its own row or dialog and nothing more. tryD gives a dialog or null; a record that throws is skipped and counted,
74
+ * and the header says how many (never silently). */
75
+ const START_HOLD = 'Starting an agent from here arrives with the live conversation (the next slice).';
76
+ const tryD = (fn) => { try { return fn(); } catch { return null; } };
77
+ const skipped = { n: 0 };
78
+
79
+ /** Two products can hold an initiative of the same name, so a lookup by id also names the workspace (none in a one-workspace read). */
80
+ const idk = (x, id) => (x.ws || '') + '|' + id;
81
+ const live = (i) => i && !i.accepted && !i.stopped && !i.legacy;
82
+
83
+ /* ── rows ─────────────────────────────────────────────────────────────────────────────────────────────────── */
84
+ function blockedRows(f) {
85
+ const out = [];
86
+ for (const i of f.initiatives) {
87
+ try {
88
+ if (!live(i)) continue;
89
+ const st = (i.steps || []).find((s) => s.status === 'blocked' && s.blocking !== false) || (i.steps || []).find((s) => s.status === 'blocked');
90
+ if (!st) continue;
91
+ const m = stepOf(st.key);
92
+ out.push({
93
+ kind: 'blocked', key: 'blocked:' + i.id, title: m.title, subject: clip(i.title, 90),
94
+ prepared: st.next ? 'Next step: ' + clip(st.next, 140) : 'No next step is on record.',
95
+ skip: 'If you skip: ' + m.skip,
96
+ from: [i.id], stepKey: st.key, order: [-(i.blockers || []).length, i.id],
97
+ detail: { id: i.id, step: st.key, rule: st.rule || null, next: st.next == null ? null : String(st.next), record: i.record || null },
98
+ dialog: tryD(() => ({
99
+ eye: 'Unblock', logTitle: 'Read from the repository',
100
+ log: ['Read the initiative record' + (i.record ? ' (' + i.record + ')' : ''), 'Ran the workflow order rules on it', 'Found the step that blocks it: ' + st.key],
101
+ say: i.id + ' is blocked at the ' + st.key + ' step.' + (st.rule ? ' The rule it does not meet: ' + clip(st.rule, 200) : ''),
102
+ hold: START_HOLD, head: 'The way through',
103
+ facts: [['Blocked at', st.key], ['Initiative', i.id], ['The check', st.rule && clip(st.rule, 200)], ['Record', i.record]].filter((x) => x[1]),
104
+ opts: st.next ? [{ k: 'next', t: clip(st.next[0].toUpperCase() + st.next.slice(1), 140), d: 'The next step on record.' }] : [],
105
+ sel: st.next ? 'next' : null,
106
+ nodef: st.next ? '' : 'No next step is on record.',
107
+ prim: { next: 'Start: ' + clip(st.next || '', 40) },
108
+ pane: { label: 'Initiative record', file: i.record || i.id, h: clip(i.title, 90), rows: fmRows(i.fm) },
109
+ })),
110
+ });
111
+ } catch { skipped.n++; }
112
+ }
113
+ return out.sort((a, b) => (a.order[0] - b.order[0]) || (a.order[1] < b.order[1] ? -1 : 1));
114
+ }
115
+
116
+ function decisionRows(f) {
117
+ const out = [];
118
+ const meetings = f.meetings || [];
119
+ for (const i of f.initiatives) {
120
+ try {
121
+ if (!live(i) || i.lane !== 'prototype') continue;
122
+ if ((i.steps || []).some((s) => s.status === 'blocked')) continue;
123
+ if (i.stage !== 'accept' && i.stage !== 'eval') continue;
124
+ if ((f.protoDecided || []).includes(i.id)) continue; // the PM recorded its disposition (pm-decisions.md)
125
+ const mt = meetings.find((x) => x.initiative === i.id && x.start >= f.now - 0);
126
+ out.push({
127
+ kind: 'decision', key: 'decision:' + i.id, title: 'Decide: promote, iterate or kill', subject: clip(i.title, 90) + (mt ? ' · needed at the ' + hhmm(mt.start) + ' ' + mt.title : ''),
128
+ prepared: 'No recommendation is on record.',
129
+ skip: 'If you skip: ' + (mt ? 'the ' + hhmm(mt.start) + ' ' + mt.title + ' runs without a disposition.' : 'the prototype stays without a disposition.'),
130
+ hero: 'the ' + shortName(i.title) + ' waits for your call', from: [i.id].concat(mt ? ['meeting:' + mt.title] : []),
131
+ detail: { id: i.id, record: i.record || null },
132
+ dialog: tryD(() => ({
133
+ eye: 'Decide with evidence', logTitle: 'Read from the repository',
134
+ log: ['Read the initiative record' + (i.record ? ' (' + i.record + ')' : ''), 'Checked the prototype lane rule (D60): a prototype needs your disposition', f.meetings === null ? 'The calendar could not be read, so no meeting was looked for' : mt ? 'Found a meeting that needs it: ' + hhmm(mt.start) + ' ' + mt.title : 'Looked for a meeting that needs it: none found'],
135
+ say: 'This prototype waits for your disposition: promote, iterate or kill (D60). ' + NO_REC,
136
+ head: 'Where it stands',
137
+ facts: [['Initiative', i.id], ['Lane', i.lane], ['Stage', i.stage], ['Anchor', i.anchor], ['Needed at', mt ? hhmm(mt.start) + ' ' + mt.title : ''], ['Record', i.record]].filter((x) => x[1]),
138
+ write: { kind: 'decision' }, opts: OPT_PROTOTYPE, sel: null, nodef: NO_REC,
139
+ prim: { promote: 'Hold to record: promote', iterate: 'Hold to record: iterate', kill: 'Hold to record: kill' },
140
+ pane: { label: 'Disposition record', file: 'pm-decisions.md · draft entry', h: shortName(i.title), intro: longDate(f.now) + '. ' + clip(i.title, 90) + ' (a prototype) waits for a disposition.', evidence: [['Lane', i.lane], ['Stage', i.stage], ['Anchor', i.anchor], ['Recommendation', 'None is on record']].filter((x) => x[1]), optsDraft: true },
141
+ })),
142
+ });
143
+ } catch { skipped.n++; }
144
+ }
145
+ for (const t of f.tripwires || []) {
146
+ try {
147
+ const due = Date.parse(t.date + 'T23:59:59');
148
+ if (!Number.isFinite(due) || due >= f.now || t.decided) continue;
149
+ const short = String(t.name || t.id).split(/[—:]/)[0].trim();
150
+ out.push({
151
+ kind: 'decision', key: 'decision:' + t.id, title: 'Decide what the missed ' + t.id + ' date means', subject: clip(t.name, 90),
152
+ prepared: 'No recommendation is on record.', skip: 'If you skip: no decision is on record and the tripwire stays fired.',
153
+ hero: 'the ' + clip(short, 40) + ' tripwire missed its date', from: ['tripwire:' + t.id],
154
+ detail: { id: t.id, date: t.date },
155
+ dialog: tryD(() => ({
156
+ eye: 'Decide with evidence', logTitle: 'Read from the repository',
157
+ log: ['Read the kill condition (planning/okrs/kill-conditions.md · ' + t.id + ')', 'Checked pm-decisions.md for an entry on or after ' + t.date, 'Found none'],
158
+ say: 'The ' + t.id + ' date, ' + isoShort(t.date) + ', passed and no decision is on record. Firing is not automatic action: it forces your decision. ' + NO_REC,
159
+ head: 'The evidence',
160
+ facts: [['Condition', mdClean(t.name)], ['Threshold', mdClean(t.threshold)], ['Date it fired', t.date], ['Review cadence', mdClean(t.cadence)], ['Firing triggers', mdClean(t.fires)], ['Decision on record', 'None found']].filter((x) => x[1]),
161
+ write: { kind: 'decision' }, opts: OPT_TRIPWIRE, sel: null, nodef: NO_REC,
162
+ prim: { reshape: 'Hold to record: reshape', extend: 'Hold to record: extend', retire: 'Hold to record: retire' },
163
+ pane: { label: 'Decision record', file: 'pm-decisions.md · draft entry', h: t.id + ': ' + clip(mdClean(t.name).split(/[—:]/)[0], 50), intro: longDate(f.now) + '. Tripwire ' + t.id + ': the date (' + isoShort(t.date) + ') passed with no recorded decision.', evidence: [['Threshold', mdClean(t.threshold)], ['Firing triggers', mdClean(t.fires)], ['Decision on record', 'None found']].filter((x) => x[1]), optsDraft: true },
164
+ })),
165
+ });
166
+ } catch { skipped.n++; }
167
+ }
168
+ return out;
169
+ }
170
+
171
+ const leadOf = (e) => { const a = Array.isArray(e.findings) ? e.findings[0] : null; const t = typeof a === 'string' ? a : a && (a.summary || a.text || a.title || a.finding); return t ? clip(t, 220) : (e.lead_finding ? clip(e.lead_finding, 220) : ''); };
172
+ function failedReviews(f) {
173
+ const last = new Map();
174
+ for (const e of f.evals || []) {
175
+ if (!e || !e.initiative_id || e.verdict === 'SKIPPED') continue;
176
+ const t = Date.parse(e.recorded_at);
177
+ if (!Number.isFinite(t)) continue;
178
+ const p = last.get(e.initiative_id);
179
+ if (!p || t > p.t) last.set(e.initiative_id, { t, verdict: e.verdict, score: e.score, thr: e.pass_threshold, lead: leadOf(e) });
180
+ }
181
+ const byId = new Map(f.initiatives.map((i) => [i.id, i]));
182
+ const out = [];
183
+ for (const [id, v] of last) {
184
+ const i = byId.get(id);
185
+ if (v.verdict !== 'FAIL' || !live(i)) continue;
186
+ out.push({ id, days: Math.floor((f.now - v.t) / DAY), score: v.score, thr: v.thr, lead: v.lead, title: i.title, anchor: i.anchor });
187
+ }
188
+ return out;
189
+ }
190
+
191
+ function waitedRows(f) {
192
+ const fr = failedReviews(f);
193
+ if (!fr.length) return [];
194
+ const ds = fr.map((x) => x.days).sort((a, b) => a - b), lo = ds[0], hi = ds[ds.length - 1], n = fr.length;
195
+ const span = lo === hi ? lo + ' days' : lo + ' to ' + hi + ' days';
196
+ return [{
197
+ kind: 'waited', key: 'waited:reviews', title: 'Weigh ' + n + ' failed ' + plural(n, 'review', 'reviews'), subject: 'Waiting ' + span + ' · ' + n + ' ' + plural(n, 'item', 'items'),
198
+ prepared: n + ' ' + plural(n, 'verdict', 'verdicts') + ', each with findings on record.',
199
+ skip: 'If you skip: the ' + num(n) + ' ' + plural(n, 'review stays', 'reviews stay') + ' open and ' + plural(n, 'keeps', 'keep') + ' ageing (' + span + ' now).',
200
+ hero: num(n) + ' failed ' + plural(n, 'review is', 'reviews are') + ' still open', from: fr.map((x) => x.id), detail: { ids: fr.map((x) => x.id), days: ds },
201
+ dialog: tryD(() => ({
202
+ eye: 'Weigh the reviews', logTitle: 'Read from the repository',
203
+ log: ['Read planning/runs/evals.jsonl', 'Kept the latest verdict of each initiative', 'Found ' + n + ' that failed and have no later review'],
204
+ say: num(n)[0].toUpperCase() + num(n).slice(1) + ' ' + plural(n, 'review failed and none since has passed', 'reviews failed and none since has passed') + '. No proposal is on record for ' + plural(n, 'it', 'any of them') + '.',
205
+ head: 'The reviews',
206
+ items: fr.slice().sort((a, b) => b.days - a.days).map((x) => ({ t: clip(x.title || x.id, 80), m: x.days + ' days' + (x.anchor ? ' · ' + x.anchor : ''), s: x.score != null ? 'Scored ' + x.score + (x.thr != null ? ' of ' + x.thr : '') : 'No score recorded', lead: x.lead })),
207
+ pane: { label: n + ' failed ' + plural(n, 'review', 'reviews'), list: true },
208
+ })),
209
+ }];
210
+ }
211
+
212
+ /** A row of one product in a merged list: its key names the workspace (kind:id:ws) so two products never share a key, and it carries the product's name. */
213
+ const tagRow = (r, p) => Object.assign({}, r, { key: r.key + ':' + p.ws, product: p.name, ws: p.ws });
214
+ /** Every row of one set of facts, in the order the rules give, with those the PM put off split out. Not yet cut to the page's cap. */
215
+ function allRowsOf(f) {
216
+ let every = [].concat(blockedRows(f), decisionRows(f), waitedRows(f));
217
+ if (f.product) every = every.map((r) => tagRow(r, f.product));
218
+ const off = (r) => (f.defers && f.defers[r.key] > f.now); // put off by the PM until a time that has not come
219
+ const all = every.filter((r) => !off(r));
220
+ const putOff = every.filter(off).map((r) => ({ key: r.key, title: r.title, until: f.defers[r.key], ...(r.product ? { product: r.product } : {}) }));
221
+ return { all, putOff };
222
+ }
223
+ const cut = (all, putOff) => {
224
+ const rows = all.slice(0, MAX_ROWS).map((r, k) => Object.assign({ n: k + 1, first: k === 0 }, r));
225
+ rows.forEach((r) => delete r.order);
226
+ return { rows, more: Math.max(0, all.length - rows.length), putOff };
227
+ };
228
+ function rowsOf(f) { const a = allRowsOf(f); return cut(a.all, a.putOff); }
229
+
230
+ /* ── hero ─────────────────────────────────────────────────────────────────────────────────────────────────── */
231
+ function correctionRate(runs, from, to) {
232
+ const w = runs.filter((r) => { const t = Date.parse(r.created_at); return Number.isFinite(t) && t >= from && t < to && Number.isFinite(Number(r.human_corrections)); });
233
+ if (w.length < 10) return null;
234
+ const c = w.reduce((a, r) => a + Number(r.human_corrections), 0);
235
+ return { rate: Math.round((c / w.length) * 100), runs: w.length };
236
+ }
237
+
238
+ function heroOf(f, rows) {
239
+ const seg = (parts) => parts.map(([t, s]) => ({ t, s: !!s }));
240
+ const sentences = [];
241
+ const midnight = startOfDay(f.now);
242
+ const merged = (f.merges || []).filter((m) => m.at >= midnight && m.at <= f.now);
243
+ const cur = correctionRate(f.runs || [], f.now - 7 * DAY, f.now + 1), prev = correctionRate(f.runs || [], f.now - 14 * DAY, f.now - 7 * DAY);
244
+ {
245
+ const p = [];
246
+ if (merged.length) p.push(['Agents merged ', 0], [num(merged.length) + ' ' + plural(merged.length, 'change', 'changes') + ' today', 1]);
247
+ if (cur && prev && cur.rate !== prev.rate) {
248
+ const lower = cur.rate < prev.rate;
249
+ p.push([p.length ? ', and corrections went from ' : 'Corrections went from ', 0], [prev.rate + ' to ' + cur.rate + ' per 100 runs', 1], [' in a week' + (lower ? ', the direction you want' : ', the wrong direction') + '.', 0]);
250
+ } else if (p.length) p.push(['.', 0]);
251
+ if (p.length) sentences.push({ parts: seg(p), from: merged.length ? ['merges'] : ['runs'] });
252
+ }
253
+ {
254
+ const blocked = rows.filter((r) => r.kind === 'blocked'), rest = rows.filter((r) => r.hero);
255
+ const clauses = [];
256
+ if (blocked.length) {
257
+ const ordering = blocked.every((r) => r.stepKey === 'spec' || r.stepKey === 'prototype');
258
+ const where = [...new Set(blocked.map((r) => r.product).filter(Boolean))];
259
+ clauses.push({ t: num(blocked.length) + ' ' + plural(blocked.length, 'initiative is', 'initiatives are') + ' blocked' + (ordering ? ' on ordering' : '') + (where.length ? ' in ' + joinNames(where) : ''), from: blocked.map((r) => r.from[0]) });
260
+ }
261
+ rest.forEach((r) => clauses.push({ t: r.hero + (r.product ? ' in ' + r.product : ''), from: r.from }));
262
+ if (clauses.length) {
263
+ const p = [['What is stuck is yours: ', 0]];
264
+ clauses.forEach((c, k) => {
265
+ p.push([c.t, 1]);
266
+ p.push([k === clauses.length - 1 ? '.' : k === clauses.length - 2 ? ' and ' : ', ', 0]);
267
+ });
268
+ sentences.push({ parts: seg(p), from: [].concat(...clauses.map((c) => c.from)) });
269
+ }
270
+ }
271
+ {
272
+ const cut = f.now - 30 * DAY / 1, recent = f.initiatives.filter((i) => i.last_activity != null && i.last_activity * 1000 >= cut && !i.stopped);
273
+ const upkeep = recent.filter((i) => /^health:/.test(i.anchor || ''));
274
+ const idle = f.initiatives.filter((i) => live(i) && i.last_activity != null && i.last_activity * 1000 < cut);
275
+ const p = [];
276
+ if (recent.length >= 5 && upkeep.length / recent.length >= 0.25) {
277
+ const share = upkeep.length / recent.length;
278
+ p.push([share >= 0.45 && share <= 0.55 ? 'half of recent work is upkeep' : Math.round(share * 100) + '% of recent work is upkeep', 1]);
279
+ }
280
+ if (idle.length) { if (p.length) p.push([', and ', 0]); p.push([idle.length + ' ' + plural(idle.length, 'initiative has', 'initiatives have') + ' sat idle for a month', 1]); }
281
+ if (p.length) sentences.push({ parts: seg([['The slower risk is drift: ', 0]].concat(p, [['.', 0]])), from: upkeep.map((i) => i.id).concat(idle.map((i) => i.id)) });
282
+ }
283
+ return sentences;
284
+ }
285
+
286
+ /* ── today ────────────────────────────────────────────────────────────────────────────────────────────────── */
287
+ const EV_NEXT = 'Acting on it arrives in the next slice. This page writes nothing.';
288
+ function mergeDialog(m, f) {
289
+ const pr = (/\(#(\d+)\)\s*$/.exec(m.title) || [])[1], title = m.title.replace(/\s*\(#\d+\)\s*$/, '');
290
+ const files = m.files != null ? m.files : null;
291
+ const facts = files == null ? [] : [['Files', files + ' changed, +' + m.ins + ' −' + m.del], ['Mostly in', (m.dirs || []).join(', ')]].filter((x) => x[1]);
292
+ return {
293
+ eye: 'Merged today', logTitle: 'Read from git', title, sub: (pr ? '#' + pr + ' · ' : '') + hhmm(m.at) + ' · merged to main',
294
+ log: ['Read the commit on main' + (m.sha ? ' (' + m.sha.slice(0, 8) + ')' : ''), files == null ? 'The commit details could not be read' : 'Counted its files and lines', m.body ? 'Read its message' : 'It has no message body'],
295
+ say: 'This reached main today. Nothing needs you unless you disagree with it.',
296
+ head: 'What changed', facts, opts: [], sel: null, nodef: '', action: 'Return this run to the agent', hold: 'A merge has no run of its own to return. Open the agent run in Today to return it.',
297
+ pane: { label: 'Summary', file: (pr ? '#' + pr + ' · ' : '') + 'merged to main', h: title, intro: files == null ? '' : files + ' files changed, +' + m.ins + ' −' + m.del + (m.dirs && m.dirs.length ? '. Mostly in ' + m.dirs.join(', ') + '.' : '.'), paras: m.body ? String(m.body).split(/\n\s*\n/).map((x) => x.replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, 8) : [], parasTitle: 'From the commit message' },
298
+ };
299
+ }
300
+ function runDialog(r, t, byId) {
301
+ const i = byId.get(idk(r, r.initiative_id)), ap = r.pre_run_contract && r.pre_run_contract.PM_approved === true;
302
+ const facts = [['Initiative', (i && i.title) ? clip(i.title, 70) : r.initiative_id], ['Run', r.run_id], ['Lane', r.lane || (i && i.lane)], ['Corrections', Number.isFinite(Number(r.human_corrections)) ? String(r.human_corrections) : ''], ['Contract approved', r.pre_run_contract ? (ap ? 'Yes' : 'No') : ''], ['Recorded', longDate(t) + ' ' + hhmm(t)]].filter((x) => x[1]);
303
+ return {
304
+ eye: 'Agent run', logTitle: 'Read from the run log', title: i ? clip(i.title, 90) : r.initiative_id, sub: hhmm(t) + ' · agent run',
305
+ log: ['Read planning/runs/agent-runs.jsonl', 'Matched the run to its initiative' + (i ? '' : ' (no record found)'), 'Read its recorded fields'],
306
+ say: 'This run is logged. Nothing needs you unless you disagree with it.',
307
+ head: 'The run', facts, opts: [], sel: null, nodef: '', action: 'Return this run to the agent', write: { kind: 'return', run: r.run_id, ...(r.ws ? { ws: r.ws } : {}) },
308
+ pane: { label: 'Run record', file: 'planning/runs/agent-runs.jsonl', h: i ? clip(i.title, 90) : r.initiative_id, intro: '', rows: facts, paras: r.note ? [clip(r.note, 600)] : [], parasTitle: 'Its note' },
309
+ };
310
+ }
311
+ function meetingDialog(m) {
312
+ const mins = Math.round((m.end - m.start) / 60000);
313
+ const facts = [['Time', hhmm(m.start) + ' to ' + hhmm(m.end)], ['Length', mins + ' min'], ['People', m.with], ['Initiative', m.initiative]].filter((x) => x[1]);
314
+ return {
315
+ eye: 'Calendar', logTitle: 'Read from the calendar', title: m.title, sub: hhmm(m.start) + ' · ' + mins + ' min' + (m.with ? ' · ' + m.with : ''),
316
+ log: ['Read the calendar PMOS holds', 'Found this meeting for today', m.initiative ? 'Found its linked initiative: ' + m.initiative : 'It names no initiative'],
317
+ say: 'This meeting is in the calendar PMOS holds. PMOS holds no prep for it.',
318
+ head: 'The meeting', facts, opts: [], sel: null, nodef: '',
319
+ pane: { label: 'Meeting', file: 'Calendar', h: m.title, intro: longDate(m.start) + ', ' + hhmm(m.start) + ' to ' + hhmm(m.end) + '.', rows: facts, paras: [], parasTitle: '' },
320
+ };
321
+ }
322
+
323
+ function todayOf(f) {
324
+ const midnight = startOfDay(f.now);
325
+ const items = [];
326
+ for (const m of f.meetings || []) {
327
+ items.push({ at: m.start, t: hhmm(m.start), kind: 'meeting', title: m.title, sub: [Math.round((m.end - m.start) / 60000) + ' min', m.with].filter(Boolean).join(' · '), src: 'Calendar', dialog: tryD(() => meetingDialog(m)) });
328
+ }
329
+ for (const m of f.merges || []) if (m.at >= midnight && m.at <= f.now) items.push({ at: m.at, t: hhmm(m.at), kind: 'merge', ...(m.product ? { product: m.product } : {}), title: clip(m.title, 110), sub: 'Merged to main', src: 'Merges', dialog: tryD(() => mergeDialog(m, f)) });
330
+ const runs = (f.runs || []).filter((r) => { const t = Date.parse(r.created_at); return t >= midnight && t <= f.now; });
331
+ const byId = new Map(f.initiatives.map((i) => [idk(i, i.id), i]));
332
+ for (const r of runs) { const t = Date.parse(r.created_at); items.push({ at: t, t: hhmm(t), kind: 'run', ...(r.product ? { product: r.product } : {}), title: clip((byId.get(idk(r, r.initiative_id)) || {}).title || r.initiative_id, 110), sub: 'Agent run' + (Number.isFinite(Number(r.human_corrections)) ? ' · ' + r.human_corrections + ' ' + plural(Number(r.human_corrections), 'correction', 'corrections') : ''), src: 'Agent runs', dialog: tryD(() => runDialog(r, t, byId)) }); }
333
+ const ahead = items.filter((x) => x.at > f.asOf).sort((a, b) => a.at - b.at), earlier = items.filter((x) => x.at <= f.asOf).sort((a, b) => b.at - a.at);
334
+ const count = (k) => items.filter((x) => x.kind === k).length;
335
+ return { ahead, earlier, now: { t: hhmm(f.asOf), label: 'snapshot' }, counts: { meeting: count('meeting'), merge: count('merge'), run: count('run') }, note: ahead.length + earlier.length ? null : 'Nothing is recorded for today yet.' };
336
+ }
337
+
338
+ /* ── the model ────────────────────────────────────────────────────────────────────────────────────────────── */
339
+ const SOURCE_NAME = { calendar: 'the calendar', merges: 'merges', runs: 'agent runs', workflow: 'initiative state', evals: 'review verdicts', tripwires: 'kill conditions' };
340
+
341
+ const backAt = (ms, now) => (startOfDay(ms) === startOfDay(now) ? 'at ' + hhmm(ms) : dateLine(ms) + ' ' + hhmm(ms));
342
+ function build(f) {
343
+ skipped.n = 0;
344
+ f = Object.assign({ pending: false, initiatives: [], runs: [], merges: [], evals: [], meetings: null, tripwires: null, missing: [] }, f);
345
+ f.asOf = f.asOf || f.now;
346
+ const { rows, more, putOff } = rowsOf(f);
347
+ const hero = heroOf(f, rows);
348
+ const today = todayOf(f);
349
+ const missing = (f.missing || []).slice();
350
+ if (f.meetings === null && !missing.includes('calendar')) missing.push('calendar');
351
+ if (f.tripwires === null && !missing.includes('tripwires')) missing.push('tripwires');
352
+ return {
353
+ date: dateLine(f.now),
354
+ pending: !!f.pending,
355
+ header: { text: 'Real data · as of ' + hhmm(f.asOf) + (missing.length ? ' · not read: ' + missing.map((k) => SOURCE_NAME[k] || k).join(', ') : '') + (f.pending ? ' · reading initiative state' : '') + (skipped.n ? ' · ' + skipped.n + (skipped.n === 1 ? ' record' : ' records') + ' could not be read and ' + (skipped.n === 1 ? 'was' : 'were') + ' left out' : ''), missing },
356
+ hero: hero.length ? hero : [{ parts: [{ t: f.pending ? 'Reading the initiative state. What needs you is not known yet.' : 'Nothing needs you and nothing moved today that the records show.', s: false }], from: [] }],
357
+ list: { title: 'What you need to do', rows, more, count: rows.length + more, pendingNote: f.pending ? 'Blocked work, prototypes waiting for you and failed reviews appear when the initiative state has been read.' : null, putOff: putOff.map((x) => ({ key: x.key, title: x.title, until: x.until, text: x.title + ' · back ' + backAt(x.until, f.now) })), order: 'In this order: blocked work first, then decisions only you can make, then what has waited longest.' },
358
+ today,
359
+ dock: { needs: f.pending ? null : rows.length + more, label: plural(rows.length + more, 'needs you', 'need you'), next: rows[0] ? rows[0].title : null },
360
+ };
361
+ }
362
+
363
+
364
+ /* ── several products (initiative briefing-premium-products) ──────────────────────────────────────────────── */
365
+ const KIND_RANK = { blocked: 0, decision: 1, waited: 2 };
366
+ const longest = (r) => { const d = (r.detail && r.detail.days) || []; return d.length ? d[d.length - 1] : 0; };
367
+
368
+ /** Several products' facts → ONE model, in the same shape build() makes.
369
+ * g = { now, asOf, meetings (shared, or null), defers { key: until }, available: [{ ws, name, home, on }],
370
+ * parts: [{ product: { ws, name }, f (that product's facts) | null, state: 'ready' | 'reading' | 'error', error? }] }
371
+ * A row belongs to one product (its key ends with the workspace id) and the rows are put in one order: blocked first, then decisions, then reviews.
372
+ * A count is never summed across products: each product gets its own failed-review row. The hero and Today read the merged facts. */
373
+ function buildMany(g) {
374
+ skipped.n = 0;
375
+ const now = g.now, asOf = g.asOf || g.now, meetings = g.meetings === undefined ? null : g.meetings;
376
+ const parts = (g.parts || []).map((p) => ({ product: p.product, state: p.state || 'ready', error: p.error, f: p.f ? Object.assign({ initiatives: [], runs: [], merges: [], evals: [], tripwires: null, missing: [], protoDecided: [] }, p.f, { now, asOf, meetings, defers: g.defers || {}, product: p.product }) : null }));
377
+ const read = parts.filter((p) => p.f && p.state !== 'error');
378
+ const every = [], putOff = [];
379
+ for (const p of read) { const a = allRowsOf(p.f); every.push(...a.all); putOff.push(...a.putOff); }
380
+ every.sort((a, b) => (KIND_RANK[a.kind] - KIND_RANK[b.kind]) || (a.kind === 'blocked' ? (a.order[0] - b.order[0]) || (a.order[1] < b.order[1] ? -1 : a.order[1] > b.order[1] ? 1 : 0) : a.kind === 'waited' ? longest(b) - longest(a) : 0));
381
+ const { rows, more } = cut(every, putOff);
382
+ const tag = (list, p) => list.map((x) => Object.assign({}, x, { ws: p.product.ws, product: p.product.name }));
383
+ const cf = {
384
+ now, asOf, meetings, defers: g.defers || {},
385
+ initiatives: [].concat(...read.map((p) => p.f.initiatives.map((i) => Object.assign({}, i, { ws: p.product.ws })))),
386
+ runs: [].concat(...read.map((p) => tag(p.f.runs, p))),
387
+ merges: [].concat(...read.map((p) => tag(p.f.merges, p))),
388
+ evals: [].concat(...read.map((p) => tag(p.f.evals, p))),
389
+ };
390
+ const available = g.available || [], inc = available.filter((a) => a.on), out = available.filter((a) => !a.on);
391
+ const waiting = parts.filter((p) => p.state === 'reading' || !p.f).filter((p) => p.state !== 'error'), failed = parts.filter((p) => p.state === 'error');
392
+ const pending = parts.some((p) => p.state === 'reading' || (p.f && p.f.pending));
393
+ const missing = [];
394
+ if (meetings === null) missing.push('calendar');
395
+ for (const p of read) for (const k of p.f.missing || []) missing.push(k + ' (' + p.product.name + ')');
396
+ for (const p of failed) missing.push(p.product.name);
397
+ const missText = [].concat(meetings === null ? ['the calendar'] : [], ...read.map((p) => (p.f.missing || []).map((k) => (SOURCE_NAME[k] || k) + ' of ' + p.product.name)), failed.map((p) => p.product.name + ' could not be read'));
398
+ const none = !inc.length;
399
+ const hero = none ? [{ parts: [{ t: available.length ? 'No product is included, so there is nothing to read. Pick one under Products.' : 'No product workspace is registered, so there is nothing to read.', s: false }], from: [] }]
400
+ : heroOf(cf, rows);
401
+ return {
402
+ date: dateLine(now),
403
+ pending,
404
+ header: { text: 'Real data · as of ' + hhmm(asOf) + ' · ' + (none ? 'no product included' : joinNames(inc.map((a) => a.name))) + (out.length ? ' · ' + joinNames(out.map((a) => a.name)) + ' not included' : '') + (missText.length ? ' · not read: ' + missText.join(', ') : '') + (waiting.length ? ' · reading ' + joinNames(waiting.map((p) => p.product.name)) : '') + (skipped.n ? ' · ' + skipped.n + (skipped.n === 1 ? ' record' : ' records') + ' could not be read and ' + (skipped.n === 1 ? 'was' : 'were') + ' left out' : ''), missing },
405
+ hero: hero.length ? hero : [{ parts: [{ t: pending ? 'Reading the initiative state. What needs you is not known yet.' : 'Nothing needs you and nothing moved today that the records show.', s: false }], from: [] }],
406
+ list: { title: 'What you need to do', rows, more, count: rows.length + more, pendingNote: pending ? 'Blocked work, prototypes waiting for you and failed reviews appear when the initiative state has been read.' : null, putOff: putOff.map((x) => ({ key: x.key, title: x.title, ...(x.product ? { product: x.product } : {}), until: x.until, text: (x.product ? x.product + ': ' : '') + x.title + ' · back ' + backAt(x.until, now) })), order: 'In this order: blocked work first, then decisions only you can make, then what has waited longest.' },
407
+ today: todayOf(cf),
408
+ dock: { needs: pending ? null : rows.length + more, label: plural(rows.length + more, 'needs you', 'need you'), next: rows[0] ? rows[0].title : null },
409
+ products: { available: available.map((a) => ({ ws: a.ws, name: a.name, home: !!a.home, on: !!a.on })), none },
410
+ };
411
+ }
412
+
413
+ /* ── I/O ──────────────────────────────────────────────────────────────────────────────────────────────────── */
414
+ const readJsonl = (file) => { try { return fs.readFileSync(file, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return null; } };
415
+ const run = (cmd, args, opts) => new Promise((res) => {
416
+ const { nice, ...o } = opts || {};
417
+ const c = execFile(cmd, args, Object.assign({ maxBuffer: 64 * 1024 * 1024, timeout: 30 * 60 * 1000 }, o), (e, out) => res(e ? null : String(out)));
418
+ if (nice) { try { require('os').setPriority(c.pid, nice); } catch { /* a platform without it runs at normal priority */ } }
419
+ });
420
+
421
+ /** The kill conditions with a dated firing threshold (planning/okrs/kill-conditions.md summary table), and whether pm-decisions.md
422
+ * holds an entry for one on or after its date. null when the file cannot be read. */
423
+ function readTripwires(root) {
424
+ let kc, pm = '';
425
+ try { kc = fs.readFileSync(path.join(root, 'planning', 'okrs', 'kill-conditions.md'), 'utf8'); } catch { return null; }
426
+ try { pm = fs.readFileSync(path.join(root, 'pm-decisions.md'), 'utf8'); } catch { pm = ''; }
427
+ const blocks = pm.split(/^## /m).slice(1).map((b) => ({ date: (b.match(/^(\d{4}-\d{2}-\d{2})/) || [])[1], text: b }));
428
+ const out = [];
429
+ for (const l of kc.split('\n')) {
430
+ const m = /^\|\s*(KC\d+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/.exec(l);
431
+ if (!m) continue;
432
+ const d = /\*\*(\d{4}-\d{2}-\d{2})\*\*/.exec(m[3]);
433
+ if (!d) continue;
434
+ const decided = blocks.some((b) => b.date && b.date >= d[1] && new RegExp('\\b' + m[1] + '\\b').test(b.text));
435
+ out.push({ id: m[1], name: m[2], date: d[1], decided, threshold: m[3], cadence: m[4], fires: m[5] });
436
+ }
437
+ return out;
438
+ }
439
+
440
+ /** The few one-line frontmatter values the dialog shows, from an initiative record. null when the file cannot be read. */
441
+ function readFm(root, rel) {
442
+ let t;
443
+ try { t = fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return null; }
444
+ const m = /^---\n([\s\S]*?)\n---/.exec(t);
445
+ if (!m) return null;
446
+ const out = {};
447
+ for (const k of ['lane', 'parent_kr', 'status']) {
448
+ const r = new RegExp('^' + k + ':\\s*(.*)$', 'm').exec(m[1]);
449
+ if (r && r[1].trim()) out[k] = r[1].trim().replace(/^"(.*)"$/, '$1');
450
+ }
451
+ return out;
452
+ }
453
+
454
+ /** The slow read: every initiative's workflow state, from the engine (a git history walk). Runs at low priority so it does not starve the machine. null on failure. */
455
+ async function readInitiatives({ root, engine }) {
456
+ const wf = engine ? await run(process.execPath, [engine, '--json', '--root', root], { cwd: root, nice: 10 }) : null;
457
+ let list;
458
+ try { list = JSON.parse(wf).map((x) => ({ id: x.id, title: x.title, lane: x.lane, stage: x.stage, accepted: !!x.accepted, stopped: !!x.stopped, legacy: !!x.legacy, anchor: x.anchor, last_activity: x.last_activity, blockers: x.blockers || [], next: x.next, record: x.record, steps: (x.steps || []).map((s) => ({ key: s.key, status: s.status, next: s.next, rule: (s.basis && s.basis.rule) || s.rule, blocking: s.blocking })) })); } catch { return null; }
459
+ for (const i of list) if (i.record && !i.accepted && (i.lane === 'prototype' || i.steps.some((x) => x.status === 'blocked'))) i.fm = readFm(root, i.record);
460
+ return list;
461
+ }
462
+
463
+ /** What changes the engine's answer: the commit the repository is on, and any uncommitted change under planning/ or scripts/. null when git cannot say. */
464
+ async function repoKey(root) {
465
+ const head = await run('git', ['-C', root, 'rev-parse', 'HEAD']);
466
+ if (head === null) return null;
467
+ const st = await run('git', ['-C', root, 'status', '--porcelain', '--', 'planning', 'scripts']);
468
+ return head.trim() + ':' + require('crypto').createHash('sha1').update(st || '').digest('hex').slice(0, 12);
469
+ }
470
+
471
+ /** Today's calendar rows, from the calendar PMOS holds (one for the whole computer, not one per workspace). null when it cannot be read. */
472
+ function readMeetings(runtimeDir, now) {
473
+ try {
474
+ const CE = require('./calendar-events.js');
475
+ if (!runtimeDir) throw new Error('no runtime dir');
476
+ const all = CE.readAll(runtimeDir, { now });
477
+ return CE.eventsOn(all, dayKey(now)).map((e) => ({ start: Date.parse(e.start), end: Date.parse(e.end), title: e.title, with: e.with || '', initiative: e.initiative || null })).filter((e) => Number.isFinite(e.start));
478
+ } catch { return null; }
479
+ }
480
+
481
+ /** The quick read: runs, reviews, merges (with their files), the calendar and the kill conditions. No initiative state. */
482
+ async function readFast({ root, now, runtimeDir, calendar = true }) {
483
+ const f = { now: now || Date.now(), asOf: now || Date.now(), initiatives: [], runs: [], merges: [], evals: [], meetings: null, tripwires: null, missing: [] };
484
+ const runs = readJsonl(path.join(root, 'planning', 'runs', 'agent-runs.jsonl')); if (runs) f.runs = runs; else f.missing.push('runs');
485
+ const evals = readJsonl(path.join(root, 'planning', 'runs', 'evals.jsonl')); if (evals) f.evals = evals; else f.missing.push('evals');
486
+ const since = new Date(startOfDay(f.now)).toISOString();
487
+ const log = await run('git', ['-C', root, 'log', '--first-parent', '--since=' + since, '--format=%H%x09%ct%x09%s', (await run('git', ['-C', root, 'rev-parse', '--verify', '-q', 'origin/main'])) ? 'origin/main' : 'HEAD']);
488
+ if (log === null) f.missing.push('merges'); else f.merges = log.split('\n').filter(Boolean).map((l) => { const [h, t, ...s] = l.split('\t'); return { sha: h, at: Number(t) * 1000, title: s.join('\t') }; });
489
+ for (const m of f.merges.slice(0, 30)) {
490
+ const sh = await run('git', ['-C', root, 'show', '--numstat', '--format=%b', m.sha]);
491
+ if (sh === null) continue;
492
+ const lines = sh.split('\n'), nums = lines.filter((x) => /^(\d+|-)\t(\d+|-)\t/.test(x)), top = {};
493
+ let ins = 0, del = 0;
494
+ for (const x of nums) { const [a, d, ...fp] = x.split('\t'); ins += Number(a) || 0; del += Number(d) || 0; const pth = fp.join('\t').split('/'); const k = pth.length > 1 ? pth[0] : pth[0]; top[k] = (top[k] || 0) + 1; }
495
+ m.files = nums.length; m.ins = ins; m.del = del; m.dirs = Object.entries(top).sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
496
+ m.body = lines.filter((x) => !/^(\d+|-)\t(\d+|-)\t/.test(x)).join('\n').trim();
497
+ }
498
+ if (calendar) f.meetings = readMeetings(runtimeDir, f.now);
499
+ f.tripwires = readTripwires(root);
500
+ f.protoDecided = [...W.decidedPrototypes(root)];
501
+ f.defers = Object.fromEntries(W.readDefers(runtimeDir, f.now));
502
+ return f;
503
+ }
504
+
505
+
506
+ /** Both reads, for a caller that wants the whole facts (the tests). */
507
+ async function readSources({ root, now, engine, runtimeDir }) {
508
+ const f = await readFast({ root, now, runtimeDir });
509
+ const inits = await readInitiatives({ root, engine });
510
+ if (inits) f.initiatives = inits; else f.missing.push('workflow');
511
+ return f;
512
+ }
513
+
514
+ /* ── the route's kept answer ──────────────────────────────────────────────────────────────────────────────── */
515
+ /* ── the route's kept answer ─────────────────────────────────────────────────────────────────────────────
516
+ * A first read takes from about half a minute on a fast machine to much longer on a slow one, so no request waits for it.
517
+ * - an answer in memory is served at once, however old, and a read older than 5 minutes is refreshed in the background;
518
+ * - after a restart the last answer is read back from <runtimeDir>/briefing-premium-model.json (a derived cache, safe to delete);
519
+ * - with neither, the answer is { state: 'reading', elapsed_ms } at once and the read goes on in the background;
520
+ * - a read that fails is kept as { state: 'error', error } until the next try, so the page can say why. */
521
+ let kept = null, partial = null, inflight = null, startedAt = 0, lastError = null, lastErrorAt = 0;
522
+ const hooks = { build }; // a seam for the suite: a failing build
523
+ const RETRY_AFTER_ERROR_MS = 2 * 60 * 1000;
524
+ const STALE_MS = 5 * 60 * 1000;
525
+ const CACHE_FILE = 'briefing-premium-model.json';
526
+ const loadCache = (dir) => { try { const j = JSON.parse(fs.readFileSync(path.join(dir, CACHE_FILE), 'utf8')); return j && j.model && j.at && j.root ? j : null; } catch { return null; } };
527
+ const saveCache = (dir, k) => { try { fs.mkdirSync(dir, { recursive: true }); const f = path.join(dir, CACHE_FILE), t = f + '.' + process.pid + '.tmp'; fs.writeFileSync(t, JSON.stringify(k)); fs.renameSync(t, f); } catch { /* a cache that cannot be written is only a slower next start */ } };
528
+
529
+ /* One refresh. The quick read always runs. The slow one (every initiative's state) runs only when the repository changed since the last answer
530
+ * (same commit and no uncommitted change under planning/ or scripts/); otherwise the last initiatives are reused. With no earlier answer, the
531
+ * quick read's model is served as a partial one while the slow read goes on, so the page shows what it can at once and says what is pending. */
532
+ async function refresh({ root, engine, runtimeDir }) {
533
+ const key = await repoKey(root);
534
+ const f = await readFast({ root, now: Date.now(), runtimeDir });
535
+ let inits = kept && kept.root === root && key && kept.key === key && kept.ok && kept.initiatives ? kept.initiatives : null; // a failed slow read is never reused: it is tried again
536
+ if (!inits) {
537
+ if (!kept) partial = { at: Date.now(), root, model: hooks.build(Object.assign({}, f, { initiatives: [], pending: true })) };
538
+ inits = await readInitiatives({ root, engine });
539
+ }
540
+ if (!inits) f.missing.push('workflow');
541
+ kept = { at: Date.now(), root, key, ok: !!inits, initiatives: inits || [], model: hooks.build(Object.assign({}, f, { initiatives: inits || [] })) };
542
+ partial = null; lastError = null; lastErrorAt = 0;
543
+ if (runtimeDir) saveCache(runtimeDir, kept);
544
+ }
545
+ function modelOf({ root, engine, runtimeDir }) {
546
+ const start = () => {
547
+ // a read that just failed is not started again on every poll: the page shows why, and the next try waits
548
+ if (!inflight && !(lastError && Date.now() - lastErrorAt < RETRY_AFTER_ERROR_MS)) {
549
+ startedAt = Date.now();
550
+ inflight = refresh({ root, engine, runtimeDir }).catch((e) => { lastError = String((e && e.stack) || e).split('\n').slice(0, 3).join(' | ').slice(0, 300); lastErrorAt = Date.now(); }).finally(() => { inflight = null; });
551
+ }
552
+ return inflight;
553
+ };
554
+ if (kept && kept.root !== root) kept = null;
555
+ if (partial && partial.root !== root) partial = null;
556
+ if (!kept && runtimeDir) { const c = loadCache(runtimeDir); if (c && c.root === root) kept = c; }
557
+ if (kept) {
558
+ if (Date.now() - kept.at > STALE_MS) start();
559
+ return Promise.resolve({ state: 'ready', model: kept.model, as_of: kept.at, refreshing: !!inflight, ...(lastError ? { error: lastError } : {}) });
560
+ }
561
+ start();
562
+ if (partial) return Promise.resolve({ state: 'ready', model: partial.model, as_of: partial.at, pending: !(lastError && !inflight), elapsed_ms: Date.now() - startedAt, ...(lastError ? { error: lastError } : {}) });
563
+ if (lastError && !inflight) return Promise.resolve({ state: 'error', error: lastError });
564
+ return Promise.resolve({ state: 'reading', elapsed_ms: Date.now() - startedAt });
565
+ }
566
+ /** After a write: rebuild the kept answer from fresh records without the slow read. A caller waits for a read in progress first. */
567
+ async function rebuild({ root, runtimeDir }) {
568
+ await settle();
569
+ if (!kept || kept.root !== root) return false;
570
+ const f = await readFast({ root, now: Date.now(), runtimeDir });
571
+ if (!kept.ok) f.missing.push('workflow');
572
+ kept = Object.assign({}, kept, { model: hooks.build(Object.assign({}, f, { initiatives: kept.initiatives })) });
573
+ if (runtimeDir) saveCache(runtimeDir, kept);
574
+ return true;
575
+ }
576
+ /** The model the route holds for this root, for the agent to find a row in. null before the first read. */
577
+ const peekModel = (root) => (kept && kept.root === root ? kept.model : partial && partial.root === root ? partial.model : null);
578
+ const settle = () => inflight || Promise.resolve();
579
+ const forget = () => { kept = null; partial = null; inflight = null; lastError = null; startedAt = 0; };
580
+
581
+ module.exports = { build, buildMany, allRowsOf, readMeetings, hooks, refresh, readSources, readFast, readInitiatives, repoKey, readFm, readTripwires, modelOf, settle, forget, rebuild, peekModel, rowsOf, heroOf, todayOf, failedReviews, num };
@@ -323,7 +323,7 @@ function buildRange(root, { spec, whole }) {
323
323
 
324
324
  // Where each path was ADDED, as an index into the same range. `--diff-filter=A` in one pass.
325
325
  const added = new Map();
326
- const addLog = git(root, ['log', '--reverse', '--format=%x1e%H', '--diff-filter=A', '--name-only', range]);
326
+ const addLog = git(root, ['log', '--reverse', '--no-renames', '--format=%x1e%H', '--diff-filter=A', '--name-only', range]); // no rename detection: a path renamed into place is an add, as the per-path lookup below sees it
327
327
  if (addLog != null) {
328
328
  for (const chunk of addLog.split('\x1e')) {
329
329
  if (!chunk.trim()) continue;
@@ -358,6 +358,11 @@ function firstAppearance(root, range, rel) {
358
358
  // ever been committed — so the fallback below only runs for a PR range, where "added before the
359
359
  // range" and "never committed" are genuinely different answers and only git can tell them apart.
360
360
  if (range.added && range.added.has(rel)) return range.added.get(rel);
361
+ // A TRACKED path that this range did not add was added before it: -1, exactly what the per-path
362
+ // `git log` below would conclude, from the one shared `ls-files`. That per-path call was one
363
+ // subprocess per record on a branch ahead of main, and most of a board read's time.
364
+ const tracked = trackedIn(root, range);
365
+ if (tracked && tracked.has(rel)) return -1;
361
366
  const log = git(root, ['log', '--diff-filter=A', '--reverse', '--format=%H', '--', rel]);
362
367
  const sha = log ? log.split('\n').map((s) => s.trim()).filter(Boolean)[0] : null;
363
368
  if (!sha) return onDisk ? Infinity : null;
@@ -63,7 +63,7 @@ const HEADLESS_SKILLS = Object.freeze([
63
63
  // The note passes that run headless, by the origin prefix lib/routes-notes.js gives them.
64
64
  const HEADLESS_ORIGINS = Object.freeze(['notes-bullets:', 'notes-summary:']);
65
65
  // page-starts-agent: Ask the agent on a document. The run reads and answers with the revised text; it writes nothing.
66
- const ASK_ORIGINS = Object.freeze(['doc-ask:', 'focus-companion:']); // focus-companion: the Meeting focus page's quiet companion and its private questions (read-only, nothing sent)
66
+ const ASK_ORIGINS = Object.freeze(['doc-ask:', 'focus-companion:', 'briefing-ask:']); // focus-companion: the Meeting focus page's quiet companion and its private questions (read-only, nothing sent)
67
67
  // initiative-driver: one short run for one initiative or one Work task. Its origin is `drive:<kind>:<id>`.
68
68
  const DRIVE_ORIGINS = Object.freeze(['drive:']);
69
69
  // horizon-scan: a scan is two runs. The web run (origin `scan-web:<board>:<scan>`) reads the public web for the kept watch list and sees nothing of