create-pmos 0.515.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.515.0",
3
+ "version": "0.516.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,27 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.516.0",
37
+ "at": "2026-11-04T00:00:07.601Z",
38
+ "f": 4,
39
+ "prs": [
40
+ {
41
+ "n": 1209,
42
+ "t": "Runbook of what Claude does behind the scenes, in the guide",
43
+ "s": "Runbook of what Claude does behind the scenes, in the guide",
44
+ "k": "x",
45
+ "h": "See what Claude does behind the scenes",
46
+ "d": "Claude's background jobs were explained one page at a time, and nothing listed them all together. One guide page lists every job Claude runs by itself: when it runs, what it reads, where you see the result, how to stop it, and what Claude fixes without asking."
47
+ },
48
+ {
49
+ "n": 1208,
50
+ "t": "Reports that need you come first, one ✓ each (redesign R)",
51
+ "s": "Reports that need you come first, one ✓ each (redesign R)",
52
+ "k": "new"
53
+ }
54
+ ]
55
+ },
35
56
  {
36
57
  "v": "0.515.0",
37
58
  "at": "2026-11-03T23:11:51.264Z",
@@ -415,53 +436,6 @@
415
436
  "d": "After a chat changed something, the Viewer still showed the old version until you refreshed. Now it updates on its own."
416
437
  }
417
438
  ]
418
- },
419
- {
420
- "v": "0.496.0",
421
- "at": "2026-11-03T00:29:32.573Z",
422
- "f": 4,
423
- "prs": [
424
- {
425
- "n": 1172,
426
- "t": "Initiative chats find the initiative's workflow through pmos, never a lane",
427
- "s": "Initiative chats find the initiative's workflow through pmos, never a lane",
428
- "k": "fix",
429
- "h": "Initiative chats find the right workflow",
430
- "d": "An initiative's chat could miss the workflow it should follow. Now it always finds it, including the ones in your own library."
431
- },
432
- {
433
- "n": 1170,
434
- "t": "Keep a rejected ✦ suggestion rejected while its facts hold",
435
- "s": "Keep a rejected ✦ suggestion rejected while its facts hold",
436
- "k": "fix",
437
- "h": "A no stays a no",
438
- "d": "A ✦ suggestion you turned down could come back the next day. Now it stays rejected while nothing about it changes."
439
- },
440
- {
441
- "n": 1171,
442
- "t": "Tasks keep their mapped stage after a workflow switch on Home and Board",
443
- "s": "Tasks keep their mapped stage after a workflow switch on Home and Board",
444
- "k": "fix",
445
- "h": "Tasks keep their stage",
446
- "d": "Switching an initiative's workflow could move its tasks to the wrong stage. Now they keep the stage they were mapped to."
447
- },
448
- {
449
- "n": 1173,
450
- "t": "Calmer Board top bar",
451
- "s": "Calmer Board top bar",
452
- "k": "new",
453
- "h": "A calmer Board top bar",
454
- "d": "The Board's top bar was crowded. Now it is lighter, with fewer controls in your way."
455
- },
456
- {
457
- "n": 1165,
458
- "t": "Pages show something at once while they read",
459
- "s": "Pages show something at once while they read",
460
- "k": "new",
461
- "h": "Pages show something at once",
462
- "d": "A page stayed blank while it read its data. Now it shows its shape right away and fills in as data arrives."
463
- }
464
- ]
465
439
  }
466
440
  ]
467
441
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@49a5661d
2
- assembled: 2026-11-03
1
+ kernel-source: pmos@454e5b13
2
+ assembled: 2026-11-04
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-03T23:11:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
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-03T23:11:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
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-03T23:11:47Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-04T00:00:04Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  Newest first, ISO dates.
4
4
 
5
- ## 2026-11-03
5
+ ## 2026-11-04
6
6
 
7
7
  - Adopted OKF v0.2 (D73).
@@ -36,19 +36,22 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
36
36
  srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
37
37
  await T.waitForServer(BASE);
38
38
  browser = await chromium.launch({ executablePath: exe });
39
- const W = F.world();
39
+ /* One clock for the page and the stand-ins: today at 11:00, so "Rail review" (30 minutes on) and the meeting 40 minutes back are today
40
+ * at any hour the suite runs. Unpinned, after 23:30 the review fell on tomorrow and C2 failed. */
41
+ const NOW = new Date(); NOW.setHours(11, 0, 0, 0);
42
+ const W = F.world(NOW);
40
43
  const tap = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
41
44
  const text = (p) => p.evaluate(() => (document.querySelector('#czViewer') || {}).innerText || '');
42
45
  const weekReads = (S) => S.gets.filter((g) => g.startsWith('/api/calendar-views')).length;
43
46
  const ALL = [];
44
- const open = async (url, o) => { const X = await F.open(browser, BASE, url, { noLive: true, ...o }); ALL.push(X); return X; };
47
+ const open = async (url, o) => { const X = await F.open(browser, BASE, url, { noLive: true, now: NOW, ...o }); ALL.push(X); return X; };
45
48
 
46
49
  /* C1 the strip (on a Saturday or Sunday no day of the Mon–Fri strip is today, so none is pressed) */
47
50
  const A = await open('/premium-viewer.html?open=calendar');
48
51
  const p = A.page;
49
52
  // Viewer kit (XvACal): the calendar opens on the day, with today's summary first; the week and month are in ⋯
