create-pmos 0.514.0 → 0.516.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 (32) hide show
  1. package/package.json +35 -95
  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/state/discoveries.md +1 -1
  6. package/payload/state/log.md +1 -1
  7. package/workspace/briefing-premium-calendar-test.js +6 -3
  8. package/workspace/briefing-premium-insights-test.js +12 -12
  9. package/workspace/briefing-premium-metrics-test.js +1 -1
  10. package/workspace/changelog.json +34 -0
  11. package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
  12. package/workspace/guide/agent-capabilities.md +73 -0
  13. package/workspace/guide/index.md +4 -1
  14. package/workspace/guide/intelligence-okr-goals.md +39 -0
  15. package/workspace/guide/reports-needs-you.md +34 -0
  16. package/workspace/lib/board-metrics-premium.js +4 -1
  17. package/workspace/lib/insights-goals.js +3 -1
  18. package/workspace/lib/insights-premium.js +53 -7
  19. package/workspace/lib/okr-goals.js +200 -0
  20. package/workspace/lib/report-acts.js +189 -0
  21. package/workspace/lib/routes-insights-premium.js +44 -5
  22. package/workspace/lib/routes-ready-reports.js +8 -1
  23. package/workspace/lib/routes-report-setups.js +21 -1
  24. package/workspace/package-lock.json +2 -2
  25. package/workspace/package.json +1 -1
  26. package/workspace/premium-vw-meet-test.js +9 -5
  27. package/workspace/public/pm/cozy-insights.css +60 -1
  28. package/workspace/public/pm/home-app.js +3 -1
  29. package/workspace/public/pm/premium-insights.js +269 -36
  30. package/workspace/public/premium-intelligence.html +1 -1
  31. package/workspace/test-files.json +1 -0
  32. package/workspace/test-lists/reports-needs-you.json +5 -0
@@ -21,6 +21,8 @@ const { recordsDirFor } = require('./records-dir.js');
21
21
  const reports = require('./routes-reports.js');
22
22
  const setups = require('./routes-report-setups.js');
23
23
  const PD = require('./premium-dashboards.js');
24
+ /** The PMOS repository's own workspace: the agent measures (merges, corrections) are read only there. */
25
+ const isSelf = (ctx, b) => { try { return !!(ctx.selfRoot && b && require('path').resolve(ctx.getWs(b.ws).root) === require('path').resolve(ctx.selfRoot)); } catch { return false; } };
24
26
 
25
27
  /** GET `pathAndQuery` from a route module's own handler, as the Board page's route does, and return { status, body }. */
