create-pmos 0.513.0 → 0.515.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.513.0",
3
+ "version": "0.515.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -32,6 +32,32 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.515.0",
37
+ "at": "2026-11-03T23:11:51.264Z",
38
+ "f": 4,
39
+ "prs": [
40
+ {
41
+ "n": 1206,
42
+ "t": "Intelligence reads the OKR file as its goal list (redesign A)",
43
+ "s": "Intelligence reads the OKR file as its goal list (redesign A)",
44
+ "k": "new"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "v": "0.514.0",
50
+ "at": "2026-11-03T16:19:12.685Z",
51
+ "f": 4,
52
+ "prs": [
53
+ {
54
+ "n": 1205,
55
+ "t": "Closed also reads the Done category and terminal stage names",
56
+ "s": "Closed also reads the Done category and terminal stage names",
57
+ "k": "fix"
58
+ }
59
+ ]
60
+ },
35
61
  {
36
62
  "v": "0.513.0",
37
63
  "at": "2026-11-03T15:46:14.696Z",
@@ -436,132 +462,6 @@
436
462
  "d": "A page stayed blank while it read its data. Now it shows its shape right away and fills in as data arrives."
437
463
  }
438
464
  ]
439
- },
440
- {
441
- "v": "0.495.0",
442
- "at": "2026-11-02T23:31:50.485Z",
443
- "f": 4,
444
- "prs": [
445
- {
446
- "n": 1164,
447
- "t": "Report fixes from the live check of 0.494.0",
448
- "s": "Report fixes from the live check of 0.494.0",
449
- "k": "fix",
450
- "h": "Report fixes from a live check",
451
- "d": "A live check of reports found several small problems. They are fixed now."
452
- },
453
- {
454
- "n": 1167,
455
- "t": "Keep a live New workflow draft on the Workflows list",
456
- "s": "Keep a live New workflow draft on the Workflows list",
457
- "k": "fix",
458
- "h": "New workflow drafts stay put",
459
- "d": "A workflow draft you were making could vanish from the list. Now it stays there until you finish or remove it."
460
- },
461
- {
462
- "n": 1166,
463
- "t": "Initiative telemetry card starts collapsed",
464
- "s": "Initiative telemetry card starts collapsed",
465
- "k": "fix",
466
- "h": "A quieter initiative page",
467
- "d": "The progress card opened fully on every initiative. Now it starts folded, and you open it when you want it."
468
- },
469
- {
470
- "n": 1168,
471
- "t": "A sent chat message shows at once",
472
- "s": "A sent chat message shows at once",
473
- "k": "fix",
474
- "h": "Sent messages show at once",
475
- "d": "A chat message you sent took a moment to appear. Now it shows the moment you send it."
476
- },
477
- {
478
- "n": 1169,
479
- "t": "A default workflow, set with a star in Settings › Workflows",
480
- "s": "A default workflow, set with a star in Settings › Workflows",
481
- "k": "new",
482
- "h": "Pick a default workflow",
483
- "d": "Every new initiative needed its workflow chosen by hand. Now you star one in Settings and new work uses it."
484
- }
485
- ]
486
- },
487
- {
488
- "v": "0.494.0",
489
- "at": "2026-11-02T15:38:38.702Z",
490
- "f": 4,
491
- "prs": [
492
- {
493
- "n": 1156,
494
- "t": "A report change from the chat edits the existing draft in place",
495
- "s": "A report change from the chat edits the existing draft in place",
496
- "k": "fix",
497
- "h": "Report edits from chat change the draft",
498
- "d": "Asking the chat to change a report could start a new draft. Now it edits the draft you already have."
499
- },
500
- {
501
- "n": 1150,
502
- "t": "A report's chat icon opens its own conversation beside it",
503
- "s": "A report's chat icon opens its own conversation beside it",
504
- "k": "new",
505
- "h": "Talk about a report beside it",
506
- "d": "A report had no place of its own to discuss it. Now its chat icon opens a conversation next to the report."
507
- },
508
- {
509
- "n": 1149,
510
- "t": "Product groups use the product name, not the folder name",
511
- "s": "Product groups use the product name, not the folder name",
512
- "k": "fix",
513
- "h": "Products show their real names",
514
- "d": "Board groups showed the folder name of a product. Now they show the product's own name."
515
- },
516
- {
517
- "n": 1157,
518
- "t": "Weekly impact check as thank-you bubbles, counted by default",
519
- "s": "Weekly impact check as thank-you bubbles, counted by default",
520
- "k": "new",
521
- "h": "Your weekly impact, as thank-yous",
522
- "d": "The weekly impact check was easy to miss. Now it arrives as small thank-you bubbles and is counted by default."
523
- },
524
- {
525
- "n": 1158,
526
- "t": "Grill every initiative with open work, not only late work; Home says how many",
527
- "s": "Grill every initiative with open work, not only late work; Home says how many",
528
- "k": "fix",
529
- "h": "Every open initiative gets a grill",
530
- "d": "Claude only grilled initiatives that were late. Now it grills every initiative with open work, and Home says how many."
531
- },
532
- {
533
- "n": 1159,
534
- "t": "Pages open faster after a break and after a start",
535
- "s": "Pages open faster after a break and after a start",
536
- "k": "imp",
537
- "h": "Pages open faster",
538
- "d": "Pages were slow after a break or a fresh start. Now they open quicker in both cases."
539
- },
540
- {
541
- "n": 1161,
542
- "t": "A report template keeps its look and sections",
543
- "s": "A report template keeps its look and sections",
544
- "k": "new",
545
- "h": "Report templates keep their look",
546
- "d": "A report made from a template could lose the template's look and sections. Now it keeps both."
547
- },
548
- {
549
- "n": 1162,
550
- "t": "Finished report slides, the Pyramid deck",
551
- "s": "Finished report slides, the Pyramid deck",
552
- "k": "new",
553
- "h": "Finished report slides",
554
- "d": "Reports could not become a finished slide deck. Now a report can come out as clean slides in the Pyramid style."
555
- },
556
- {
557
- "n": 1160,
558
- "t": "Tighter phone initiative page, one 16 px inset",
559
- "s": "Tighter phone initiative page, one 16 px inset",
560
- "k": "new",
561
- "h": "A tidier initiative page on the phone",
562
- "d": "On a phone the initiative page had uneven edges. Now everything lines up on one even margin."
563
- }
564
- ]
565
465
  }