50
53
  ok('C1 the calendar opens in the Viewer card on the day', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzc-dy').length === 5 && document.querySelectorAll('#czViewer .vzc-de').length > 0), 15000));
51
- const weekday = ![0, 6].includes(new Date().getDay());
54
+ const weekday = ![0, 6].includes(NOW.getDay());
52
55
  await until(() => p.evaluate(() => [...document.querySelectorAll('#czViewer .vzc-dy')].every((d) => d.getBoundingClientRect().height >= 44)), 8000); // the Meetings stylesheet loads after the day is drawn
53
56
  const strip = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzc-dy')].map((d) => ({ dow: d.querySelector('span').textContent, on: d.getAttribute('aria-pressed'), h: Math.round(d.getBoundingClientRect().height) })));
54
57
  ok('C1 the strip has Monday to Friday and today is pressed on a weekday', strip.map((d) => d.dow).join() === 'MON,TUE,WED,THU,FRI' && strip.filter((d) => d.on === 'true').length === (weekday ? 1 : 0), JSON.stringify(strip));
@@ -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('#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') }));
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(' / '), needs: [...document.querySelectorAll('#repNeeds .ins-nrow .nm b')].map((b) => b.textContent).join('|'), setups: document.querySelectorAll('#repNeeds [data-setup], #listRows [data-setup]').length, asks: new Set([...document.querySelectorAll('#repNeeds [data-ask], #listRows [data-ask]')].map((e) => e.dataset.ask)).size, 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 (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));
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 Working; a ready proposal waits in Needs you (redesign R)', 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(\||$)/.test(lb.needs) && lb.setups === 1 && lb.asks === 2, JSON.stringify([lb.rows, lb.needs]));
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, {}];
@@ -1,4 +1,25 @@
1
1
  [
2
+ {
3
+ "v": "0.516.0",
4
+ "at": "2026-11-04T00:00:07.601Z",
5
+ "f": 4,
6
+ "prs": [
7
+ {
8
+ "n": 1209,
9
+ "t": "Runbook of what Claude does behind the scenes, in the guide",
10
+ "s": "Runbook of what Claude does behind the scenes, in the guide",
11
+ "k": "x",
12
+ "h": "See what Claude does behind the scenes",
13
+ "d": "Claude's background jobs were explained one page at a time, and nothing listed them all together. One guide page lists every job Claude runs by itself: when it runs, what it reads, where you see the result, how to stop it, and what Claude fixes without asking."
14
+ },
15
+ {
16
+ "n": 1208,
17
+ "t": "Reports that need you come first, one ✓ each (redesign R)",
18
+ "s": "Reports that need you come first, one ✓ each (redesign R)",
19
+ "k": "new"
20
+ }
21
+ ]
22
+ },
2
23
  {
3
24
  "v": "0.515.0",
4
25
  "at": "2026-11-03T23:11:51.264Z",
@@ -62,6 +62,7 @@ const REPORT = '---\ntype: Report\ntitle: "Password login and sign-up"\n---\n\n#
62
62
 
63
63
  function install(page, o = {}) {
64
64
  const W = world(o.now || new Date());
65
+ const shift = o.now ? o.now.getTime() - Date.now() : 0; // a pinned clock (o.now): the stand-ins answer in the page's time
65
66
  const S = { posts: [], gets: [], scene: o.scene || 'notes', W };
66
67
  const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'application/json', body: JSON.stringify(b) });
67
68
  const body = (r) => { try { return JSON.parse(r.request().postData() || '{}'); } catch { return {}; } };
@@ -118,8 +119,8 @@ function install(page, o = {}) {
118
119
  const u = new URL(r.request().url()), ev = u.searchParams.get('event'), note = u.searchParams.get('note');
119
120
  const live = ev === 'ev-live' || note === 'n2';
120
121
  /* a note with no calendar event has no event, brief or topics */
121
- if (!ev && note && note !== 'n1' && note !== 'n2') return json(r, { ok: true, now_ms: Date.now(), event: null, note: { id: note, state: (S.store[note] || {}).state || 'ended' }, brief: null, material: [], topics: [], runs: [], captured: {}, tail: [] });
122
- const base = { ok: true, now_ms: Date.now(), event: live ? { id: 'ev-live', title: 'Design crit', at: W.LIVE_NOTE.meeting.start, end: W.LIVE_NOTE.meeting.end, attendees: ['Ana', 'Wawan'] } : ev === 'ev-rail' ? { id: 'ev-rail', title: 'Rail review', at: iso(W.RAIL), attendees: ['Sam', 'Wawan'] } : { id: 'ev-sync', title: 'Viewer sync', at: W.NOTE.meeting.start, end: W.NOTE.meeting.end, attendees: ['Ana', 'Dita', 'Wawan'] },
122
+ if (!ev && note && note !== 'n1' && note !== 'n2') return json(r, { ok: true, now_ms: Date.now() + shift, event: null, note: { id: note, state: (S.store[note] || {}).state || 'ended' }, brief: null, material: [], topics: [], runs: [], captured: {}, tail: [] });
123
+ const base = { ok: true, now_ms: Date.now() + shift, event: live ? { id: 'ev-live', title: 'Design crit', at: W.LIVE_NOTE.meeting.start, end: W.LIVE_NOTE.meeting.end, attendees: ['Ana', 'Wawan'] } : ev === 'ev-rail' ? { id: 'ev-rail', title: 'Rail review', at: iso(W.RAIL), attendees: ['Sam', 'Wawan'] } : { id: 'ev-sync', title: 'Viewer sync', at: W.NOTE.meeting.start, end: W.NOTE.meeting.end, attendees: ['Ana', 'Dita', 'Wawan'] },
123
124
  note: live ? { id: 'n2', state: 'recording' } : ev === 'ev-rail' ? null : { id: 'n1', state: 'ended' },
124
125
  brief: { gist: ['Agree the rail before the Commit.'], purpose: 'Agree the rail before the Commit.', matters: [{ tag: 'Late', tone: 'bad', text: 'The SSO spike', meta: 'due 14 Oct' }, { tag: 'Changed', tone: '', text: 'The report panel design', meta: 'Oct 21' }], last: { id: 'n0', at: '2026-10-15T10:00:00Z', decisions: [{ text: 'a' }], actions: [{ text: 'b' }, { text: 'c' }] }, prep: 'Read the rail PRD.', initiative: { id: 'viewer', title: 'Viewer', ws: 'w1' } },
125
126
  material: [{ kind: 'pmos-deck', title: 'Release deck', ref: 'decks/release.md', from: 'you' }, { kind: 'pmos-doc', title: 'Rail PRD', ref: 'planning/prd/rail.md', from: 'you' }, { kind: 'link', title: 'Figma: report panel', url: 'https://x.example/f', topic: 'The report panel' }],
@@ -189,6 +190,7 @@ async function open(browser, base, url, o = {}) {
189
190
  const ctx = await browser.newContext({ viewport: { width: o.w || 1280, height: o.h || 900 }, acceptDownloads: true });
190
191
  const page = await ctx.newPage(), errors = [];
191
192
  page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
193
+ if (o.now) await page.clock.setSystemTime(o.now); // the page's Date starts at o.now and runs on; timers are real
192
194
  const S = await install(page, o);
193
195
  await page.addInitScript(RECORDER);
194
196
  if (o.init) await page.addInitScript(o.init);
@@ -0,0 +1,73 @@
1
+ ---
2
+ type: Guide
3
+ title: "What Claude does behind the scenes"
4
+ description: "The runbook of the workspace agent: each job Claude runs by itself, when it runs, what it reads, where its result shows, how to stop it, what it fixes by itself, the rules every job follows, and how to see what ran."
5
+ timestamp: 2026-11-03
6
+ ---
7
+
8
+ # What Claude does behind the scenes
9
+
10
+ Claude works in the background while the workspace runs. It reads your records and the tools you connected, it drafts, and it checks. It does not decide for you. A result waits for your ✓, most often under the blue ✦ of an initiative or as one ✦ in Home's chat. A background job never sends, posts, pushes or merges anything.
11
+
12
+ This page lists every job, so you know what runs and why. Each row links to the page with the full detail.
13
+
14
+ ## The rules every job follows
15
+
16
+ - **Propose-only.** A background job files proposals. Nothing lands on an initiative, a task or a metric until you press ✓. ✕ drops a proposal, and Undo brings back what was there.
17
+ - **Every claim has a source.** An AI job files through one shared guard. The guard checks that each quote is really at the file and line it cites. A claim with no source that checks out is dropped. A proposal with a key that would write, apply, send or run a command is dropped.
18
+ - **Few at a time.** A run shows at most 3 proposals. In a quiet week it shows nothing.
19
+ - **Tested first.** A new version of a job shows nothing until its replay passes: its own known cases, and the guard's own cases (a made-up quote, a missing source, a path outside the repository, a write key). Each of those must be dropped.
20
+ - **Your login, your connections.** A job runs as a background Claude session with your own login, model, profile and connectors. The workspace holds no password or token. `PMOS_HEADLESS=off` sends every job back to a terminal pane.
21
+ - **Your hours.** Scheduled jobs run inside your working hours and never inside your quiet hours.
22
+ - **A spending limit.** Claude's checks spend at most the monthly credit you set in Settings › Claude's checks ($5, $10, $20 or $50). At the limit Claude pauses. It never spends beyond the limit.
23
+ - **Every run is logged.** Each job writes its own ledger in the runtime folder on this computer. Only you see it, and it never goes into a repository.
24
+
25
+ ## The jobs
26
+
27
+ | Job | When it runs | What it reads | Where you see it | How to stop it |
28
+ |---|---|---|---|---|
29
+ | [Initiative Driver](initiative-driver.md) | An initiative is created, moved or sent back. The unblock sweep runs every 30 minutes in working hours. At most 3 runs at once. | The initiative record, its tasks and its Jira tickets | The Agent strip of the initiative, and Needs you | Board settings › Agent: Off. `PMOS_DRIVER=off` |
30
+ | [Found elsewhere](claude-proposes.md) | You add an initiative | Drive, Docs, Sheets, Slides, Jira, the calendar, meeting notes, Gmail, Slack and Confluence | The initiative's ✦ | `PMOS_PROPOSE=off` |
31
+ | [New dates, done and tidy names](claude-proposes.md) | Each morning, and when a date passes | The tasks of the initiative, and the evidence that work happened | The initiative's ✦ | `PMOS_PROPOSE=off` |
32
+ | [Grill](claude-proposes.md) | Before an initiative leaves a stage, when a forecast turns at risk, when scope or dates move a lot, and once a month on running work with late work. Also when you write "grill this" in the initiative's chat. | The initiative, your stored research, your meeting notes and the decisions that stand | "Claude grilled it" under the initiative's ✦ | Settings › Claude's checks: Off. `PMOS_GRILL=off` |
33
+ | [You decided this before](ask-the-past.md) | You add an initiative that repeats an earlier decision | `DECISIONS.md`, the decision records, the PM journal, `planning/out-of-scope/` and the initiative records | The initiative's ✦ | `pmos ai off past-repeat` |
34
+ | [Expected outcomes](outcomes.md) | Once a day. At most 3 drafts a day for each product. | The PRD's success criteria, or the anchored key result | "Expected outcome" under the initiative's ✦. Today when a check is due. | `pmos ai off outcomes`. `PMOS_OUTCOMES=off` |
35
+ | [Metric drafts](metric-drafts.md) | A product with no North Star, the first 14 days of a quarter, a program with no goal, and an initiative that gets a PRD | The last 8 weeks of real readings | The page of that product, program or initiative | `PMOS_METRIC_DRAFT=off` |
36
+ | [Horizon scan](horizon-scan.md) | Daily by default, at most once a day. Scan now: at most 3 a day. | The public web for your kept watch list, and your repository | Report documents and proposals you approve | Cadence: Off. `PMOS_SCAN=off` |
37
+ | [What you might have missed](what-you-missed.md) | Each Monday, for the week before. It uses no model. | Records only: tasks, dates, initiatives and Jira status | One ✦ in Home's chat, at most 3 items | `pmos ai off drift`. `PMOS_DRIFT=off` |
38
+ | [Your impact](your-impact.md) | Claude reads once a day. Home shows the week after it ends. | Slack, mail, Jira, your meeting notes, your documents and your key results | Adler's sprout on Home, and your impact log | Settings › General › Your impact. `PMOS_IMPACT=off` |
39
+ | [Morning brief and day check](proactive-engine.md) | The morning brief at 06:45, once you schedule it. The day check every 60 minutes by default (30, 60 or off). | Calendar, Wispr Flow, Slack, Jira and Confluence are on by default. Mail, GitHub, Drive, Figma and websites are off by default. Mail: subjects and senders only. | Today | Each source is a setting. `pmos proactive settings` |
40
+ | [Day tasks](proactive-engine.md) | You press Prepare my day. At most 5 sessions at once. | The sources of the brief, read-only | Today: a draft waits for Approve or Revise | Do not press Prepare my day |
41
+ | [Next prompt and Sharper](next-prompt.md) | After Claude replies, and when you stop typing a rough prompt | The reply and your message | Cards above the chat box. Nothing is sent. | Settings › Claude. `pmos ai off next-prompt`, `pmos ai off sharpen-prompt` |
42
+ | [Rehearse a plan](rehearse.md) | You ask Claude to move a date or cut a task or an initiative | Task links, initiative records, goals and program charters | Your session, before Claude makes the change | `pmos ai off rehearse` |
43
+ | [Data refresh](data-freshness.md) | In working hours: the calendar every 30 minutes, and Jira, My work and your tools every hour. Tracker items on Friday 15:30. | Each tool with your own connection, read-only | Each page shows how old its data is | Settings › Data refresh |
44
+
45
+ Ask about the past runs only when you ask a question in an initiative's chat. It answers from the records, with the file and line of each claim, and says **No record found** when no record answers.
46
+
47
+ ## What Claude fixes by itself
48
+
49
+ Some problems are safe to fix with no question. Claude fixes these by itself and tells you only when it cannot.
50
+
51
+ - **A product folder on the wrong branch.** A document repository whose folder is not on its default branch looks empty. The workspace puts it back: it saves the records, checks that the merge has no conflict, merges, and pushes with no force. It never deletes or rewrites a branch. It never touches a code repository. When it cannot do this safely, Today shows one "Needs repair" card in plain words, with **Ask Claude to sort it out**. It tries again after 30 minutes, or after 10 minutes when it only waited for the network. `PMOS_AUTO_REPAIR=0` stops it.
52
+ - **Folders of finished work.** About every 6 hours the workspace removes a linked worktree only when its branch is merged, it holds no unsaved file, nothing used it for 14 days, and no session uses it. The branch is kept. `PMOS_AUTO_CLEAN=0` stops it.
53
+ - **A report source that does not answer.** A scheduled report waits up to 4 hours for a source that failed its check. Today shows "Check <source> for <report>" with **Check again**. Then the report is drafted without that source, and it says so. It never guesses a number.
54
+ - **A failed run.** A metric draft that failed tries again the next day. A Driver run that failed or timed out writes its reason in the ledger.
55
+ - **A source that was not reached.** It is shown as not reached, never as read. Today says which sources were not read.
56
+
57
+ Sync by itself is on by default. `PMOS_AUTO_SYNC=0` stops the sync, the repair and the clean-up.
58
+
59
+ ## What a background job never does
60
+
61
+ - It sends, posts, replies, pushes and merges nothing. A message to a person is a draft that you send. A Jira change is a drafted comment in your outbox.
62
+ - It does not create an initiative. The Horizon scan proposes one, and you approve it.
63
+ - It does not record a verdict on an outcome. The verdict is always yours.
64
+
65
+ When you ask an agent session to do a piece of work, it can finish it, including acts that were yours alone. Each line it writes names the agent, so you see who did it. See [PM-only acts](pm-only-acts.md).
66
+
67
+ ## See what ran, and turn it off
68
+
69
+ - `pmos ai status`: whether each AI job is on, whether its replay passed, and what its last run filed.
70
+ - `pmos ai log [<job>]`: the last audit lines. Each dropped proposal shows why.
71
+ - `pmos ai off [<job>]` and `pmos ai on [<job>]`: turn every AI job, or one job, off or on. `PMOS_AI=off` in the environment wins over the setting.
72
+
73
+ The AI jobs are `past-repeat`, `decision-memory`, `outcomes`, `drift`, `rehearse`, `next-prompt` and `sharpen-prompt`. A background run cannot change the switch.
@@ -41,6 +41,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
41
41
 
42
42
  ## Where to read next
43
43
 
44
+ - [What Claude does behind the scenes](agent-capabilities.md): every job Claude runs by itself, when it runs, what it reads, where you see it, how to stop it, and what it fixes by itself.
44
45
  - [First run](welcome.md): test your Claude login, describe the project, choose its folder, search your tools read-only, and set up PMOS from Home.
45
46
  - [The Briefing](briefing.md): the console's first page: Today's meetings, Needs you (Done, Not yet, Drop) and the Portfolio pulse.
46
47
  - [Start the workspace without a terminal](launcher.md): the PMOS app, the login item and `pmos launcher`.
@@ -81,6 +82,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
81
82
  - [Initiatives › Timeline](initiatives-timeline.md): when each initiative runs, one row per initiative, and how to reorder rows by their grip.
82
83
  - [Workflows](workflows.md): Settings › Workflows, the stages each kind of initiative goes through, New workflow in the chat, and `pmos workflow`.
83
84
  - [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.
85
+ - [Reports and dashboards: what needs you](reports-needs-you.md): the reports that wait for you come first, each with one ✓ (Issue, Confirm the definition, Move to a product, Resume); the first dashboard is drafted from the OKR file.
84
86
  - [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`.
85
87
  - [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).
86
88
  - [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,34 @@
1
+ ---
2
+ type: Guide
3
+ title: "Reports and dashboards: what needs you"
4
+ description: "How the Reports and dashboards panel in Intelligence lists the reports that wait for you first, each with one ✓, drafts the first dashboard from the OKR file, and how + makes a report."
5
+ timestamp: 2026-11-03
6
+ ---
7
+
8
+ # Reports and dashboards: what needs you
9
+
10
+ Open Intelligence (`/premium-intelligence.html`), then **⋯ › Reports and dashboards**. The panel shows these parts, in this order:
11
+
12
+ 1. **Needs you.** Each report that waits for you. A row shows the report name, the draft date, the readers, and the first line of the headline. Press the name to open the report or its setup. Each row has one ✓:
13
+ - **Issue**: the draft is ready. The ✓ issues the draft as you see it. Send it from your own mail.
14
+ - **Confirm the definition**: an agent proposed the definition of this report (for example the daily report), and nobody confirmed it yet. The ✓ changes `status: draft` to `status: stable` in the definition. It changes nothing else.
15
+ - **Move to <product>**: the report is about a different product than the one it is in. For example, the Scarlet assessment report was set up in the IDPF workspace. The ✓ moves the definition, the drafts and the setup to that product. A report that has an issued issue is not moved, because an issued report stays where it was issued.
16
+ - **Resume**: the report is paused. The ✓ starts its schedule again.
17
+ 2. **Drafting on their moment.** The ready-made reviews (quarter, milestone, stage, did it work). They draft when their moment comes. Open the fold to see each switch.
18
+ 3. **Weekly updates.** One line tells how many are on and off. Open the fold to see each switch.
19
+ 4. **Yours.** The other reports that you set up.
20
+ 5. **Dashboards.** When no dashboard is saved, Claude drafts the first one from the OKR file: one tile for each key result. A tile that has a reading shows the value, its reader and the date. A tile with no reading shows the source it needs, and its ✓ opens Settings › Connectors. Press ✓ on the card to keep the dashboard. It keeps the key results that have a reading.
21
+
22
+ A report from the daily, weekly or monthly definition has the title of its definition, for example "Daily report", not its id. Its preview is the headline. The line that says the definition is a draft is not the preview.
23
+
24
+ ## + in the panel
25
+
26
+ **+** opens the report that Claude drafts from the view on the screen. It does not open an empty chat. With all products on the screen, **+** opens Home, because a report is for one product.
27
+
28
+ ## Edit chart
29
+
30
+ **Edit chart** on a key result that has no reading shows the connector card for its source. When no dashboard is saved, **Save chart** keeps the drafted dashboard first.
31
+
32
+ ## The product page
33
+
34
+ The ✦ line on a product page is the same line as on Intelligence for that product. "Your weekly update is ready to send" shows only in a week with news. In a quiet week the Weekly update row says "Nothing new this week · skipped".
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+ /* report-acts.js — the one ✓ of a report that waits for the PM, in Reports and dashboards (Reports and dashboards redesign R, frame RgR1Panel;
3
+ * brief /mnt/project-files/intelligence-grill/brief-E-R-build.md step 9).
4
+ *
5
+ * defTitleOf(def) → the definition's title as a report name: "Daily report definition" → "Daily report". PURE
6
+ * reportAboutOf(text, boards, here) → { ws, name } of the one OTHER product a report names, or null. PURE
7
+ * reportActOf({ id, setup, rows, def, about, sha }) → { act, label, … } | null the report's one press. PURE
8
+ * reportActsOf(facts) → { [report id]: act } for every report of the model, and { names, about }. PURE
9
+ * leadOf(text) the opening sentence, past a line that only says the definition is not confirmed yet. PURE
10
+ *
11
+ * WHY. The grill (3 Nov, on 0.514.0) found the five drafts that wait for the PM at the bottom of the panel, under "Yours", named by their id
12
+ * ("daily"), previewed by the warning "this definition is a draft", and with no press that settles them. Each waiting report now has ONE ✓,
13
+ * the act that unblocks it, in this order:
14
+ * move the report is about another product than the one it sits in (Scarlet assessment in the IDPF workspace) → "Move to Scarlet"
15
+ * confirm its draft follows a definition an agent proposed and nobody has confirmed (status: draft, no setup) → "Confirm the definition"
16
+ * issue a draft waits and its definition is confirmed → "Issue" (POST /api/report/issue with the sha of the file as read)
17
+ * resume a set-up report is paused → "Resume"
18
+ * A report with none of these is not waiting for the PM and gets no ✓ (zero is silence).
19
+ *
20
+ * WHAT A NAME MATCHES. A product is named when its board name, as a whole word of four letters or more, is in the report's name or purpose and
21
+ * the product it sits in is not named too. Two other products named → no move: the report does not say which one it is about.
22
+ */
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const RS = () => require('./report-setups.js'); // read late: report-setups is wide, and this file only writes through its one writer
26
+ const refuse = (status, error) => ({ ok: false, status, error });
27
+ const clean = (s) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
28
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+
30
+ /** "Daily report definition" → "Daily report"; "Scarlet assessment report definition" → "Scarlet assessment report". '' when there is none. PURE. */
31
+ function defTitleOf(def) {
32
+ const t = clean(def && def.title).replace(/^["']|["']$/g, '');
33
+ if (!t || (def && t === def.id)) return '';
34
+ return t.replace(/\s+(definition|template)$/i, '').replace(/\s+template$/i, '').trim();
35
+ }
36
+
37
+ /** A product's name in `text` as a whole word (case-insensitive). Names shorter than four letters never match: "PI" is not a product name in prose. */
38
+ function mentionsProduct(text, name) {
39
+ const n = clean(name);
40
+ if (n.length < 4) return false;
41
+ return new RegExp('(^|[^A-Za-z0-9])' + escRe(n) + '(?![A-Za-z0-9])', 'i').test(String(text || ''));
42
+ }
43
+
44
+ /** The one other product a report is about. boards: [{ ws, name }]; here: the ws it sits in. → { ws, name } | null. PURE. */
45
+ function reportAboutOf(text, boards, here) {
46
+ const B = (boards || []).filter((b) => b && b.ws && b.name);
47
+ const mine = B.find((b) => b.ws === here);
48
+ if (mine && mentionsProduct(text, mine.name)) return null;
49
+ const hit = B.filter((b) => b.ws !== here && mentionsProduct(text, b.name));
50
+ return hit.length === 1 ? { ws: hit[0].ws, name: clean(hit[0].name) } : null;
51
+ }
52
+
53
+ /** A report's one press. rows: its files, newest first ({ slot, state, path, archived }); setup: its setup or null; def: its definition or null.
54
+ * → { act: 'move'|'confirm'|'issue'|'resume', label, … } | null. PURE. */
55
+ function reportActOf({ id, setup, rows, def, about, sha }) {
56
+ const live = (rows || []).filter((r) => r && !r.archived);
57
+ const draft = live.find((r) => r.state !== 'issued') || null;
58
+ const issued = live.some((r) => r.state === 'issued');
59
+ if (about && setup && !issued) return { act: 'move', label: 'Move to ' + about.name, to: about.ws, toName: about.name, id };
60
+ if (setup && setup.paused) return { act: 'resume', label: 'Resume', id };
61
+ if (!draft) return null;
62
+ // an agent-written definition (the daily, weekly and monthly skills write status: draft and only the PM confirms it). A report the PM set up
63
+ // wrote its own definition with that press, so its draft is issued
64
+ if (!setup && def && def.status === 'draft') return { act: 'confirm', label: 'Confirm the definition', id, slot: draft.slot };
65
+ return { act: 'issue', label: 'Issue', id, slot: draft.slot, sha: sha || null, path: draft.path || '' };
66
+ }
67
+
68
+ /** Every report's press and display name. f: { rows, setups, defs, boards, here, shas }. → { acts, names, about }. PURE. */
69
+ function reportActsOf(f) {
70
+ const rows = (f.rows || []).filter(Boolean), setups = (f.setups || []).filter(Boolean), defs = new Map((f.defs || []).filter(Boolean).map((d) => [d.id, d]));
71
+ const ids = [...new Set(setups.map((s) => s.id).concat(rows.map((r) => r.cadence)))];
72
+ const acts = {}, nameOf = {}, about = {};
73
+ for (const id of ids) {
74
+ const setup = setups.find((s) => s.id === id) || null, def = defs.get(id) || null, mine = rows.filter((r) => r.cadence === id);
75
+ const t = defTitleOf(def);
76
+ if (!setup && t) nameOf[id] = t;
77
+ const a = setup ? reportAboutOf([setup.name, setup.purpose].join(' · '), f.boards, f.here) : null;
78
+ if (a) about[id] = a;
79
+ const draft = mine.find((r) => !r.archived && r.state !== 'issued');
80
+ const act = reportActOf({ id, setup, rows: mine, def, about: a, sha: draft && f.shas ? f.shas[draft.path] : null });
81
+ if (act) acts[id] = act;
82
+ }
83
+ return { acts, names: nameOf, about };
84
+ }
85
+
86
+ /** A line that only says the definition is not confirmed: the daily skill puts it first in the Headline (report-daily.skill step 6). */
87
+ const WARN = /\bdefinition\b[^.]*\b(draft|unconfirmed|not (yet )?confirmed|proposed|waits? for your)\b|\b(draft|unconfirmed|proposed)\b[^.]*\bdefinition\b/i;
88
+ /** The opening sentence of a report, past the definition warning: its headline is the preview. `lead` is the caller's leadOf (insights-premium). PURE. */
89
+ function leadPastWarning(text, lead) {
90
+ const first = lead(text);
91
+ if (!first || !WARN.test(first)) return first;
92
+ const t = String(text || ''), at = t.indexOf(first.replace(/…$/, '').slice(0, 40));
93
+ // the same paragraph's next sentence, else the next paragraph
94
+ if (at >= 0) {
95
+ const rest = t.slice(at).replace(/^[^]*?[.!?](\s+|$)/, '');
96
+ const next = lead('\n\n' + rest);
97
+ if (next && !WARN.test(next)) return next;
98
+ }
99
+ return '';
100
+ }
101
+
102
+ // ── the writes of the ✓ (their routes: lib/routes-report-setups.js confirm and move) ─────────────────────────────────────────────
103
+ /** Confirm a definition (Reports and dashboards redesign R): the PM's ✓ turns `status: draft` into `status: stable` in its frontmatter. Only
104
+ * that line changes. A definition with no frontmatter status, or one confirmed already, is left as it is. → { ok, path, sha, unchanged? } */
105
+ function confirmDefinition(root, id, opts = {}) {
106
+ if (!RS().ID_RE.test(String(id || ''))) return refuse(400, 'the id is lower-case letters, digits and "-"');
107
+ const file = RS().definitionFile(root, id), rel = require('./report-draft.js').definitionPath(root, id);
108
+ let cur;
109
+ try { cur = fs.readFileSync(file, 'utf8'); } catch { return refuse(404, `there is no definition ${rel}`); }
110
+ const m = /^(---\r?\n)([\s\S]*?)(\r?\n---)/.exec(cur);
111
+ if (!m) return refuse(409, `${rel} has no frontmatter, so there is no status to confirm`);
112
+ if (!/^status:\s*["']?draft["']?\s*$/m.test(m[2])) return { ok: true, path: rel, sha: RS().shaOf(cur), unchanged: true };
113
+ const next = m[1] + m[2].replace(/^status:\s*["']?draft["']?\s*$/m, 'status: stable') + m[3] + cur.slice(m[0].length);
114
+ const tmp = file + '.tmp-' + process.pid;
115
+ try { fs.writeFileSync(tmp, next); fs.renameSync(tmp, file); } catch (e) { try { fs.unlinkSync(tmp); } catch { /* none */ } return refuse(500, 'could not write the definition; nothing changed: ' + String((e && e.message) || e).slice(0, 160)); }
116
+ void opts;
117
+ return { ok: true, path: rel, sha: RS().shaOf(next) };
118
+ }
119
+
120
+ /** Copy a folder's files, recursively, refusing to overwrite. → the repository-relative paths written under `to`. */
121
+ function copyReportTree(from, to) {
122
+ const out = [];
123
+ fs.mkdirSync(to, { recursive: true });
124
+ for (const e of fs.readdirSync(from, { withFileTypes: true })) {
125
+ const a = path.join(from, e.name), b = path.join(to, e.name);
126
+ if (e.isDirectory()) out.push(...copyReportTree(a, b));
127
+ else if (e.isFile()) { fs.copyFileSync(a, b, fs.constants.COPYFILE_EXCL); out.push(b); }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * Move a set-up report to the product it is about (Reports and dashboards redesign R, "✦ Move to Scarlet"): its definition, its drafts and its
134
+ * setup line go to `toRoot`, and it is removed here. A report with an ISSUED issue is refused (409): an issued report is never moved or deleted,
135
+ * it stays where it was issued. The id must be free in the target. Everything is copied before anything is removed here, so a failed copy
136
+ * leaves this workspace as it was (and removes what it copied). → { ok, id, moved: [rel] } | { ok: false, status, error }
137
+ */
138
+ function moveReportTo(root, id, toRoot, opts = {}) {
139
+ const abs = path.resolve(String(root)), to = path.resolve(String(toRoot || ''));
140
+ if (!toRoot || to === abs) return refuse(400, 'say which product to move it to');
141
+ const file = RS().ledgerFile(abs), st = RS().fold(RS().readLines(file)), cur = st.setups.get(id);
142
+ if (!cur) return refuse(404, `no recurring report ${String(id).slice(0, 40)}`);
143
+ const docs = (require('./report-needs.js').reportsOf(abs).reports || []).filter((r) => r.cadence === id);
144
+ if (docs.some((r) => r.issued)) return refuse(409, `${cur.name} has issued reports. An issued report stays where it was issued, so it was not moved`);
145
+ const tst = RS().fold(RS().readLines(RS().ledgerFile(to)));
146
+ const srcDef = RS().definitionFile(abs, id), dstDef = RS().definitionFile(to, id);
147
+ const srcDir = path.join(RS().stateDir(abs), 'reports', id), dstDir = path.join(RS().stateDir(to), 'reports', id);
148
+ if (tst.setups.has(id) || fs.existsSync(dstDef) || fs.existsSync(dstDir)) return refuse(409, `${id} is taken in that product already. Nothing was moved`);
149
+ const now = opts.now == null ? new Date() : new Date(opts.now);
150
+ const written = [];
151
+ try {
152
+ if (fs.existsSync(srcDef)) {
153
+ const t = fs.readFileSync(srcDef, 'utf8').replace(/^(product:\s*).*$/m, '$1' + JSON.stringify(path.basename(to)));
154
+ fs.mkdirSync(path.dirname(dstDef), { recursive: true }); fs.writeFileSync(dstDef, t, { flag: 'wx' }); written.push(dstDef);
155
+ }
156
+ if (fs.existsSync(srcDir)) written.push(...copyReportTree(srcDir, dstDir));
157
+ } catch (e) {
158
+ for (const w of written.reverse()) { try { fs.unlinkSync(w); } catch { /* gone */ } }
159
+ try { fs.rmSync(dstDir, { recursive: true, force: true }); } catch { /* gone */ }
160
+ return refuse(500, 'the copy failed, so nothing was moved: ' + String((e && e.message) || e).slice(0, 160));
161
+ }
162
+ // the setup line in the target, as it is here, with its new home; then the schedule moves and it is removed here
163
+ const line = { ev: 'setup' };
164
+ for (const k of ['id', 'name', 'purpose', 'audience', 'source', 'output', 'schedule', 'sources', 'sections', 'learned_from', 'fixes', 'last_run', 'def_sha', 'from', 'storyline', 'ready']) if (cur[k] !== undefined) line[k] = cur[k];
165
+ line.product = path.basename(to); line.ws = opts.toWsId || null; line.moved_from = path.basename(abs);
166
+ RS().append(RS().ledgerFile(to), line, { now, by: opts.by });
167
+ if (cur.status === 'paused') RS().append(RS().ledgerFile(to), { ev: 'pause', id }, { now, by: opts.by });
168
+ if (cur.source === 'work') {
169
+ RS().unregisterSchedule(opts.scheduleStore, cur, opts.wsId);
170
+ if (cur.status !== 'paused') RS().registerSchedule(opts.scheduleStore, Object.assign({}, cur, { product: line.product }), { wsId: opts.toWsId, lastRun: cur.last_run, now });
171
+ }
172
+ RS().append(file, { ev: 'remove', id }, { now, by: opts.by });
173
+ try { if (fs.existsSync(srcDef)) fs.unlinkSync(srcDef); } catch { /* the copy is in place; a stale definition here is harmless */ }
174
+ try { fs.rmSync(srcDir, { recursive: true, force: true }); } catch { /* the drafts stay here too */ }
175
+ return { ok: true, id, moved: written.map((w) => path.relative(to, w)) };
176
+ }
177
+
178
+ /** The product page's ✦ line is Intelligence's (RgR4Make, "one brain"). The grill found the product page saying "2 things are behind: KR-C4,
179
+ * KR-A1 · weekly update ready" while Intelligence said "I have not read a goal yet". Both now read the same model's line (lib/insights-premium.js
180
+ * okrOf → model.okr.line); "Your weekly update is ready to send" follows it only in a week with news. Without that line, the dashboard's own
181
+ * line stays. PURE. */
182
+ function productLineOfIntel(intel, { deskReady, news, fallback }) {
183
+ const L = intel && intel.okr && intel.okr.line;
184
+ if (!L || !L.head) return fallback;
185
+ const rest = [L.sub || '', deskReady && news ? 'Your weekly update is ready to send.' : ''].filter(Boolean).join(' ');
186
+ return { lead: L.head, rest, tone: L.tone === 'crit' || L.tone === 'behind' ? 'warn' : 'calm' };
187
+ }
188
+
189
+ module.exports = { defTitleOf, reportAboutOf, reportActOf, reportActsOf, leadPastWarning, WARN, confirmDefinition, moveReportTo, productLineOfIntel };
@@ -34,14 +34,19 @@ async function via(mod, pathAndQuery, req, ctx) {
34
34
  }
35
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. */
36
36
  const LEADS_MAX = 60;
37
- function leadsOf(ctx, ws, list) {
37
+ function leadsOf(ctx, ws, list, shas) {
38
38
  const out = {};
39
39
  let root = null;
40
40
  try { root = require('path').resolve(ctx.getWs(ws).root); } catch { return out; }
41
41
  for (const r of list.slice().sort((a, b) => String(b.slot).localeCompare(String(a.slot))).slice(0, LEADS_MAX)) {
42
42
  const rel = String(r.path || '');
43
43
  if (!rel || rel.includes('..') || require('path').isAbsolute(rel)) continue;
44
- 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 */ }
45
50
  }
46
51
  return out;
47
52
  }
@@ -96,7 +101,11 @@ module.exports = {
96
101
  const su = await via(setups, '/api/report-setups' + q, req, ctx).catch(() => ({ status: 500, body: null }));
97
102
  const setupsOk = su.status === 200 && !!su.body && Array.isArray(su.body.setups);
98
103
  if (!setupsOk) missing.push('report setups');
99
- 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 = []; } }
100
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.
101
110
  let initiatives = null;
102
111
  if (pick && pick.id) {
@@ -137,6 +146,11 @@ module.exports = {
137
146
  outcomes, reports: reportsOk ? rp.body.reports || [] : [], reportsOk, missing, saved, mine,
138
147
  setups: setupsOk ? su.body : null, pinned, leads, archived, initiatives, goalMeta, ins, budgets, okr, promises, self: isSelf(ctx, pick),
139
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
+ }
140
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
141
155
  if (pick) {
142
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.515.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.515.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.515.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; })(),
@@ -955,3 +955,37 @@ body.ins-trv #pGoals .ins-trstrip { display: flex; } /* the narrow form hides
955
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
956
  .ins-allp .ins-th { display: none; }
957
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 = [
@@ -1902,9 +1902,15 @@ async function saveNewDash() {
1902
1902
  let chartDash = null;
1903
1903
  function chartHost(D) { return D && D.saved ? D : savedDash().find((d) => d.id === M.pinned) || savedDash()[0] || null; }
1904
1904
  function openChartEdit(goalId, D0) {
1905
- const D = chartHost(D0 || curDash()); chartDash = D;
1905
+ let D = chartHost(D0 || curDash());
1906
1906
  const g = M.goals.find((x) => x.id === goalId) || M.goals.find((x) => x.key) || M.goals[0];
1907
1907
  if (!g) { toast('There is no goal to chart yet. Add one with New goal.'); return; }
1908
+ // Reports and dashboards redesign R (RgR3Dash): a key result nothing reads opens its connector card, never "no reading yet, make a dashboard first"
1909
+ const kw = !g.readings ? okrKrs().find((k) => k.goal === g.id && k.need && !k.decision) : null;
1910
+ if (kw) { openKr(kw.key); return; }
1911
+ // with no saved dashboard, the chart goes on the one drafted from the OKR file; Save chart keeps that dashboard first
1912
+ if (!D) { const dd = draftedDash(); if (dd && dd.charts.length) D = { id: null, drafted: true, saved: false, name: dd.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: dd.charts, main: true }; }
1913
+ chartDash = D;
1908
1914
  const on = D && D.charts.includes(g.id), cs = (D && (M.chartSet || {})[D.id] || {})[g.id] || {};
1909
1915
  openSide({ kind: 'chart', eye: D ? 'Chart on ' + D.name : 'Chart · no dashboard yet', title: g.title, go: 'Save chart', onGo: saveChart,
1910
1916
  goOff: !D ? 'Make a dashboard first, with New dashboard' : '',
@@ -1962,8 +1968,13 @@ function chartSvg(o) {
1962
1968
  return `<svg class="ins-ch2" viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(o.pts.length + ' readings as a line')}">${tgt}${pv}<polygon class="ar" points="${line[0].split(',')[0]},${H} ${line.join(' ')} ${last[0]},${H}"/>${pj}<polyline class="ln" points="${line.join(' ')}"/>${ship}<circle class="dt" cx="${last[0]}" cy="${last[1]}" r="3.6" style="fill:${TONE_BAR[o.tone] || TONE_BAR.q}"/></svg>`;
1963
1969
  }
1964
1970
  async function saveChart() {
1965
- const D = chartDash, id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
1971
+ let D = chartDash; const id = $('#chGoal').value, m = $('#sideMsg'); if (!D) return;
1966
1972
  $('#sideGo').disabled = true; m.textContent = 'Saving.';
1973
+ if (D.drafted) {
1974
+ const k = await saveDash({ name: D.name, purpose: D.purpose, charts: D.charts.includes(id) ? D.charts : D.charts.concat(id), main: true });
1975
+ if (!k || k.ok !== true || !k.dashboard) { $('#sideGo').disabled = false; m.textContent = (k && k.error) || 'The dashboard could not be kept. Nothing changed.'; return; }
1976
+ D = Object.assign({}, k.dashboard, { saved: true }); chartDash = D;
1977
+ }
1967
1978
  const res = await pmPost('/api/insights-records/chart', { ws: M.board.ws, board: M.board.id, dash: D.id, goal: id, form: segVal('Form'), range: segVal('Range'), compare: $('#chCmp').value,
1968
1979
  target: $('#chTgt').getAttribute('aria-checked') === 'true', forecast: $('#chFc').getAttribute('aria-checked') === 'true', note: $('#chNote').value.trim(), add: !D.charts.includes(id) });
1969
1980
  $('#sideGo').disabled = false;
@@ -2013,7 +2024,7 @@ function reportGroups() {
2013
2024
  for (const f of R.missing ? [] : R.rows) { if (!by.has(f.cadence)) by.set(f.cadence, []); by.get(f.cadence).push(f); }
2014
2025
  const out = [], shown = (fs) => fs && fs.some((x) => !x.archived);
2015
2026
  for (const s of R.setupsOk === false ? [] : (R.setups || [])) { const fs = by.get(s.id); by.delete(s.id); out.push(shown(fs) ? { kind: 'file', x: { id: s.id, name: s.name, setup: s, files: fs } } : { kind: 'setup', x: s }); }
2016
- for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: id, setup: null, files: fs } });
2027
+ for (const [id, fs] of by) if (shown(fs)) out.push({ kind: 'file', x: { id, name: (R.names && R.names[id]) || id, setup: null, files: fs } }); // redesign R: a definition report is named by its title
2017
2028
  return out;
2018
2029
  }
2019
2030
  /** A schedule said short, for the status pill: "Every other Monday at 09:00" is "Every other Monday". */
@@ -2053,31 +2064,118 @@ function drawList() {
2053
2064
  const reps = reportGroups().filter((g) => hit(g.x.name)), dashes = M.dashboards.filter((d) => hit(dashName(d)));
2054
2065
  const nRep = reps.length + asks.length;
2055
2066
  $('#repCap').textContent = String(nRep + dashes.length);
2056
- $('#newRep').href = homeAsk('insights', 'Make a new report: ');
2067
+ $('#newRep').href = homeAsk('insights', 'Make a new report: '); // with no product picked, Home still makes it; with one, + opens the drafted report (below)
2057
2068
  const fbox = found.length ? `<div class="ins-fd" aria-label="Found in your work"><div class="ins-fdh"><b>Found in your work · ${found.length}</b><span class="note">Things you write by hand often</span></div>`
2058
2069
  + found.map((f) => `<div class="ins-fcard" data-f="${esc(f.id)}"><button type="button" class="nm" data-fx="open" aria-label="${esc(f.name)}: what PMOS found"><b>${esc(f.name)}</b><small>${esc(f.why)}</small></button><button type="button" class="ins-btn sm dark" data-fx="setup">Set it up</button><button type="button" class="ins-btn sm ghost" data-fx="dismiss">Not a report</button></div>`).join('') + '</div>' : '';
2059
2070
  const empty = R.missing ? '<div class="ins-empty"><b>The report files could not be read</b>, so none is listed.</div>'
2060
2071
  : R.setupsOk === false ? '<div class="ins-empty"><b>The report setups could not be read</b>, so what you set up is not listed.</div>' : '';
2061
- /* Intelligence redesign A (IgA5Reports): what needs you first (a draft waiting for your ✓, a proposal ready), then the ready-made set folded
2062
- * (its switches stay under the fold), then Yours, then the dashboards. */
2063
- const waits = reps.filter((G) => repState(G).draft), waitAsks = asks.filter((a) => a.state === 'ready');
2072
+ /* Reports and dashboards redesign R (RgR1Panel; A's IgA5Reports): Needs you first, each waiting report with one line of its headline and its one ✓
2073
+ * (Issue · Confirm the definition · Move to <product> · Resume, lib/report-acts.js), then Drafting on their moment and Weekly updates (the
2074
+ * ready-made switches, folded), then Yours, then the dashboards. Needs you is first on a phone too, before the one-product note. */
2075
+ const acts = R.acts || {};
2076
+ const waits = reps.filter((G) => acts[G.x.id]), waitAsks = asks.filter((a) => a.state === 'ready');
2064
2077
  const rest = reps.filter((G) => !waits.includes(G)), restAsks = asks.filter((a) => !waitAsks.includes(a));
2065
2078
  const head3 = '<div class="ins-rl ins-rlh" role="presentation"><span>Report</span><span>Next</span><span>Status</span></div>';
2066
- const needs = waits.length + waitAsks.length ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${waits.length + waitAsks.length}</span></div><div class="ins-rtbl" id="repNeeds">${head3}<div role="list">${waits.map(repRowHtml).join('')}${waitAsks.map(askRowHtml).join('')}</div></div>` : '';
2079
+ const nNeed = waits.length + waitAsks.length;
2080
+ const needs = nNeed ? `<div class="ins-sec"><h3>Needs you</h3><span class="q">${nNeed}</span></div><div class="ins-nlist" id="repNeeds" role="list">${waits.map(needRowHtml).join('')}${waitAsks.map(askNeedHtml).join('')}</div>` : '';
2067
2081
  const table = rest.length + restAsks.length ? `<div class="ins-sec"><h3>Yours</h3><span class="q">${rest.length + restAsks.length}</span></div><div class="ins-rtbl" id="repAsks">${head3}<div role="list">${rest.map(repRowHtml).join('')}${restAsks.map(askRowHtml).join('')}</div></div>`
2068
2082
  : !empty && !needs ? '<div class="ins-empty"><b>No report is set up and no report file is on record.</b> Reports are made in Home.</div>' : '';
2069
- const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length ? dashes.length + ' · a dashboard is a saved view of Goals' : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dashes.map(dashCard).join('')}</div>`;
2070
- const lib = RL.libHtml(LIB.L, { open: LIB.open, yours: false });
2071
- $('#repBody').innerHTML = `${oneNote()}${needs}<details class="ins-libfold"${LIB.fold ? ' open' : ''}><summary><b>Drafting on their moment</b> <span class="note">ready-made reports, nothing to set up</span></summary><div id="readyLib">${lib}</div></details><div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
2072
- const fd = $('#repBody .ins-libfold'); if (fd) fd.addEventListener('toggle', () => { LIB.fold = fd.open; });
2083
+ const dd = savedDash().length ? null : draftedDash();
2084
+ const dashBlock = `<div class="ins-sec"><h3>Dashboards</h3><span class="q">${dashes.length && !dd ? dashes.length + ' · a dashboard is a saved view of Goals' : dd ? '1 drafted · ' + esc(dd.name) : 'none saved · select rows, then Save'}</span></div><div class="ins-dgrid" role="list">${dd ? draftedCard(dd) : ''}${dashes.map(dashCard).join('')}</div>`;
2085
+ $('#repBody').innerHTML = `${needs}${oneNote()}${libFolds(LIB.L) || '<div id="readyLib"></div>'}<div id="listRows" aria-label="Dashboards and reports">${fbox}${empty}${table}${dashBlock}</div>`;
2086
+ wireFolds();
2073
2087
  if (!LIB.read && M.board && M.board.ws) { LIB.read = true; RL.readLib(M.board.ws).then((L) => { LIB.L = L; if (!$('#libSide').hidden) drawLib(); }); }
2074
2088
  }
2089
+ const wireFolds = () => { for (const fd of $$('#repBody .ins-libfold')) fd.addEventListener('toggle', () => { LIB.fold[fd.dataset.fold] = fd.open; }); };
2090
+ /** The ready-made switches as two folds (RgR1Panel): the moment reports (up to four peek lines: what each drafts from), then the weekly updates
2091
+ * (one line: how many are on and off). The switches themselves are inside, unchanged (pm/ready-lib.js). Zero is silence: an empty fold is not drawn. */
2092
+ function libFolds(L) {
2093
+ if (!L || !(L.groups || []).length) return '';
2094
+ const onlyKind = (weekly) => Object.assign({}, L, { lead: weekly ? L.lead : null, overlap: weekly ? L.overlap : null,
2095
+ groups: L.groups.map((g) => Object.assign({}, g, { rows: (g.rows || []).filter((r) => (r.kind === 'weekly') === weekly), more: weekly ? g.more : null }))
2096
+ .filter((g) => g.rows.length || (weekly && g.more && g.more.count)) });
2097
+ const all = L.groups.flatMap((g) => (g.rows || []).concat((g.more && g.more.rows) || []).map((r) => Object.assign({ level: g.level, gname: g.name }, r)));
2098
+ const moments = all.filter((r) => r.kind !== 'weekly' && r.on), wk = all.filter((r) => r.kind === 'weekly'), wkOn = wk.filter((r) => r.on).length;
2099
+ const peek = moments.slice(0, 4).map((r) => `<li><b>${esc(r.name)}</b>${r.level === 'initiative' ? '' : ' · ' + esc(r.gname)}${r.sub ? ` <span class="note">${esc(r.sub)}</span>` : ''}</li>`).join('');
2100
+ const mHtml = RL.libHtml(onlyKind(false), { open: LIB.open, yours: false }), wHtml = RL.libHtml(onlyKind(true), { open: LIB.open, yours: false });
2101
+ const foldBox = (k, title, sub, pk, body) => (body ? `<details class="ins-libfold" data-fold="${k}"${LIB.fold[k] ? ' open' : ''}><summary><b>${esc(title)}</b> <span class="note">${esc(sub)}</span></summary>${pk}<div class="ins-libin">${body}</div></details>` : '');
2102
+ return `<div id="readyLib">${foldBox('moment', 'Drafting on their moment', 'nothing to set up', peek ? `<ul class="ins-mlist">${peek}</ul>` : '', mHtml)}`
2103
+ + `${foldBox('weekly', 'Weekly updates', wk.length ? (wkOn ? wkOn + ' on' : 'none on') + (wk.length - wkOn ? ' · ' + (wk.length - wkOn) + ' off' : '') : '', '', wHtml)}</div>`;
2104
+ }
2105
+ /** A waiting report (RgR1Panel): its name, when it was drafted and for whom, one line of its headline, and its one ✓. */
2106
+ function needRowHtml(G) {
2107
+ const st = repState(G), a = (M.reports.acts || {})[G.x.id], s = st.s, d = st.draft;
2108
+ const about = (M.reports.about || {})[G.x.id];
2109
+ const sub = [d ? 'Drafted ' + md(day(d.slot)) : s && s.paused ? 'Paused' : '', s && s.audience ? 'for ' + s.audience : '', !s && a && a.act === 'confirm' ? 'its definition waits for your word' : '',
2110
+ s && s.next && !s.paused ? 'next ' + s.next : '', about ? 'in ' + (M.board.name || 'this product') + ', about ' + about.name : ''].filter(Boolean).join(' · ');
2111
+ const lead = d && d.lead ? d.lead : s && s.latest && s.latest.lead ? s.latest.lead : '';
2112
+ const key = G.kind + ':' + G.x.id;
2113
+ return `<div class="ins-nrow${selRow === key ? ' sel' : ''}" role="listitem"><button type="button" class="nm" ${G.kind === 'setup' ? `data-setup="${esc(G.x.id)}"` : `data-rpt="${esc(G.x.id)}"`}><b>${esc(G.x.name)}</b><small>${esc(sub)}</small>${lead ? `<span class="hl">${esc(lead)}</span>` : ''}</button>`
2114
+ + `<span class="ac"><span class="note">${esc(a.label.charAt(0).toLowerCase() + a.label.slice(1))}</span><button type="button" class="cz-ib sm solid" data-nact="${esc(G.x.id)}" aria-label="${esc(a.label + ': ' + G.x.name)}" title="${esc(a.label)}">${SV(I.chk, 2.4)}</button></span></div>`;
2115
+ }
2116
+ /** A proposal from Home that is ready: what was asked, and its ✓, which opens the proposal (Set it up is one press there). */
2117
+ const askNeedHtml = (a) => `<div class="ins-nrow" role="listitem"><button type="button" class="nm" data-ask="${esc(a.id)}"><b>${esc(a.proposal && a.proposal.name ? a.proposal.name : a.text)}</b><small>${esc('You asked in Home: ' + a.text)}</small></button>`
2118
+ + `<span class="ac"><span class="note">set it up</span><button type="button" class="cz-ib sm solid" data-ask="${esc(a.id)}" aria-label="${esc('Open the proposal: ' + a.text)}" title="Open the proposal">${SV(I.chk, 2.4)}</button></span></div>`;
2119
+ /** The one ✓ of a waiting report: Issue, Confirm the definition, Move to <product> or Resume. One press, then the panel reads again. */
2120
+ async function needPress(b) {
2121
+ const id = b.dataset.nact, a = (M.reports.acts || {})[id]; if (!a) return;
2122
+ const G = reportGroups().find((x) => x.x.id === id), name = G ? G.x.name : id;
2123
+ b.disabled = true;
2124
+ const r = a.act === 'issue' ? (a.sha ? await pmPost('/api/report/issue', { ws: M.board.ws, cadence: id, slot: a.slot, sha: a.sha }) : { ok: false, error: 'The draft could not be read, so it was not issued. Open it and issue it from the reader.' })
2125
+ : a.act === 'confirm' ? await pmPost('/api/report-setups/confirm', { ws: M.board.ws, id })
2126
+ : a.act === 'move' ? await pmPost('/api/report-setups/move', { ws: M.board.ws, id, to: a.to })
2127
+ : await pmPost('/api/report-setups/resume', { ws: M.board.ws, id });
2128
+ if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'Nothing changed. The server did not say why.'); return; }
2129
+ toast(a.act === 'issue' ? 'Issued ' + name + '. Send it from your own mail.' : a.act === 'confirm' ? 'Confirmed the definition of ' + name + '. Its drafts follow it as it is.'
2130
+ : a.act === 'move' ? 'Moved ' + name + ' to ' + a.toName + '. It drafts there from now on.' : 'Resumed ' + name + '.');
2131
+ await load();
2132
+ }
2133
+
2134
+ /* ── the first dashboard, drafted from the OKR file (RgR3Dash) ──────────────────────────────────────────────────────────────────────
2135
+ * With no saved dashboard, the panel offers one tile per key result of the OKR file (A's model.okrFile): a key result with a reader shows its
2136
+ * reading with the reader and its date; one without shows what it needs and the connector ✓. Nothing is a number without a reading. ✓ Keep saves
2137
+ * it as a dashboard of the read key results (POST /api/premium-dashboards/save); the waiting ones join it once a reader reads them. */
2138
+ function draftedDash() {
2139
+ const F = M.okrFile; if (!F || !F.total) return null;
2140
+ const tiles = F.objectives.flatMap((o) => o.krs.map((k) => {
2141
+ const g = k.goal ? M.goals.find((x) => x.id === k.goal) : null, rd = g && g.reading;
2142
+ if (k.read && g) return { k, read: true, big: g.nowTxt || (rd && rd.txt) || '—', dot: rd && rd.dot === 'behind' ? 'risk' : rd && rd.dot === 'broken' ? 'late' : 'ok', meta: [rd && rd.source ? rd.source : 'its reader', rd && rd.at ? md(day(rd.at)) : ''].filter(Boolean).join(' · ') };
2143
+ if (k.decision) return { k, read: false, big: 'Not yet', dot: 'none', meta: k.due ? 'a decision, due ' + md(day(k.due)) : 'a decision' };
2144
+ return { k, read: false, big: '—', dot: 'none', meta: k.need ? 'needs ' + k.need : 'no source named', need: k.need || '' };
2145
+ }));
2146
+ return { name: (F.period ? F.period.label + ' ' : '') + 'key results', tiles, charts: tiles.filter((t) => t.read).map((t) => t.k.goal), nRead: tiles.filter((t) => t.read).length };
2147
+ }
2148
+ const okrTileHtml = (t) => `<div class="ins-dtile${t.read ? '' : ' w'}" role="listitem"><span class="lb"><span class="kr">${esc(t.k.token)}</span>${esc(t.k.title)}</span><b class="big">${esc(t.big)}</b>`
2149
+ + `<span class="ft"><span class="note"><span class="ins-tdot ${t.dot}" aria-hidden="true"></span>${esc(t.meta)}</span>${t.need ? `<button type="button" class="cz-ib sm" data-ddash="con" aria-label="${esc('Connect ' + t.need)}" title="${esc('Connect ' + t.need)}">${SV(I.chk, 2.4)}</button>` : ''}</span></div>`;
2150
+ function draftedCard(D) {
2151
+ return `<div class="ins-dcard ins-ddraft" role="listitem"><span class="ins-dh"><b>${esc(D.name)}</b><span class="cz-pill q">Drafted</span></span>`
2152
+ + `<p class="note">${esc('I drafted it from your OKR file: one tile per key result. ' + (D.nRead ? D.nRead + ' read' + (D.tiles.length - D.nRead ? ', ' + (D.tiles.length - D.nRead) + ' wait on a source, and each tile says which.' : '.') : 'None has a reading yet, and each tile says what it needs.'))}</p>`
2153
+ + `<div class="ins-dtiles" role="list">${D.tiles.slice(0, 8).map(okrTileHtml).join('')}</div>`
2154
+ + `<span class="ins-dact">${D.nRead ? `<span class="note">keep it, and it opens first</span><button type="button" class="cz-ib sm solid" data-ddash="keep" aria-label="Keep this dashboard" title="Keep this dashboard">${SV(I.chk, 2.4)}</button>` : '<span class="note">kept once one key result has a reading</span>'}</span></div>`;
2155
+ }
2156
+ async function draftedPress(b) {
2157
+ if (b.dataset.ddash === 'con') { import('./cozy-settings.js').then((S) => S.openSettings('connectors')).catch(() => toast('Open Settings › Connectors.')); return; }
2158
+ const D = draftedDash(); if (!D || !D.charts.length) return;
2159
+ b.disabled = true;
2160
+ const r = await saveDash({ name: D.name, purpose: 'Drafted from the OKR file: one tile per key result with a reading.', charts: D.charts, main: true });
2161
+ if (!r || r.ok !== true) { b.disabled = false; toast((r && r.error) || 'The dashboard could not be saved. Nothing changed.'); return; }
2162
+ toast('Kept ' + D.name + '. It opens first.'); await load();
2163
+ }
2075
2164
  /* the ready-made group before Yours (batch 17 item 1, PbA6): read once per open of the panel, again after a switch */
2076
- const LIB = { L: null, read: false, open: new Set(), fold: false };
2077
- const drawLib = () => { const el = $('#readyLib'); if (el) el.innerHTML = RL.libHtml(LIB.L, { open: LIB.open, yours: false }); };
2165
+ const LIB = { L: null, read: false, open: new Set(), fold: {} };
2166
+ const drawLib = () => { const el = $('#readyLib'); if (el) { el.outerHTML = libFolds(LIB.L) || '<div id="readyLib"></div>'; wireFolds(); } };
2167
+ /* + in the panel header (RgR4Make): the drafted report of this view, never a blank chat. With all products on screen there is no one product to
2168
+ * draft for, so it keeps its link to Home. */
2169
+ $('#newRep').addEventListener('click', (e) => {
2170
+ if (PF || !M || !M.board || !M.board.ws) return;
2171
+ e.preventDefault();
2172
+ const D = curDash(); RI.openDraft(D && D.saved ? 'dashboard:' + D.id : 'view');
2173
+ });
2078
2174
  RL.wireLib($('#repBody'), { get ws() { return M && M.board ? M.board.ws : ''; }, open: LIB.open, toast: (t) => toast(t), redraw: drawLib,
2079
2175
  reload: async () => { LIB.L = await RL.readLib(M.board.ws); drawLib(); } });
2080
2176
  $('#repBody').addEventListener('click', async (e) => {
2177
+ const na = e.target.closest('[data-nact]'); if (na) { if (!na.disabled) needPress(na); return; }
2178
+ const dk = e.target.closest('[data-ddash]'); if (dk) { draftedPress(dk); return; }
2081
2179
  const d = e.target.closest('[data-d]'); if (d) { setView(d.dataset.d); return; }
2082
2180
  const fx = e.target.closest('[data-fx]');
2083
2181
  if (fx) {
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- Reports and dashboards (⋯; the old Library): Found in your work, requests from Home, the reports, then the dashboards -->
70
70
  <aside class="ins-side ins-lib" id="libSide" role="dialog" aria-modal="false" aria-labelledby="libTitle" tabindex="-1" hidden>
71
- <div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report (opens Home)" title="New report (opens Home)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
71
+ <div class="ins-sh"><div><h2 id="libTitle">Reports and dashboards</h2></div><span class="ins-st"><a class="cz-ib" id="newRep" href="/premium.html?ask=insights" aria-label="New report: Claude drafts it from this view" title="New report: Claude drafts it from this view"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></a></span><button type="button" class="cz-ib" id="libX" aria-label="Close" title="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
72
72
  <div class="ins-sb" id="repBody"></div>
73
73
  <span class="vh" id="repCap"></span>
74
74
  </aside>
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "Reports and dashboards redesign R: each waiting report has one ✓ (Issue, Confirm the definition, Move to a product, Resume), a definition report is named by its title, ✦ Move takes a report to the product it is about, and the product page's ✦ line is Intelligence's (lib/report-acts.js).",
3
+ "modules": ["report-acts.test.js"],
4
+ "seconds": { "report-acts.test.js": 1 }
5
+ }