26
28
  async function via(mod, pathAndQuery, req, ctx) {
@@ -32,19 +34,40 @@ async function via(mod, pathAndQuery, req, ctx) {
32
34
  }
33
35
  /** The opening sentence of each listed report file (insights-one-list L6), read from the workspace's repository. A file that cannot be read has none. */
34
36
  const LEADS_MAX = 60;
35
- function leadsOf(ctx, ws, list) {
37
+ function leadsOf(ctx, ws, list, shas) {
36
38
  const out = {};
37
39
  let root = null;
38
40
  try { root = require('path').resolve(ctx.getWs(ws).root); } catch { return out; }
39
41
  for (const r of list.slice().sort((a, b) => String(b.slot).localeCompare(String(a.slot))).slice(0, LEADS_MAX)) {
40
42
  const rel = String(r.path || '');
41
43
  if (!rel || rel.includes('..') || require('path').isAbsolute(rel)) continue;
42
- try { out[rel] = Insights.leadOf(require('fs').readFileSync(require('path').join(root, rel), 'utf8').slice(0, 20000)); } catch { /* an unreadable file has no lead */ }
44
+ try {
45
+ const buf = require('fs').readFileSync(require('path').join(root, rel));
46
+ // Reports and dashboards redesign R: the headline is the preview, never the line that says the definition is a draft
47
+ out[rel] = require('./report-acts.js').leadPastWarning(buf.toString('utf8').slice(0, 20000), Insights.leadOf);
48
+ if (shas) shas[rel] = require('crypto').createHash('sha1').update(`blob ${buf.length}\0`).update(buf).digest('hex'); // what Issue sends: the file as read
49
+ } catch { /* an unreadable file has no lead */ }
43
50
  }
44
51
  return out;
45
52
  }
46
53
  /** The product's open drafts: the ✦ line's ✓✓ count (lib/metric-drafts.js). */
47
54
  const openDrafts = (dir, ws) => { const MD = require('./metric-drafts.js'), S = MD.read(dir).scopes.get(MD.keyOf('product', ws)); return S ? [...S.rows.values()].filter((r) => r.state === 'open').length : 0; };
55
+ /** The health budget registry (D59): planning/okrs/health-budgets.md, or pmos/planning/okrs/ in a product repository (the kit layout). Without
56
+ * the second path every product said "not in the budget registry" although it declares one (Intelligence grill, 3 Nov). null when neither reads. */
57
+ function readBudgets(root) {
58
+ const P = require('path'), IG = require('./insights-goals.js');
59
+ for (const f of [P.join(root, 'planning', 'okrs', 'health-budgets.md'), P.join(root, 'pmos', 'planning', 'okrs', 'health-budgets.md')]) {
60
+ try { return IG.parseBudgets(require('fs').readFileSync(f, 'utf8')); } catch { /* the next place */ }
61
+ }
62
+ return null;
63
+ }
64
+ /** Each initiative's D57 promise: { [id]: { metric, horizon, disposition } } from lib/outcomes.js scan. */
65
+ function promisesOf(S) {
66
+ const out = {};
67
+ for (const it of (S && S.subjects) || []) out[it.id] = { metric: it.metric || '', horizon: it.horizon || '', disposition: null };
68
+ for (const it of (S && S.disposed) || []) if (out[it.id]) out[it.id].disposition = it.disposition;
69
+ return out;
70
+ }
48
71
  const qs = (o) => '?' + Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
49
72
 
50
73
  module.exports = {
@@ -78,7 +101,11 @@ module.exports = {
78
101
  const su = await via(setups, '/api/report-setups' + q, req, ctx).catch(() => ({ status: 500, body: null }));
79
102
  const setupsOk = su.status === 200 && !!su.body && Array.isArray(su.body.setups);
80
103
  if (!setupsOk) missing.push('report setups');
81
- const leads = reportsOk ? leadsOf(ctx, ws, rp.body.reports || []) : {};
104
+ const shas = {};
105
+ const leads = reportsOk ? leadsOf(ctx, ws, rp.body.reports || [], shas) : {};
106
+ // Reports and dashboards redesign R: each report's definition (its title names it; status: draft is "Confirm the definition")
107
+ let defs = [];
108
+ if (pick) { try { defs = require('./report-setups.js').definitionsOf(require('path').resolve(ctx.getWs(pick.ws).root)).map((d) => ({ id: d.id, title: d.title, status: d.status })); } catch { defs = []; } }
82
109
  // premium-agreed-design (G1, G2): the board's initiatives, so a goal can name what moves it and Outcomes can list shipped work. Read-only.
83
110
  let initiatives = null;
84
111
  if (pick && pick.id) {
@@ -91,7 +118,14 @@ module.exports = {
91
118
  if (pick) {
92
119
  const root = require('path').resolve(ctx.getWs(pick.ws).root);
93
120
  try { ins = require('./insights-records.js').read(root); } catch { missing.push('insights records'); }
94
- try { budgets = require('./insights-goals.js').parseBudgets(require('fs').readFileSync(require('path').join(root, 'planning', 'okrs', 'health-budgets.md'), 'utf8')); } catch { budgets = null; }
121
+ budgets = readBudgets(root);
122
+ }
123
+ // Intelligence redesign A: the OKR file is the goal list (lib/okr-goals.js), and each initiative's promise is its D57 outcome (lib/outcomes.js scan)
124
+ let okr = null, promises = null;
125
+ if (pick) {
126
+ const root = require('path').resolve(ctx.getWs(pick.ws).root);
127
+ try { okr = require('./okr-goals.js').read(root); } catch { okr = null; }
128
+ try { promises = promisesOf(require('./outcomes.js').scan(root)); } catch { promises = null; }
95
129
  }
96
130
  // premium-uncovered-features (IsMenus): the product picker says how many goals each product's workspace records; a workspace that cannot be read says none
97
131
  const counts = new Map();
@@ -110,8 +144,13 @@ module.exports = {
110
144
  watch,
111
145
  now: Date.now(), board: pick ? { ws: pick.ws, id: pick.id, name: pick.name } : { ws, id: null, name: 'Intelligence' }, boards: all.map((x) => Object.assign({ ws: x.ws, id: x.id, name: x.name }, goalsIn(x.ws))),
112
146
  outcomes, reports: reportsOk ? rp.body.reports || [] : [], reportsOk, missing, saved, mine,
113
- setups: setupsOk ? su.body : null, pinned, leads, archived, initiatives, goalMeta, ins, budgets,
147
+ setups: setupsOk ? su.body : null, pinned, leads, archived, initiatives, goalMeta, ins, budgets, okr, promises, self: isSelf(ctx, pick),
114
148
  });
149
+ // Reports and dashboards redesign R (RgR1Panel): each waiting report's one ✓, its display name, and the other product it is about (lib/report-acts.js)
150
+ if (model && model.reports) {
151
+ const R = require('./report-acts.js').reportActsOf({ rows: model.reports.rows || [], setups: model.reports.setups || [], defs, boards: (model.board && model.board.switcher) || [], here: model.board ? model.board.ws : null, shas });
152
+ Object.assign(model.reports, { acts: R.acts, names: R.names, about: R.about });
153
+ }
115
154
  // batch 16 item 1: each ✦ strip finding about a goal that is behind carries one drafted move (lib/insight-moves.js), done ones marked
116
155
  if (pick) {
117
156
  try {
@@ -225,7 +225,14 @@ async function dashboardAnswer(req, ctx, w, scope, from) {
225
225
  }
226
226
  if (!X.model) X.f.goalDraft = null;
227
227
  else if (!RR.goalsOf(X.f.intel, scope, Date.parse(X.f.today + 'T00:00:00Z'), false).goals.length) X.f.goalDraft = { title: '', sub: '', anchor: scope };
228
- return { status: 200, body: { ok: true, dashboard: RR.dashboardOf(X.f) } };
228
+ const d = RR.dashboardOf(X.f);
229
+ // Reports and dashboards redesign R (RgR4Make): a product page's ✦ line is Intelligence's line for that product (lib/report-acts.js). A held
230
+ // source keeps its own warning; "ready to send" follows only in a week with news (something behind, a ticket closed, an update drafted)
231
+ if (d && d.level === 'product' && !d.heldBy) {
232
+ const R = d.reports || [], news = (d.goals || []).some((g) => g.dot === 'behind') || !!d.done || R.some((r) => r.kind === 'weekly' && r.state === 'drafted' && !r.desk);
233
+ d.handled = require('./report-acts.js').productLineOfIntel(X.f.intel, { deskReady: R.some((r) => r.kind === 'weekly' && r.state === 'drafted' && r.desk), news, fallback: d.handled });
234
+ }
235
+ return { status: 200, body: { ok: true, dashboard: d } };
229
236
  }
230
237
 
231
238
  /** The scopes of a workspace: its product, its programs (the ones whose charter is here) and its live initiatives. */
@@ -17,6 +17,9 @@
17
17
  * POST /api/report-setups/pause | resume { ws, id } → { ok, setup }
18
18
  * POST /api/report-setups/edit { ws, id, schedule?, name?, audience?, output? } → { ok, setup } change a TEMPLATE (premium-reports-from-chat L1).
19
19
  * schedule { cadence: 'off' } pauses it; any other schedule changes it and resumes it
20
+ * POST /api/report-setups/confirm { ws, id } → { ok, path, sha } ✓ Confirm the definition: status: draft → stable (redesign R)
21
+ * POST /api/report-setups/move { ws, id, to } → { ok, id, to, moved } ✦ Move to <product>: definition, drafts and setup go to
22
+ * the workspace `to`; a report with an issued issue is 409 (redesign R)
20
23
  * POST /api/report-setups/remove { ws, id } → { ok } delete a TEMPLATE: its schedule stops; its drafts and issued reports stay (L2)
21
24
  * POST /api/report-setups/from-dashboard { ws, key, name, output, schedule, audience } → { ok, setup }
22
25
  * POST /api/report-setups/finder { ws, cadence?, sources? } → { ok, finder }
@@ -62,7 +65,7 @@ const headless = require('./headless.js');
62
65
 
63
66
  const text = (v) => (typeof v === 'string' ? v : '');
64
67
  const bad = (status, error) => ({ status, body: { ok: false, error } });
65
- const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder', 'edit', 'remove', 'documents/discard', 'documents/archive', 'strip-dismiss'];
68
+ const POSTS = ['ask', 'change', 'cancel', 'setup', 'draft', 'dismiss', 'pause', 'resume', 'from-dashboard', 'finder', 'edit', 'remove', 'documents/discard', 'documents/archive', 'strip-dismiss', 'confirm', 'move'];
66
69
  const IR = require('./intel-report.js');
67
70
  const MADE_BY = ['schedule', 'you', 'agent'];
68
71
 
@@ -508,6 +511,23 @@ async function handle(req, res, url, ctx) {
508
511
  const now = new Date();
509
512
  return send({ status: 200, body: { ok: true, setup: setupOut(RS.fold(RS.readLines(file)).setups.get(id), now, documentsOf(ctx, w), copyIndex(root), root) } });
510
513
  }
514
+ if (sub === 'confirm') {
515
+ // Reports and dashboards redesign R: ✓ Confirm the definition (status: draft → stable). Only the PM's press confirms one (report-daily.skill)
516
+ const r = require('./report-acts.js').confirmDefinition(root, text(body.id), opts);
517
+ if (!r.ok) return fail(r);
518
+ if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
519
+ return send({ status: 200, body: r });
520
+ }
521
+ if (sub === 'move') {
522
+ // Reports and dashboards redesign R: ✦ Move to <product>. The target is a workspace this server knows; an issued report is never moved (409)
523
+ const t = wsOf(ctx, text(body.to));
524
+ if (!t || !text(body.to)) return send(bad(404, 'no such product to move it to'));
525
+ if (t.id === w.id) return send(bad(400, 'it is in that product already'));
526
+ const r = require('./report-acts.js').moveReportTo(root, text(body.id), t.root, Object.assign({}, opts, { toWsId: t.id }));
527
+ if (!r.ok) return fail(r);
528
+ if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
529
+ return send({ status: 200, body: Object.assign({ to: t.id }, r) });
530
+ }
511
531
  if (sub === 'remove') {
512
532
  const r = RS.remove(root, text(body.id), opts);
513
533
  if (!r.ok) return fail(r);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.514.0",
3
+ "version": "0.516.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.514.0",
9
+ "version": "0.516.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.514.0",
3
+ "version": "0.516.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",
@@ -39,6 +39,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
39
39
  srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
40
40
  await T.waitForServer(BASE);
41
41
  browser = await chromium.launch({ executablePath: exe });
42
+ /* One clock for the page and the stand-ins: today at 11:00, so "Rail review" (30 minutes on) is today at any hour the suite runs.
43
+ * Unpinned, after 23:30 it fell on tomorrow and S6 lost its Record. */
44
+ const NOW = new Date(); NOW.setHours(11, 0, 0, 0);
45
+ const open = (u, o) => F.open(browser, BASE, u, { now: NOW, ...o });
42
46
  const body = (p) => p.evaluate(() => (document.querySelector('#czViewer .cz-vw-body') || {}).innerText || '');
43
47
  const press = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
44
48
  const folds = (p) => p.evaluate(() => [...document.querySelectorAll('#czViewer [data-czs-fold]')].map((x) => x.dataset.czsFold + (x.getAttribute('aria-expanded') === 'true' ? '*' : '')).join());
@@ -46,7 +50,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
46
50
  const sum = (p) => p.evaluate(() => { const s = [...document.querySelectorAll('#czViewer .czs-sum')].find((x) => x.offsetParent) || document.querySelector('#czViewer .czs-sum'); return s ? { line: (s.querySelector('.czs-line') || {}).innerText || '', text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
47
51
 
48
52
  /* an ended meeting with notes and a recording */
49
- const A = await F.open(browser, BASE, '/premium-viewer.html?open=meeting&note=n1');
53
+ const A = await open('/premium-viewer.html?open=meeting&note=n1');
50
54
  const p = A.page;
51
55
  const up = await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="notes"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
52
56
  ok('0 the meeting opens in the Viewer on its own page', up);
@@ -128,7 +132,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
128
132
  await A.ctx.close();
129
133
 
130
134
  /* S6 Before: a meeting to come, no note */
131
- const B = await F.open(browser, BASE, '/premium-viewer.html?open=meeting&event=ev-rail&date=' + F.dayKey(F.world().RAIL), { scene: 'prep' });
135
+ const B = await open('/premium-viewer.html?open=meeting&event=ev-rail&date=' + F.dayKey(F.world(NOW).RAIL), { scene: 'prep' });
132
136
  const q = B.page;
133
137
  const prepUp = await until(() => q.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="prep"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
134
138
  const s6 = await sum(q);
@@ -169,7 +173,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
169
173
  await B.ctx.close();
170
174
 
171
175
  /* S7 During: a note that records */
172
- const C = await F.open(browser, BASE, '/premium-viewer.html?open=meeting&note=n2', { init: () => { window.__premiumHolding = { id: 'n2' }; } });
176
+ const C = await open('/premium-viewer.html?open=meeting&note=n2', { init: () => { window.__premiumHolding = { id: 'n2' }; } });
173
177
  const r = C.page;
174
178
  const liveUp = await until(() => r.evaluate(() => !!document.querySelector('#czViewer .vz-mtg[data-mk="live"]') && !!document.querySelector('#czViewer .czs-sum')), 15000);
175
179
  const s7 = await sum(r);
@@ -196,7 +200,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
196
200
  await C.ctx.close();
197
201
 
198
202
  /* S10 a note that is not there */
199
- const D = await F.open(browser, BASE, '/premium-viewer.html?open=meeting&note=nope');
203
+ const D = await open('/premium-viewer.html?open=meeting&note=nope');
200
204
  ok('S10 a note that is not there says so, and the card still opens', await until(() => D.page.evaluate(() => !!document.querySelector('#czViewer') && /Untitled note|could not|not/.test(document.querySelector('#czViewer').innerText))));
201
205
  await D.ctx.close();
202
206
 
@@ -204,7 +208,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
204
208
  // the meeting's own presses: the summary's (the shell's primary button is the shell's) and the folds; a press may keep its drawn size and take a 44 px hit area from ::before
205
209
  const HITS = () => { window.hits = () => [...document.querySelectorAll('#czViewer .czs-sum button:not(.czs-pri), #czViewer [data-czs-fold]')].filter((b) => { if (!b.getClientRects().length) return false; const pb = getComputedStyle(b, '::before'), hit = pb.content !== 'none' && pb.position === 'absolute' ? parseFloat(pb.height) || 0 : 0; return Math.max(b.getBoundingClientRect().height, hit) < 44; }).map((b) => (b.dataset.a || b.dataset.czsFold || b.className) + ':' + Math.round(b.getBoundingClientRect().height)); };
206
210
  for (const [u, o] of [['/premium-viewer.html?open=meeting&note=n1', {}], ['/premium-viewer.html?open=meeting&note=n2', { init: () => { window.__premiumHolding = { id: 'n2' }; } }]]) {
207
- const E = await F.open(browser, BASE, u, { w: 390, h: 844, ...o });
211
+ const E = await open(u, { w: 390, h: 844, ...o });
208
212
  await E.page.evaluate(HITS);
209
213
  await until(() => E.page.evaluate(() => !!document.querySelector('#czViewer .vz-mtg') && !!document.querySelector('#czViewer .czs-sum')), 15000);
210
214
  const fl = await until(async () => { const v = await E.page.evaluate(() => ({ over: document.documentElement.scrollWidth - innerWidth, full: (() => { const r = document.querySelector('#czViewer').getBoundingClientRect(); return r.left === 0 && Math.round(r.width) === innerWidth; })(),
@@ -75,7 +75,7 @@ body.cz { padding: 0; }
75
75
  .ins-tbl { display: flex; flex-direction: column; border: 1px solid var(--cz-hair); border-radius: 18px; overflow: hidden; background: var(--cz-card); }
76
76
  .ins-tr { display: grid; align-items: center; column-gap: 16px; padding: 0 16px; }
77
77
  .ins-cols { grid-template-columns: minmax(210px, 270px) 190px 230px 170px minmax(0, 1fr); }
78
- .ins-ocols { grid-template-columns: minmax(200px, 270px) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr) 96px; }
78
+ .ins-ocols { grid-template-columns: minmax(200px, 250px) minmax(0, 1.3fr) minmax(0, .7fr) minmax(0, 1fr) minmax(0, 1.2fr) 96px; } /* Promised · Check · Got · Verdict (IgA3Outcomes) */
79
79
  .ins-dcols { grid-template-columns: minmax(200px, 270px) 110px 150px minmax(0, 1fr) 150px 112px; }
80
80
  .ins-lcols { grid-template-columns: 40px minmax(0, 1fr) 170px 200px 190px 90px; }
81
81
  .ins-th { background: var(--cz-well); font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-ink2); min-height: 36px; line-height: var(--lh-label); }
@@ -665,6 +665,7 @@ body.ins-trv #goalTbl { border: 0; background: none; border-radius: 0; overflow:
665
665
  /* Reports and dashboards (⋯): the old Library in a side panel */
666
666
  .ins-lib { width: min(560px, 100%); }
667
667
  .ins-lib .ins-sb { display: flex; flex-direction: column; gap: 12px; }
668
+ .ins-lib .ins-sb > * { flex-shrink: 0; } /* the panel scrolls; a table never shrinks under the block after it (Needs you) */
668
669
  .ins-lib .ins-dgrid { grid-template-columns: minmax(0, 1fr); }
669
670
  /* motion (design principle 12 as amended for Intelligence): a soft rise as each section arrives, once; nothing loops; reduced motion turns it off */
670
671
  .ins > .ins-strip, .ins > .ins-ns, .ins > .ins-dr, .ins > .ins-pane { animation: ins-rise .45s cubic-bezier(.2, .8, .2, 1) both; }
@@ -930,3 +931,61 @@ body.ins-trv #pGoals .ins-trstrip { display: flex; } /* the narrow form hides
930
931
  .ins-mrow b { margin-left: auto; }
931
932
  .ins-mrow .cz-pill { margin-left: auto; }
932
933
  .ins-trstrip .t { display: block; -webkit-line-clamp: unset; overflow: visible; }
934
+
935
+ /* ── Intelligence redesign A: the OKR file is the goal list (IgA1Open, IgA2Goal, IgA3Outcomes, IgA7Phone) ── */
936
+ .ins-krt { font-size: var(--fs-badge); font-weight: var(--fw-regular); color: var(--cz-ink3); margin-right: 6px; line-height: var(--lh-badge); }
937
+ .ins-need { display: inline-flex; align-items: center; gap: 6px; }
938
+ .ins-chk { display: inline-flex; align-items: center; gap: 6px; }
939
+ .ins-krcon { display: flex; align-items: center; gap: 10px; background: var(--cz-bluebg); border-radius: 12px; padding: 8px 10px 8px 12px; margin: 10px 0; font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--cz-ink2); }
940
+ .ins-krcon .sp { color: var(--cz-blueink); display: inline-flex; }
941
+ .ins-krcon .sp svg { width: 14px; height: 14px; }
942
+ .ins-krcon .cz-ib { margin-left: auto; }
943
+ .ins-cfold { display: block; border: 1px solid var(--cz-hair); border-radius: 18px; background: var(--cz-card); padding: 11px 14px; }
944
+ .ins-cfold > summary { cursor: pointer; font-size: var(--fs-row); line-height: var(--lh-row); }
945
+ .ins-cfold[open] > summary { margin-bottom: 8px; }
946
+ .ins-cfold .ins-card { margin-top: 8px; }
947
+ .ins-libfold { display: block; border: 1px solid var(--cz-hair); border-radius: 16px; padding: 10px 14px; margin: 8px 0; }
948
+ .ins-libfold > summary { cursor: pointer; font-size: var(--fs-row); line-height: var(--lh-row); }
949
+ .ins-libfold[open] > summary { margin-bottom: 8px; }
950
+ .ins-pcols { grid-template-columns: minmax(160px, 220px) minmax(0, 1.2fr) 130px 70px 110px minmax(0, 1.6fr); }
951
+ .ins-allp { margin-bottom: 14px; }
952
+ .ins-allp .ins-row { cursor: pointer; }
953
+ .ins-pav { width: 26px; height: 26px; border-radius: 50%; background: var(--cz-well2); color: var(--cz-ink2); font-size: var(--fs-badge); font-weight: var(--fw-strong); display: inline-flex; align-items: center; justify-content: center; flex: none; }
954
+ @media (max-width: 720px) {
955
+ .ins-row.ins-pcols { grid-template-columns: minmax(0, 1fr); row-gap: 6px; border: 1px solid var(--cz-hair); border-radius: 18px; background: var(--cz-card); padding: 12px 14px; }
956
+ .ins-allp .ins-th { display: none; }
957
+ }
958
+
959
+ /* Reports and dashboards redesign R (RgR1Panel, RgR3Dash): Needs you rows with one ✓ each, the two ready-made folds, the drafted first dashboard */
960
+ .ins-nlist { flex: none; display: flex; flex-direction: column; border: 1px solid var(--cz-hair); border-radius: 16px; overflow: hidden; margin: 4px 0 8px; }
961
+ .ins-nrow { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-top: 1px solid var(--cz-hair); }
962
+ .ins-nrow:first-child { border-top: 0; }
963
+ .ins-nrow.sel { background: var(--cz-bluebg); }
964
+ .ins-nrow .nm { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; border: 0; background: none; padding: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; }
965
+ .ins-nrow .nm b { font-size: var(--fs-body); font-weight: var(--fw-strong); line-height: var(--lh-body); }
966
+ .ins-nrow .nm small { font-size: var(--fs-meta); color: var(--cz-ink3); line-height: var(--lh-meta); }
967
+ .ins-nrow .hl { font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
968
+ .ins-nrow .ac { flex: none; display: flex; align-items: center; gap: 8px; }
969
+ .ins-nrow .ac .note { white-space: nowrap; }
970
+ @media (hover: hover) { .ins-nrow:not(.sel):hover { background: rgba(244, 243, 240, .5); } }
971
+ .ins-mlist { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-meta); line-height: var(--lh-meta); }
972
+ .ins-libfold[open] .ins-mlist { display: none; }
973
+ .ins-libfold:not([open]) .ins-libin { display: none; }
974
+ .ins-ddraft { grid-column: 1 / -1; cursor: default; }
975
+ .ins-dtiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
976
+ .ins-dtile { position: relative; display: flex; flex-direction: column; gap: 2px; border: 1px solid var(--cz-hair); border-radius: 14px; padding: 10px 12px; min-width: 0; }
977
+ .ins-dtile.w { border-style: dashed; border-color: var(--cz-hair2, rgba(42,46,53,.14)); }
978
+ .ins-dtile .lb { font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
979
+ .ins-dtile .kr { color: var(--cz-ink3); margin-right: 6px; }
980
+ .ins-dtile .big { font-size: var(--fs-panel); font-weight: var(--fw-strong); line-height: var(--lh-panel, 1.3); }
981
+ .ins-dtile .ft { display: flex; align-items: center; gap: 6px; justify-content: space-between; }
982
+ .ins-dtile .ft .cz-ib { flex: none; }
983
+ .ins-tdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; background: var(--cz-dot-q); }
984
+ .ins-tdot.ok { background: var(--cz-dot-ok); } .ins-tdot.risk { background: var(--cz-dot-risk); } .ins-tdot.late { background: var(--cz-dot-late); }
985
+ .ins-dact { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
986
+ @media (max-width: 760px) {
987
+ .ins-nlist { border: 0; gap: 8px; overflow: visible; }
988
+ .ins-nrow { border: 1px solid var(--cz-hair); border-radius: 16px; background: var(--cz-card); }
989
+ .ins-nrow:first-child { border-top: 1px solid var(--cz-hair); }
990
+ .ins-nrow .ac .note { display: none; }
991
+ }
@@ -986,7 +986,9 @@ function productPageHtml(x, ws, vid, name, g) {
986
986
  const tRow = (t) => row(`data-hp="ini" data-k="${esc(t.ini)}"`, '', t.name, t.of, t.late && t.due ? `<span class="pv-late">${esc(lateDays(t.due) + ' days late')}</span>` : '');
987
987
  const U = own ? H.weeklyUpdate(S.D, S.D.now) : null, sent = U && (U.sent || []).some((y) => y.week === U.week.iso);
988
988
  const R = d ? DB.reportsHtml(d, { open: x.open }) : '';
989
- const extra = own ? (R ? '' : row('data-hp="update"', '', 'Weekly update', sent ? 'Sent this week' : 'Ready to send'))
989
+ // redesign R (RgR4Make): "Ready to send" only in a week with news (something at risk or waiting for a decision); a quiet week says so
990
+ const news = U && U.sections.some(([k, , L]) => k !== 'ok' && L.length);
991
+ const extra = own ? (R ? '' : row('data-hp="update"', '', 'Weekly update', sent ? 'Sent this week' : news ? 'Ready to send' : 'Nothing new this week · skipped'))
990
992
  : `<a class="pv-row" href="${esc(ownHome)}" target="_blank" rel="noopener"><span class="pv-rt"><b>Weekly update</b><small>In ${esc(name)}'s own Home, in a new tab</small></span></a>`;
991
993
  const reps = ((d && d.reports) || []).filter((r) => r.kind !== 'heads-up');
992
994
  const tabs = [