566
466
  ]
567
467
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@d90291c5
1
+ kernel-source: pmos@49a5661d
2
2
  assembled: 2026-11-03
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-11-03T15:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T23:11:47Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-11-03T15:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T23:11:47Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-11-03T15:46:12Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T23:11:47Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -47,7 +47,7 @@ const finish = async (code) => {
47
47
  process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
48
48
 
49
49
  const BUDGETS = [{ id: 'docs', covers: 'documentation upkeep', cap: 0.2 }, { id: 'ops', covers: 'pipelines and deploys', cap: 0.1 }]; // as parseBudgets reads planning/okrs/health-budgets.md
50
- const mk = (o) => IP.build(F.facts(Object.assign({ pinned: 'all', budgets: BUDGETS }, o))); // All goals pinned, so Goals opens on every goal
50
+ const mk = (o) => IP.build(F.facts(Object.assign({ pinned: 'all', budgets: BUDGETS, self: true }, o))); // the PMOS workspace itself: its own agent tiles show (redesign A) // All goals pinned, so Goals opens on every goal
51
51
  /* the Board's measures, as GET /api/board-metrics-premium answers them: two with a series, one to look at, one the engine has not derived */
52
52
  const BMR = { ok: true, state: 'ready', model: { tiles: [
53
53
  { key: 'corrections', value: '2.6', ser: [3.1, 2.9, 2.6] }, { key: 'merges', value: '174', ser: [150, 160, 174] },
@@ -351,7 +351,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
351
351
  {
352
352
  const R = await open(), q = R.page;
353
353
  const o = await q.evaluate(() => ({ cols: [...document.querySelectorAll('#outTbl .ins-th [role="columnheader"]')].map((c) => c.textContent).join('|'), rows: [...document.querySelectorAll('#outTbl [data-ini]')].map((r) => r.dataset.ini), sso: document.querySelector('#outTbl [data-ini="sso-partners"]').textContent.replace(/\s+/g, ' '), decide: document.querySelectorAll('#outTbl [data-verdict]').length, keep: document.querySelectorAll('#outTbl [data-keep]').length, shown: !document.querySelector('#pOutcomes').hidden }));
354
- ok('O1 Did it work? sits under the key results with the columns Promised, Got and Verdict, and counts what is to judge and in window', o.shown && o.cols === 'Did it work? 1 to judge · 2 in window|Promised|Got|Verdict', o.cols);
354
+ ok('O1 Did it work? sits under the key results with the columns Promised, Check, Got and Verdict (redesign A: the check date is a column), and counts what is to judge and in window', o.shown && o.cols === 'Did it work? 1 to judge · 2 in window|Promised|Check|Got|Verdict', o.cols);
355
355
  ok('O1 it lists only the work to judge and in its window; a row says what its goal promised and what it got, and its date', o.rows.join() === 'refund-reason-codes,sso-partners,audit-log-export' && /Partner activation \+30 pts/.test(o.sso) && /\+8 pts/.test(o.sso) && /Judged Dec 31/.test(o.sso) && /shipped Oct 23/.test(o.sso), JSON.stringify(o));
356
356
  ok('O1 a promise with no reading has no drafted verdict: Decide only, no ✓', o.decide === 1 && o.keep === 0, JSON.stringify(o));
357
357
  await R.ctx.close();
@@ -359,7 +359,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
359
359
  ok('O1 a guardrail is read from the readings: since Audit log export shipped, partner activation held or got worse', /Partner activation: worse by 2 pts/.test(await txt('#outTbl [data-ini="audit-log-export"]', R2.page)), await txt('#outTbl [data-ini="audit-log-export"]', R2.page));
360
360
  await R2.ctx.close();
361
361
  /* churn met its promise (4% down to 1.8%): Claude drafts Keep, one ✓ records it with the drafted line, and Undo takes it back */
362
- const met = F.facts({ pinned: 'all', budgets: BUDGETS }); met.outcomes.find((x) => x.id === 'krm-churn').readings = [{ at: '2026-09-01', v: 4 }, { at: '2026-10-15', v: 1.8 }];
362
+ const met = F.facts({ pinned: 'all', budgets: BUDGETS, self: true }); met.outcomes.find((x) => x.id === 'krm-churn').readings = [{ at: '2026-09-01', v: 4 }, { at: '2026-10-15', v: 1.8 }];
363
363
  const R3 = await open({ model: IP.build(met) }), q3 = R3.page, sent = [];
364
364
  await q3.route(/\/api\/insights-records\/verdict(-undo)?$/, (r) => { sent.push([new URL(r.request().url()).pathname, JSON.parse(r.request().postData() || '{}')]); r.fulfill({ contentType: 'application/json', body: '{"ok":true}' }); });
365
365
  const dv = await q3.evaluate(() => ({ text: document.querySelector('#outTbl [data-ini="refund-reason-codes"]').textContent.replace(/\s+/g, ' '), keep: (document.querySelector('#outTbl [data-keep]') || {}).getAttribute && document.querySelector('#outTbl [data-keep]').getAttribute('aria-label') }));
@@ -381,7 +381,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
381
381
  const dc0 = await R4.page.evaluate(() => ({ rows: [...document.querySelectorAll('#outTbl [data-ini]')].map((r) => r.dataset.ini).join(), foot: (document.querySelector('#decFold') || {}).textContent }));
382
382
  await R4.page.click('#decFold'); await sleep(80);
383
383
  const dc = await R4.page.evaluate(() => (document.querySelector('#outTbl [data-ini="refund-reason-codes"]') || {}).textContent || '');
384
- ok('O2 a decided row folds under one line that counts the verdicts; opened, it says Kept and what was learned', !/refund-reason-codes/.test(dc0.rows) && /^Decided 1 · 1 kept, 0 changed, 0 stopped/.test(dc0.foot || '') && /Kept/.test(dc) && /Codes work\./.test(dc), JSON.stringify([dc0, dc]));
384
+ ok('O2 a decided row folds under one line that counts the verdicts, zero counts left out (redesign A); opened, it says Kept and what was learned', !/refund-reason-codes/.test(dc0.rows) && (dc0.foot || '').trim() === 'Decided 1 · 1 kept' && /Kept/.test(dc) && /Codes work\./.test(dc), JSON.stringify([dc0, dc]));
385
385
  await R4.ctx.close();
386
386
  }
387
387
 
@@ -390,9 +390,9 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
390
390
  const R = await open(), q = R.page;
391
391
  await q.click('#moreB'); await q.click('#moreMenu [data-more=\"library\"]');
392
392
  /* re-pointed (premium-uncovered-features, IsLibrary): the bar hands New report to Home, Found in your work is cards, the reports are Report · Next · Status */
393
- const lb = await q.evaluate(() => ({ side: !document.querySelector('#libSide').hidden, nr: document.querySelector('#newRep').getAttribute('href'), found: document.querySelector('#listRows .ins-fdh b').textContent, fcards: [...document.querySelectorAll('#listRows .ins-fcard')].map((c) => c.dataset.f + ':' + [...c.querySelectorAll('.ins-btn[data-fx]')].map((b) => b.textContent).join('/')).join(), heads: [...document.querySelectorAll('#repAsks .ins-rlh span')].map((s) => s.textContent).join('|'), rows: [...document.querySelectorAll('#listRows .ins-rl:not(.ins-rlh)')].map((r) => r.querySelector('.nm b').textContent + '|' + r.querySelector('.nx').textContent + '|' + r.querySelector('.st').textContent).join(' / '), setups: document.querySelectorAll('#listRows [data-setup]').length, asks: document.querySelectorAll('#listRows [data-ask]').length, cards: [...document.querySelectorAll('#listRows .ins-dcard')].map((c) => c.dataset.d + (c.querySelector('.ins-pinned') ? '*' : '')).join(), cap: document.querySelector('#repCap').textContent, old: !!document.querySelector('#listSeg, #sugB') }));
393
+ const lb = await q.evaluate(() => ({ side: !document.querySelector('#libSide').hidden, nr: document.querySelector('#newRep').getAttribute('href'), found: document.querySelector('#listRows .ins-fdh b').textContent, fcards: [...document.querySelectorAll('#listRows .ins-fcard')].map((c) => c.dataset.f + ':' + [...c.querySelectorAll('.ins-btn[data-fx]')].map((b) => b.textContent).join('/')).join(), heads: [...document.querySelectorAll('#repAsks .ins-rlh span')].map((s) => s.textContent).join('|'), rows: [...document.querySelectorAll('#repNeeds .ins-rl:not(.ins-rlh), #listRows .ins-rl:not(.ins-rlh)')].map((r) => r.querySelector('.nm b').textContent + '|' + r.querySelector('.nx').textContent + '|' + r.querySelector('.st').textContent).join(' / '), setups: document.querySelectorAll('#repNeeds [data-setup], #listRows [data-setup]').length, asks: document.querySelectorAll('#repNeeds [data-ask], #listRows [data-ask]').length, cards: [...document.querySelectorAll('#listRows .ins-dcard')].map((c) => c.dataset.d + (c.querySelector('.ins-pinned') ? '*' : '')).join(), cap: document.querySelector('#repCap').textContent, old: !!document.querySelector('#listSeg, #sugB') }));
394
394
  ok('L1 Reports and dashboards opens as a side panel from ⋯; New report in Home hands off there, and Found in your work holds the finding as a card', lb.side && /^\/premium\.html\?ws=w1&ask=insights&q=Make%20a%20new%20report/.test(lb.nr) && lb.found === 'Found in your work · 1' && lb.fcards === 'f-1:Set it up/Not a report' && lb.cap === '9' && !lb.old, JSON.stringify(lb));
395
- ok('L1 the reports table is Report, Next and Status; a draft says "Draft ready for" its date; a request says Proposal or Working', lb.heads === 'Report|Next|Status' && /Draft ready for Oct 14/.test(lb.rows) && /Partner update\|First draft [^|]+\|Runs every other monday/i.test(lb.rows) && /Churn for finance\|—\|Proposal/.test(lb.rows) && lb.setups === 1 && lb.asks === 2, JSON.stringify(lb.rows));
395
+ ok('L1 the reports table is Report, Next and Status (what needs you first, redesign A); a draft says "Draft ready for" its date; a request says Proposal or Working', lb.heads === 'Report|Next|Status' && /Draft ready for Oct 14/.test(lb.rows) && /Partner update\|First draft [^|]+\|Runs every other monday/i.test(lb.rows) && /Churn for finance\|—\|Proposal/.test(lb.rows) && lb.setups === 1 && lb.asks === 2, JSON.stringify(lb.rows));
396
396
  ok('L1 dashboards are cards; only the pinned one carries the pin', lb.cards === 'all*,pd-1,p:Growth,p:Reliability', lb.cards);
397
397
  const rs = []; await q.route('**/api/report-setups/*', (r) => { rs.push([new URL(r.request().url()).pathname.replace('/api/report-setups/', ''), JSON.parse(r.request().postData() || '{}')]); r.fulfill({ contentType: 'application/json', body: '{"ok":true,"drafted":{"slot":"2026-10-23","path":"planning/reports/x/2026-10-23.md"}}' }); });
398
398
  const lastRs = () => rs[rs.length - 1] || [null, {}];
@@ -412,7 +412,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
412
412
  await q.click('#czViewer [data-st="brief"]'); await sleep(80);
413
413
  ok('L2 its Brief says what it is for and for whom', /What moved for partners this fortnight/.test(await txt('#czViewer .ins-gb', q)) && /Leadership/.test(await txt('#czViewer .ins-gb', q)));
414
414
  await vwClose(q);
415
- await q.click('#listRows [data-ask="a-1"]'); const s3 = await until(() => vw(q));
415
+ await q.click('#repBody [data-ask="a-1"]'); const s3 = await until(() => vw(q));
416
416
  await q.click('#czViewer [data-act="chg"]'); await q.click('#czViewer [data-act="change"]'); const need = await txt('#czViewer [data-msg]', q);
417
417
  await q.fill('#czViewer [data-chg]', 'quarterly'); await q.click('#czViewer [data-act="change"]'); await until(() => rs.length > 2, 4000);
418
418
  ok('L2 a request from Home opens with what you asked and its proposal; Change it needs words, then posts the correction', s3 && !s3.wait && /A request from Home/.test(s3.text) && /A monthly churn report for finance/.test(s3.text) && /Set it up/.test(s3.text) && /Say what to change/.test(need) && lastRs()[0] === 'change' && lastRs()[1].id === 'a-1' && lastRs()[1].text === 'quarterly', JSON.stringify([s3, need, rs]));
@@ -420,13 +420,13 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
420
420
  ok('L2 every Library press went to a report-setups route', R.posts.every((x) => /^POST \/api\/report-setups\/(setup|pause|change)$/.test(x)), R.posts.join());
421
421
  await q.click('#listRows [data-d="p:Reliability"]'); await sleep(150);
422
422
  ok('L3 a dashboard card closes the panel and opens the key results on that view', (await txt('#viewName', q)) === 'Reliability' && (await q.evaluate(() => document.querySelector('#libSide').hidden)) && (await q.evaluate(() => [...document.querySelectorAll('#goalTbl [data-goal]')].map((r) => r.dataset.goal).join())) === 'krm-up,krm-churn');
423
- await q.click('#moreB'); await q.click('#moreMenu [data-more=\"library\"]'); await q.locator('#listRows [data-rpt]').first().click(); // a report file with no setup opens straight in the reader
423
+ await q.click('#moreB'); await q.click('#moreMenu [data-more=\"library\"]'); await q.locator('#repBody [data-rpt]').first().click(); // a report file with no setup opens straight in the reader
424
424
  ok('L4 a report opens in the page\'s reader, not the console (premium-report-read R1; re-pointed by premium-agreed-design: the reader is the report panel in the Viewer card)', await until(() => q.evaluate(() => !!document.querySelector('#czViewer:not([hidden]) .vz-rp0')), 5000) && /premium-intelligence\.html/.test(q.url()), q.url());
425
425
  ok('L4 a draft report has Issue it in the reader, so nothing sends you to the console to issue it', await until(() => q.evaluate(() => !!document.querySelector('#czViewer .vz-rp0 .rp-sum [data-r="issue"]')), 8000) && await q.evaluate(() => !document.querySelector('#czViewer a[href*="console.html"]')));
426
426
  await R.ctx.close();
427
427
  const lead = { 'planning/reports/biweekly/2026-10-14.md': 'Activation slipped to 28% this fortnight.' };
428
428
  const R2 = await open({ model: mk({ leads: lead }) });
429
- await R2.page.click('#moreB'); await R2.page.click('#moreMenu [data-more=\"library\"]'); const ln = await R2.page.evaluate(() => ({ dash: [...document.querySelectorAll('#listRows [data-d] .ins-mini small')].map((e) => e.textContent), rep: [...document.querySelectorAll('#listRows [data-rpt] .nm small')].map((e) => e.textContent), su: document.querySelector('#listRows [data-setup] .nm small').textContent }));
429
+ await R2.page.click('#moreB'); await R2.page.click('#moreMenu [data-more=\"library\"]'); const ln = await R2.page.evaluate(() => ({ dash: [...document.querySelectorAll('#listRows [data-d] .ins-mini small')].map((e) => e.textContent), rep: [...document.querySelectorAll('#repBody [data-rpt] .nm small')].map((e) => e.textContent), su: document.querySelector('#listRows [data-setup] .nm small').textContent }));
430
430
  ok('L5 each dashboard card draws its first goals, a report row quotes its file, a setup with no file says no draft is written yet', ln.dash[0] === 'Partner activation' && /“Activation slipped to 28% this fortnight\.”/.test(ln.rep[0]) && /No draft is written yet/.test(ln.su), JSON.stringify(ln));
431
431
  await R2.ctx.close();
432
432
  }
@@ -463,7 +463,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
463
463
  {
464
464
  const R = await open({ model: mk({ outcomes: [] }) });
465
465
  /* re-pointed (batch 16 item 6): the ✦ line never says "no insight"; with no goal it says what Claude drafts, with ✦ to start it */
466
- ok('I10 with no KR metric the ✦ line says no goal is set and offers to draft them, and the page draws no goal; Library still holds the report files', await R.page.evaluate(() => /has no goal yet/.test(document.querySelector('#brief').textContent) && !/no insight/i.test(document.querySelector('#brief').textContent) && !!document.querySelector('#insAct [data-okl="start"]') && /No key result is recorded for this workspace/.test(document.querySelector('#goalTbl').textContent) && document.querySelectorAll('#goalTbl [data-goal]').length === 0 && document.querySelectorAll('#listRows [data-rpt]').length === 2 && /draft the North Star/.test(document.querySelector("#insWhy").textContent) && !document.querySelector('#goalTbl .ins-th')));
466
+ ok('I10 with no KR metric the ✦ line says no goal is set and offers to draft them, and the page draws no goal; Library still holds the report files', await R.page.evaluate(() => /has no goal yet/.test(document.querySelector('#brief').textContent) && !/no insight/i.test(document.querySelector('#brief').textContent) && !!document.querySelector('#insAct [data-okl="start"]') && /No key result is recorded for this workspace/.test(document.querySelector('#goalTbl').textContent) && document.querySelectorAll('#goalTbl [data-goal]').length === 0 && document.querySelectorAll('#repBody [data-rpt]').length === 2 && /draft the North Star/.test(document.querySelector("#insWhy").textContent) && !document.querySelector('#goalTbl .ins-th')));
467
467
  await R.ctx.close();
468
468
  const R2 = await open({ model: mk({ reportsOk: false, reports: [], missing: ['reports'] }) });
469
469
  await R2.page.click('#moreB'); await R2.page.click('#moreMenu [data-more=\"library\"]');
@@ -500,7 +500,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
500
500
  const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }), pg = await ctx.newPage(), errs = [];
501
501
  pg.on('pageerror', (e) => errs.push(String(e)));
502
502
  await pg.goto(BASE + '/premium-insights.html');
503
- ok('E4 the real page says there is no goal, and has no script error', await until(() => pg.evaluate(() => /has no goal yet/.test(document.querySelector('#brief').textContent) && /No key result is recorded/.test(document.querySelector('#goalTbl').textContent)), 20000) && errs.length === 0, errs.join(' | '));
503
+ ok('E4 the real page lists the key results of this repository\'s OKR file with no reading, or says there is no goal, and has no script error (redesign A)', await until(() => pg.evaluate(() => (/^I read your OKR file/.test(document.querySelector('#brief').textContent) && !!document.querySelector('#goalTbl [data-kr]')) || (/has no goal yet/.test(document.querySelector('#brief').textContent) && /No key result is recorded/.test(document.querySelector('#goalTbl').textContent))), 20000) && errs.length === 0, errs.join(' | '));
504
504
  await ctx.close();
505
505
  }
506
506
  {
@@ -570,7 +570,7 @@ const BMR = { ok: true, state: 'ready', model: { tiles: [
570
570
  await q.route('**/api/insights-premium?ws=w2*', (r) => { asked.push(r.request().url()); r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: other }) }); });
571
571
  await q.reload(); // the route above answers the second product from the first read on
572
572
  const pf = await until(async () => { const x = await q.evaluate(() => ({ chip: (document.querySelector('#prodPick') || { getAttribute: () => '' }).getAttribute('aria-label'), all: (document.querySelector('#prodPick .ppk-all') || {}).textContent, groups: [...document.querySelectorAll('#goalTbl .ins-grp')].map((g) => g.querySelector('[role="cell"]').textContent), rows: document.querySelectorAll('#goalTbl [data-goal]').length, brief: document.querySelector('#brief').textContent, budgets: document.querySelectorAll('#goalTbl [data-budget]').length })); return x.groups.includes('Billing') ? x : null; }, 8000);
573
- ok('M1 two products ticked (the chip says All): Portfolio reads the other product once and groups every goal by product; budgets stay with their product', !!pf && pf.chip === 'Products: all 2' && pf.all === 'All' && pf.groups.slice(0, 2).join() === 'pmos,Billing' && pf.rows === 6 && /^Portfolio: 6 goals across 2 products/.test(pf.brief) && pf.budgets === 0 && asked.length === 1, JSON.stringify(pf));
573
+ ok('M1 two products ticked (the chip says All; the ✦ line sums All products, redesign A): Portfolio reads the other product once and groups every goal by product; budgets stay with their product', !!pf && pf.chip === 'Products: all 2' && pf.all === 'All' && pf.groups.slice(0, 2).join() === 'pmos,Billing' && pf.rows === 6 && /^Across your 2 products/.test(pf.brief) && pf.budgets === 0 && asked.length === 1, JSON.stringify(pf));
574
574
  await q.click('#prodPick'); const pm = await q.evaluate(() => [...document.querySelectorAll('#ppMenu [data-pp]')].map((b) => b.querySelector('b').textContent + ':' + ((b.querySelector('em') || {}).textContent || '') + ':' + b.getAttribute('aria-checked')).join());
575
575
  ok('M1 the list counts each product\'s goals and ticks both', pm === 'All products::true,pmos:5 goals:true,Billing:1 goal:true', pm);
576
576
  await q.keyboard.press('Escape');
@@ -62,7 +62,7 @@ const ready = (model) => ({ status: 200, body: { ok: true, state: 'ready', model
62
62
  const IP = require('./lib/insights-premium.js'), IF = require('./fixtures/insights-premium-facts.js');
63
63
  const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }), q = await ctx.newPage(), errs = [], saves = [];
64
64
  q.on('pageerror', (e) => errs.push(String((e && e.message) || e)));
65
- await q.route('**/api/insights-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: IP.build(IF.facts()), as_of: 1 }) }));
65
+ await q.route('**/api/insights-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: IP.build(Object.assign(IF.facts(), { self: true })), as_of: 1 }) }));
66
66
  await q.route('**/api/board-metrics-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify(ready(metrics()).body) }));
67
67
  await q.route('**/api/board-metric-targets/save', (r) => { saves.push(JSON.parse(r.request().postData() || '{}')); return r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, targets: {} }) }); });
68
68
  await q.route('**/api/briefing-premium/**', (r) => r.fulfill({ contentType: 'application/json', body: '{"ok":true,"turns":[],"working":false}' }));
@@ -1,4 +1,30 @@
1
1
  [
2
+ {
3
+ "v": "0.515.0",
4
+ "at": "2026-11-03T23:11:51.264Z",
5
+ "f": 4,
6
+ "prs": [
7
+ {
8
+ "n": 1206,
9
+ "t": "Intelligence reads the OKR file as its goal list (redesign A)",
10
+ "s": "Intelligence reads the OKR file as its goal list (redesign A)",
11
+ "k": "new"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "v": "0.514.0",
17
+ "at": "2026-11-03T16:19:12.685Z",
18
+ "f": 4,
19
+ "prs": [
20
+ {
21
+ "n": 1205,
22
+ "t": "Closed also reads the Done category and terminal stage names",
23
+ "s": "Closed also reads the Done category and terminal stage names",
24
+ "k": "fix"
25
+ }
26
+ ]
27
+ },
2
28
  {
3
29
  "v": "0.513.0",
4
30
  "at": "2026-11-03T15:46:14.696Z",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: Guide
3
3
  title: "Guide: start here"
4
- timestamp: 2026-11-02
4
+ timestamp: 2026-11-03
5
5
  ---
6
6
 
7
7
  # Guide: start here
@@ -80,6 +80,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
80
80
  - [Breakdown › Timeline](timeline.md): an initiative's items on a time axis, split by stage, with milestones, today, and the at-risk flag when an item starts before its predecessor ends.
81
81
  - [Initiatives › Timeline](initiatives-timeline.md): when each initiative runs, one row per initiative, and how to reorder rows by their grip.
82
82
  - [Workflows](workflows.md): Settings › Workflows, the stages each kind of initiative goes through, New workflow in the chat, and `pmos workflow`.
83
+ - [Intelligence reads your OKR file](intelligence-okr-goals.md): each key result of the OKR file is a row, with its reading or the source it needs; Did it work?, All products and Delivery trends.
83
84
  - [Expected outcomes](outcomes.md): every initiative says what should change once it ships and when to check it. Claude drafts each one from the PRD or the KR, you keep it with ✓, and `pmos outcomes`.
84
85
  - [The next prompt, suggested and sharpened](next-prompt.md): after a reply, up to two prompts to send next; while you type a rough prompt, a sharper version in your style (Polish, Prompt engineer, Your own).
85
86
  - [Rehearse a plan](rehearse.md): before you approve a date change or a cut, Claude shows what it breaks or helps in other initiatives, goals and program milestones, with the record each effect comes from, and `pmos rehearse`.
@@ -0,0 +1,39 @@
1
+ ---
2
+ type: Guide
3
+ title: "Intelligence reads your OKR file"
4
+ description: "How Intelligence lists the key results of a product's OKR file, which ones have a reading, Did it work?, All products and Delivery trends."
5
+ timestamp: 2026-11-03
6
+ ---
7
+
8
+ # Intelligence reads your OKR file
9
+
10
+ Intelligence (`/premium-intelligence.html`) uses the OKR file of a product as its goal list. It reads `pmos/planning/okrs/okrs-*.md` in the product repository, or `planning/okrs/` in PMOS itself. When a file says `Supersedes x.md`, Intelligence does not read `x.md`.
11
+
12
+ ## The goal list
13
+
14
+ - **The period.** The first period in the period chip is the period of the OKR file, for example H2 2026. It is the default, so a filter for one quarter does not hide a goal of the half.
15
+ - **Each key result is a row.** The row shows the objective, the key result, its line from the file (baseline → target) and its due date.
16
+ - **A reading.** When a metric record names the key result (its `kr` field, for example `O1.KR2`), that record reads the key result. The row then shows the reading and its line.
17
+ - **Needs a source.** When nothing reads a key result, the row says which source the file names, for example New Relic, ServiceNow or a host. The row shows no number. Click the row to see the key result and **Connect** its source in Settings.
18
+ - **Other goals.** Metric records that read no key result show under **Other goals**.
19
+ - **+ New goal.** The form is filled from the first key result that nothing reads: its name, baseline, target, direction, due date and objective. Check it, then save. The saved goal reads that key result. It is not a second goal.
20
+
21
+ ## The ✦ line
22
+
23
+ The ✦ line says what Intelligence read this week and the one call to make. When no key result has a reading yet, it says how many key results the file holds and which sources they wait on. It does not guess a number. After the findings from the readings, the strip also says when an outcome check is overdue and which initiatives are late.
24
+
25
+ ## Did it work?
26
+
27
+ Each shipped initiative shows what it promised (its `outcome_metric`), the date to check it (its `outcome_horizon`), what it got and the verdict. A verdict shows only when one is recorded. A Done status alone does not make a verdict. **Decided** counts the recorded verdicts and leaves out a count of zero. The table also says how many initiatives have no promise. [Expected outcomes](outcomes.md) explains the promise.
28
+
29
+ ## All products
30
+
31
+ When you tick two or more products, a table shows one row for each product: its North Star, how many key results have a reading, the late initiatives, the outcome checks that are overdue and the next step Claude would take. Click a row to open the Intelligence of that product. On a phone each row is a card.
32
+
33
+ ## Delivery trends
34
+
35
+ On a product repository, Delivery trends does not show **Merged a week** or **Corrections / 100 runs**. Those tiles measure PMOS agents, so they show only on the PMOS workspace. When the history is shorter than 30 days, **Idle 30+ days** shows — and says the date it is known from.
36
+
37
+ ## Reports and dashboards
38
+
39
+ The panel shows **Needs you** first: the drafts that wait for you and the asks that are ready. The ready-made reports are next, folded under **Drafting on their moment**. Your own reports and dashboards follow.
@@ -95,7 +95,10 @@ function build(f) {
95
95
  const ch = prev == null ? null : (() => { const d = live - prev; return { d, good: d < 0, txt: (d === 0 ? 'No change' : (d > 0 ? 'Up ' : 'Down ') + Math.abs(d)) + ' vs last week' }; })();
96
96
  return { key, label, value: String(live), text: 'of ' + open.length + ' open · ' + since, kind, ser, wk: W.map((w) => w.label), nt: ser.map(() => ''), dec: 0, suf: '', change: ch, since, def, lowBetter: true };
97
97
  };
98
- tiles.push(hTile('blocked', 'Blocked', blocked, blocked ? 'bad' : '', DEFS.blocked), hTile('idle', 'Idle for a month', idle, idle ? 'warn' : '', DEFS.idle));
98
+ // Intelligence redesign A: an initiative with no activity on record is counted idle, and a month of watching is what makes that true. Until
99
+ // the history is a month old, the idle tile says the day it becomes known (Intelligence shows that instead of the count).
100
+ const known = first + IDLE_DAYS * DAY > now ? Rec.dayOf(first + IDLE_DAYS * DAY) : null;
101
+ tiles.push(hTile('blocked', 'Blocked', blocked, blocked ? 'bad' : '', DEFS.blocked), Object.assign(hTile('idle', 'Idle for a month', idle, idle ? 'warn' : '', DEFS.idle), known && open.some((i) => !Number.isFinite(Date.parse(i.last_activity))) ? { knownFrom: known, historyFrom: Rec.dayOf(first) } : {}));
99
102
  counted = { blocked, idle, open: open.length };
100
103
  }
101
104
 
@@ -90,6 +90,8 @@ function goalFields(input, { today } = {}) {
90
90
  if (SOURCES[i.source]) f.source = i.source;
91
91
  const owner = tidy(i.owner, 60); if (owner) f.owner = owner;
92
92
  const guard = tidy(i.guardrail, 120); if (guard) f.guardrail = guard;
93
+ // Intelligence redesign A: a goal drafted from the OKR file reads that key result (lib/okr-goals.js readerOf), so it is never a second goal
94
+ const kr = tidy(i.kr, 40); if (kr && /^(?:O[\w-]*\.)?KR[\w.-]+$/i.test(kr)) f.kr = kr;
93
95
  return { ok: true, fields: f };
94
96
  }
95
97
 
@@ -119,7 +121,7 @@ function describe(text, goals) {
119
121
  function parseBudgets(md) {
120
122
  const out = [];
121
123
  for (const line of String(md || '').split('\n')) {
122
- const m = /^\|\s*`health:([a-z0-9-]+)`\s*\|\s*([^|]*)\|\s*(\d+(?:\.\d+)?)\s*%\s*\|/.exec(line);
124
+ const m = /^\|\s*`?health:([a-z0-9-]+)`?\s*\|\s*([^|]*)\|\s*(\d+(?:\.\d+)?)\s*%\s*\|/.exec(line);
123
125
  if (m) out.push({ id: m[1], covers: m[2].trim(), cap: Number(m[3]) / 100 });
124
126
  }
125
127
  return out;
@@ -23,6 +23,7 @@
23
23
  const IG = require('./insights-goals.js');
24
24
  const GS = require('./goal-status.js'); // batch 16 item 6: the one status of a goal, from its readings
25
25
  const MR = require('./metric-reads.js');
26
+ const OG = require('./okr-goals.js'); // Intelligence redesign A: the OKR file is the goal list
26
27
  const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
27
28
  const isoDay = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? Date.UTC(+m[1], +m[2] - 1, +m[3]) : null; };
28
29
  const shortDate = (v) => { const t = isoDay(v); if (t === null) return ''; const d = new Date(t); return d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
@@ -394,17 +395,55 @@ function okrOf(model, drafts) {
394
395
  const alert = a ? { rid: a.id, title: a.title, at: a.alert.day, txt: a.reading.txt, line: a.reading.line } : null;
395
396
  // the owner the page shows is the goal's owner string: a goal with one has an owner on the ✦ line too
396
397
  const named = goals.map((g) => (g.who || !g.owner ? g : Object.assign({}, g, { who: { name: g.owner, initials: GS.initials(g.owner), from: 'pm' } })));
397
- return { line: GS.lineOf({ goals: named, drafts, alert, moved: GS.movedOf(named) }), alert };
398
+ let line = GS.lineOf({ goals: named, drafts, alert, moved: GS.movedOf(named) });
399
+ // Intelligence redesign A: with nothing read and nothing drafted, the line was "I have not read a goal yet" although the OKR file was read.
400
+ // It leads with what was read instead, then what waits on a source (grouped by the source the file names).
401
+ if (model.okrFile && line.tone === 'drafts' && !drafts) line = okrLead(model.okrFile, model.goals || []);
402
+ return { line, alert, more: moreOf(model) };
403
+ }
404
+ const noun = (n, one, many) => n + ' ' + (n === 1 ? one : many);
405
+ /** The ✦ line from the OKR file (IgA1Open): what was read this week, then what waits on which source. Never a number without a reading. PURE. */
406
+ function okrLead(F, goals) {
407
+ const krs = F.objectives.flatMap((o) => o.krs), byId = new Map(goals.map((g) => [g.id, g]));
408
+ const fresh = krs.filter((k) => { const g = k.read && byId.get(k.goal); return g && g.reading && g.reading.ageDays !== null && g.reading.ageDays <= 7; });
409
+ const per = F.period ? ' for ' + F.period.label : '';
410
+ const head = fresh.length ? 'Read this week: ' + fresh.slice(0, 2).map((k) => k.token + ' ' + byId.get(k.goal).nowTxt).join(', ') + (fresh.length > 2 ? ' and ' + (fresh.length - 2) + ' more' : '') + '.'
411
+ : F.read ? 'I read your OKR file: ' + noun(F.total, 'key result', 'key results') + per + ', ' + F.read + ' with a reading.'
412
+ : 'I read your OKR file: ' + noun(F.total, 'key result', 'key results') + per + '. None has a reading yet.';
413
+ const wait = Object.entries(F.needs).sort((a, b) => b[1] - a[1]);
414
+ const nw = wait.reduce((a, [, n]) => a + n, 0);
415
+ const sub = nw ? noun(nw, 'key result waits', 'key results wait') + ' on a source: ' + wait.slice(0, 3).map(([k, n]) => n + ' on ' + k).join(', ') + (wait.length > 3 ? ', and ' + (wait.length - 3) + ' more' : '') + '. I will not guess a number.' : '';
416
+ return { head, sub, tone: 'calm', keepAll: 0 };
417
+ }
418
+ /** More ✦ findings after the first line (IgA1Open's pager): outcome checks that are due, and late initiatives. Zero is silence. PURE. */
419
+ function moreOf(model) {
420
+ const out = [];
421
+ const due = (model.outcomes || []).filter((r) => r.group === 'verdict' && r.promise && r.promise.overdue);
422
+ if (due.length) {
423
+ const r = due[0];
424
+ out.push({ text: (due.length === 1 ? 'An outcome check is due: ' : due.length + ' outcome checks are due. The oldest: ') + r.title + ', checked by ' + mdDate(r.promise.horizon) + '.',
425
+ why: r.promise.metric ? 'It promised: ' + clip(r.promise.metric, 140) : 'See Did it work? below.', outcome: r.id });
426
+ }
427
+ const late = model.lateRows || [];
428
+ if (late.length) out.push({ text: noun(late.length, 'initiative is late', 'initiatives are late') + ': ' + late.slice(0, 3).map((r) => r.title + (r.owner ? ' (' + r.owner + ')' : '')).join(', ') + (late.length > 3 ? ', and ' + (late.length - 3) + ' more' : '') + '.', why: 'Late is the Board’s own status for each.' });
429
+ return out;
398
430
  }
399
431
  /** Shipped work (Deliver, Monitor, Evaluate, or done) against what its anchor goal promised. PURE. */
400
- function outcomesOf(inits, goals, outcomes, verdicts, todayIso) {
432
+ /* Intelligence redesign A (IgA3Outcomes): "Decided" and "kept" come only from a recorded verdict (this page's verdict, or the D57 outcome check in
433
+ * outcome_checks.jsonl). A status word ("done") never makes a row kept: before, "Decided 2 · 2 kept" showed with no verdict recorded anywhere.
434
+ * promises: { [initiative id]: { metric, horizon, disposition } } from lib/outcomes.js scan; a check date that has passed makes the row due. */
435
+ const DISPO = { validated: 'Keep', refuted: 'Stop', inconclusive: 'Iterate' };
436
+ function outcomesOf(inits, goals, outcomes, verdicts, todayIso, promises) {
401
437
  const ORDER = ['verdict', 'window', 'decided'];
402
438
  const rows = (inits || []).map(initOf).filter((r) => r.idx >= 5 || r.status === 'done');
403
439
  return rows.map((r) => {
404
440
  const g = goals.find((x) => anchorHits(r.anchor, { id: x.id, name: (outcomes.find((o) => o.id === x.id) || {}).name })) || null;
405
- const vd = (verdicts && verdicts[r.id]) || null, o = g ? outcomes.find((x) => x.id === g.id) : null;
406
- const group = vd || r.status === 'done' || r.status === 'cancelled' ? 'decided' : r.idx === 7 ? 'verdict' : 'window';
407
- return Object.assign(r, { group, verdict: vd ? { verdict: vd.verdict, learned: vd.learned, at: vd.at ? String(vd.at).slice(0, 10) : null } : null,
441
+ const pr = (promises && promises[r.id]) || null;
442
+ const vd0 = (verdicts && verdicts[r.id]) || null, vd = vd0 || (pr && pr.disposition ? { verdict: DISPO[pr.disposition] || 'Iterate', learned: '', at: null, check: true } : null), o = g ? outcomes.find((x) => x.id === g.id) : null;
443
+ const dueNow = !!(pr && pr.horizon && pr.horizon <= todayIso);
444
+ const group = vd || r.status === 'cancelled' ? 'decided' : r.idx === 7 || r.status === 'done' || dueNow ? 'verdict' : 'window';
445
+ return Object.assign(r, { group, promise: pr ? { metric: pr.metric ? clip(pr.metric, 160) : '', horizon: pr.horizon || null, overdue: dueNow && !vd } : null,
446
+ verdict: vd ? { verdict: vd.verdict, learned: vd.learned || '', at: vd.at ? String(vd.at).slice(0, 10) : null, check: !!vd.check } : null,
408
447
  window: IG.windowOf({ shipAt: r.endAt, byAt: g ? g.byAt : null, verdict: vd, today: todayIso }),
409
448
  evidence: g && o ? IG.evidenceOf((o.readings || []).map((x) => ({ at: String(x.at), v: x.v })), r.endAt, g.unit) : [], goal: g ? { id: g.id, key: g.key, title: g.title, type: g.type, baseTxt: g.baseTxt, targetTxt: g.targetTxt, nowTxt: g.nowTxt, by: g.by, done: g.done, tone: g.tone, pace: g.pace } : null });
410
449
  }).sort((a, b) => ORDER.indexOf(a.group) - ORDER.indexOf(b.group));
@@ -412,6 +451,8 @@ function outcomesOf(inits, goals, outcomes, verdicts, todayIso) {
412
451
 
413
452
  /** The health budgets the board's initiatives anchor to (D59: an anchor health:<id>). A budget's floor or ceiling and its weekly readings are not
414
453
  * read on this page, so a budget holds only its id and what moves it. PURE. */
454
+ /** A PMOS kit chore in a product repository: its work is on PMOS itself (adopting the kit, its gates, its paths), not on the product. */
455
+ const isKitChore = (r) => !!r && /\b(pmos|kit|create-pmos)\b/i.test(String(r.title || '') + ' ' + String(r.id || '')) && (r.lane === 'chore' || /^health:/.test(String(r.anchor || '')));
415
456
  function budgetsOf(inits, registry) {
416
457
  const by = new Map();
417
458
  for (const b of registry || []) by.set(b.id, []);
@@ -476,11 +517,16 @@ function build(facts) {
476
517
  model.goals = goalsOf(outcomes, charts, f.initiatives || [], today, f.goalMeta || {}, f.watch || null);
477
518
  model.initiativesRead = Array.isArray(f.initiatives);
478
519
  const ins = f.ins || { charts: {}, verdicts: {}, targets: {} }, bid = board.id || '';
479
- model.outcomes = outcomesOf(f.initiatives || [], model.goals, outcomes, ins.verdicts[bid] || {}, model.today);
480
- model.budgets = budgetsOf(f.initiatives || [], f.budgets || []);
520
+ model.outcomes = outcomesOf(f.initiatives || [], model.goals, outcomes, ins.verdicts[bid] || {}, model.today, f.promises || null);
521
+ model.self = f.self === true;
522
+ // a product's budgets count its own work: the PMOS kit's chores in a product repository (kit adoption, the gates) are not its health work
523
+ model.budgets = budgetsOf(model.self ? f.initiatives || [] : (f.initiatives || []).filter((r) => !isKitChore(r)), f.budgets || []);
481
524
  model.budgetsRead = Array.isArray(f.budgets);
482
525
  budgetWatch(model.budgets, model.goals);
483
526
  model.metrics = GS.metricsOf(model.goals);
527
+ // Intelligence redesign A: the OKR file is the goal list; a KR metric record is a key result's reader (lib/okr-goals.js)
528
+ model.okrFile = OG.build({ okr: f.okr, goals: model.goals, initiatives: (f.initiatives || []).map(initOf), records: f.goalMeta || {}, today: model.today });
529
+ model.lateRows = Array.isArray(f.initiatives) ? f.initiatives.filter((r) => r && r.status === 'late').map(initOf) : [];
484
530
  model.okr = okrOf(model, (f.watch && f.watch.drafts) || 0);
485
531
  model.lateNow = Array.isArray(f.initiatives) ? f.initiatives.filter((r) => r && r.status === 'late').length : null;
486
532
  model.trends = Object.assign(IG.trendsOf(Array.isArray(f.initiatives) ? f.initiatives : null, model.today), { targets: ins.targets || {}, tiles: ins.tiles || {} });