create-pmos 0.210.0 → 0.212.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-light-load-test.js +110 -0
  7. package/workspace/c3-settings-test.js +53 -2
  8. package/workspace/guide/briefing.md +7 -1
  9. package/workspace/guide/data-freshness.md +35 -8
  10. package/workspace/guide/index.md +1 -1
  11. package/workspace/lib/driver-run.js +4 -3
  12. package/workspace/lib/freshness.js +2 -0
  13. package/workspace/lib/refresh-run.js +300 -0
  14. package/workspace/lib/refresh.js +464 -0
  15. package/workspace/lib/report-finder.js +22 -1
  16. package/workspace/lib/route-modules.js +1 -0
  17. package/workspace/lib/routes-calendar-views.js +10 -2
  18. package/workspace/lib/routes-pm-items.js +2 -0
  19. package/workspace/lib/routes-refresh.js +93 -0
  20. package/workspace/lib/settings-store.js +0 -2
  21. package/workspace/package-lock.json +2 -2
  22. package/workspace/package.json +1 -1
  23. package/workspace/public/pm/c3-brief-pulse.js +4 -4
  24. package/workspace/public/pm/c3-calpop.js +11 -3
  25. package/workspace/public/pm/c3-dock.js +9 -0
  26. package/workspace/public/pm/c3-driver.js +1 -1
  27. package/workspace/public/pm/c3-look.js +27 -0
  28. package/workspace/public/pm/c3-plan.js +3 -2
  29. package/workspace/public/pm/c3-quiet.css +6 -1
  30. package/workspace/public/pm/c3-report-store.js +2 -1
  31. package/workspace/public/pm/c3-scan-setup.js +3 -7
  32. package/workspace/public/pm/c3-scan-words.js +7 -0
  33. package/workspace/public/pm/c3-scan.js +6 -4
  34. package/workspace/public/pm/c3-settings.css +11 -0
  35. package/workspace/public/pm/c3-settings.js +236 -27
  36. package/workspace/public/pm/c3-shell.js +56 -100
  37. package/workspace/replaced-tests.json +11 -0
  38. package/workspace/server.js +106 -12
  39. package/workspace/test-lists/c3-desktop-light-load.json +6 -0
  40. package/workspace/test-lists/data-refresh-settings.json +5 -0
@@ -15,29 +15,18 @@ import { row, button } from './primitives.js';
15
15
  import { pmGet } from './http.js';
16
16
  import { ensureC3Css, c3ic, c3Phone, mountPmWork } from './pm-work-view.js';
17
17
  import { mountBriefing, showCard, flipBack } from './briefing-deck.js';
18
- import { mountCalendarViews } from './calendar-views.js';
19
18
  import { todaySection } from './c3-brief-today.js';
20
19
  import { needsSection, needsCount } from './c3-brief-needs.js';
21
20
  import { scanSection } from './c3-scan.js';
22
21
  import { reportFoundSection } from './c3-report-store.js';
23
22
  import { pulse } from './c3-brief-pulse.js';
24
23
  import { loadBriefPage, onBriefPage } from './c3-brief-data.js';
25
- import { mountLifecycle } from './lifecycle-lens.js';
26
- import { mountForecast } from './timeline-forecast-view.js';
27
- import { mountReleases } from './releases-view.js';
28
- import { mountReports, showReportsView } from './c3-reports.js';
29
24
  import { createDock } from './c3-dock.js';
30
25
  import { reviewSection, footLine, rowApi, BX, toast, setNeedsTitle, hooks as briefHooks } from './c3-brief.js';
31
26
  import { openSheet } from './c3-sheet.js';
32
27
  import { mountCalPopupTrigger } from './c3-calpop.js';
33
- import { planNode, dueCard, crossLinks, newInitLine } from './c3-plan.js';
34
- import { initiativesViewCtx } from './registry.js';
35
- import { mount as mountRoadmap } from './c3-roadmap.js';
36
- import { mount as mountBreakdown } from './c3-breakdown.js';
28
+ import { planNode } from './c3-plan.js';
37
29
  import { ic } from './c3-terminal-icons.js';
38
- import { mount as mountTickets, badge as ticketsBadge } from './c3-tickets.js';
39
- import { mount as mountDocs, badge as docsBadge } from './c3-docs.js';
40
- import { mount as mountSignals, badge as signalsBadge } from './c3-signals.js';
41
30
  import { mountDaypart } from './c3-daypart.js';
42
31
  import { mountAmbient } from './c3-ambient.js';
43
32
  import { mountTicker } from './shell-ticker.js';
@@ -45,20 +34,12 @@ import { mountBell, openBellList } from './shell-bell.js';
45
34
  import { onBriefing, loadBriefing } from './shell-ticker.js';
46
35
  import { registry, filterPalette, rowStep } from './briefing-model.js';
47
36
  import { freshnessLine } from './freshness-line.js';
48
- import { mountInitiatives } from './c3-initiatives.js';
49
- import { mountSettings, gearIcon, applyLook } from './c3-settings.js';
37
+ import { gearIcon, applyLook } from './c3-look.js';
50
38
  import { mountQuiet } from './c3-quiet.js';
51
- import { mountPhone, phoneWanted } from './c3-phone.js';
52
39
 
53
40
  /* In demo mode each tab's deck asks for its own cards (the mockup draws a different deck per tab); real mode sends no scope. Reports always sends its own (repTab). */
54
41
  const sc = (name) => (DEMO ? name : undefined);
55
42
  const TABS = [['brief', 'Briefing', 'sun'], ['cal', 'Calendar', 'cal'], ['work', 'Work', 'todo'], ['init', 'Initiatives', 'init'], ['rep', 'Reports', 'report']];
56
- /* The mockup's eight Initiatives views, in its order: Lifecycle, Roadmap, Breakdown, Timeline (= Forecast), Tickets, Releases, Documents, Signals. */
57
- const INITS = [['lifecycle', 'Lifecycle', mountLifecycle, 'init'], ['roadmap', 'Roadmap', mountRoadmap, 'flag'], ['breakdown', 'Breakdown', mountBreakdown, 'tree'], ['forecast', 'Timeline', mountForecast, 'cal'],
58
- ['tickets', 'Tickets', mountTickets, 'check'], ['releases', 'Releases', mountReleases, 'out'], ['documents', 'Documents', mountDocs, 'doc'], ['signals', 'Signals', mountSignals, 'alert']];
59
- /* The five views of this page's own (c3-roadmap.js ... c3-signals.js) draw the Plan lens in their own left column, as Lifecycle does. A view with a `badge` gets a count on its button. */
60
- const OWN = new Set(['roadmap', 'breakdown', 'tickets', 'documents', 'signals']);
61
- const BADGES = { tickets: ticketsBadge, documents: docsBadge, signals: signalsBadge };
62
43
  const THEME_KEY = 'pmos.c3.theme';
63
44
  const q = new URLSearchParams(location.search);
64
45
  const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } } };
@@ -66,7 +47,7 @@ const DEMO = q.get('demo') === '1'; // demo mode: c3-demo.js answers the page'
66
47
  /* The dock's open or folded choice, kept on this device. No choice yet: folded. */
67
48
  const dockPref = () => store.get('pmos.c3.dock') === 'open';
68
49
  const keepDock = () => store.set('pmos.c3.dock', S.dockOpen ? 'open' : 'folded');
69
- const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {}, init: 'lifecycle', initRows: null, dockOpen: true, wide: true };
50
+ const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {}, dockOpen: true, wide: true };
70
51
 
71
52
  /* A view reads a route with no ws and the server answers for its home workspace. This page names one, so a GET to /api/ with no ws gets it. */
72
53
  function withWs() {
@@ -152,12 +133,15 @@ function toggleMore() {
152
133
  }
153
134
  /* The phone companion (c3-phone.js): at 640px or less it fills the page and the desktop grid is hidden. There is no Full console:
154
135
  * the old choice (pmos.c3.phone = 'desk', stored before 2026-10-13) is cleared so no phone stays on the desktop pages. */
136
+ /* as c3-phone.js phoneWanted, without loading that module on a desktop */
137
+ const phoneWanted = () => typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches && store.get('pmos.c3.testDesk') !== 'desk';
155
138
  function syncPhone() {
156
139
  if (store.get('pmos.c3.phone')) store.set('pmos.c3.phone', '');
157
- const want = phoneWanted(store);
140
+ const want = phoneWanted();
158
141
  if (want && !S.phone) {
159
142
  S.root.classList.add('c3-phone-on');
160
- S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo });
143
+ const p = S.phone = { dispose: () => { p.gone = true; } }; // the companion's code loads first (c3-phone.js); narrowing back before it lands cancels it
144
+ import('./c3-phone.js').then((m) => { if (p.gone || S.phone !== p) return; S.phone = m.mountPhone(S.root, { ws: S.ws, demo: S.demo }); }).catch(() => { if (S.phone === p) { S.phone = null; S.root.classList.remove('c3-phone-on'); } });
161
145
  } else if (!want && S.phone) {
162
146
  S.phone.dispose(); S.phone = null;
163
147
  S.root.classList.remove('c3-phone-on');
@@ -319,12 +303,12 @@ function lensChanged(l) {
319
303
  if (typeof window !== 'undefined') window.addEventListener('pmos:go-tab', (e) => {
320
304
  const d = (e && e.detail) || {}; if (!d.tab) return;
321
305
  showTab(d.tab, true);
322
- if (d.tab === 'settings' && d.section && S.settings) S.settings.show(d.section);
323
- if (d.tab === 'rep' && d.view) showReportsView(d.view);
306
+ if (d.tab === 'settings' && d.section) settingsReady().then((st) => st.show(d.section), () => {});
307
+ if (d.tab === 'rep' && d.view) reportsView(d.view);
324
308
  });
325
309
  function goTarget(t) {
326
310
  /* a dashboard schedule's notice (routes-briefing.js notices): Reports, then its Drafts tab */
327
- if (t && t.tab && BUILD[t.tab]) { showTab(t.tab, true); if (t.tab === 'rep' && t.view) showReportsView(t.view); return; }
311
+ if (t && t.tab && BUILD[t.tab]) { showTab(t.tab, true); if (t.tab === 'rep' && t.view) reportsView(t.view); return; }
328
312
  showTab('brief', true);
329
313
  if (!t) return;
330
314
  if (t.card !== undefined && t.card !== null) { showCard(S.deckHost, t.card); return; }
@@ -363,7 +347,7 @@ function paletteItems() {
363
347
  ...TABS.map(([id, label, icon]) => ({ label, kind: 'place', icon, run: () => showTab(id, true) })),
364
348
  { label: 'Settings', kind: 'place', icon: 'shield', run: () => showTab('settings', true) }];
365
349
  for (const k of Object.keys(registry('people') || {})) { const p = registry('people')[k]; items.push({ label: p.n + ' · ' + p.role, kind: 'person', icon: 'user', run: () => openPerson(k) }); }
366
- for (const d of registry('docs') || []) items.push({ label: d.n, kind: 'document', icon: d.icon || 'doc', run: () => { showTab('init', true); const b = S.initSeg && S.initSeg.querySelector('[data-c3-init="documents"]'); if (b) b.click(); } });
350
+ for (const d of registry('docs') || []) items.push({ label: d.n, kind: 'document', icon: d.icon || 'doc', run: () => showTab('init', true) });
367
351
  for (const x of S.palSessions || []) items.push({ label: (x.branch || x.tree || '') ? ((x.branch || String(x.tree).split('/').pop()) + ' · ' + x.name) : x.name, kind: 'session', icon: 'branch', run: () => openSession(x) });
368
352
  return items;
369
353
  }
@@ -457,78 +441,44 @@ function dock() {
457
441
  }
458
442
  /* The dock is open on the Briefing and folded away on the other tabs. A folded dock leaves no rail (PM, 2026-10-12): the header's Sessions button, Ctrl J and the More menu reach it. */
459
443
  function syncDock() {
444
+ const opening = S.dockEl.hidden && S.dockOpen;
460
445
  S.dockEl.hidden = !S.dockOpen;
446
+ if (opening && S.dockStarted) S.dock.refresh(); // a folded dock reads only the runs; opening it reads the tree (c3-dock.js refresh)
461
447
  }
462
448
 
463
449
  /* ── the other tabs ── */
464
- function initTab(pane) {
465
- S.initSeg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Initiatives views', 'data-c3pg': 'init-switch', style: 'overflow-x:auto;max-width:100%' });
466
- S.initHosts = {};
467
- const ws = () => S.workspaces.find((w) => w.id === S.ws) || {};
468
- const reload = () => { S.initRows = null; S.mounted = Object.fromEntries(Object.entries(S.mounted).filter(([k]) => !k.startsWith('init-'))); loadInitRows().then(() => showInit(S.init)); };
469
- const add = newInitLine({ get repo() { return ws().root || null; }, ws: S.ws, get rows() { return (S.initRows && S.initRows.rows) || []; }, onAdded: reload });
470
- const bar = el('div', { class: 'calbar' }, S.initSeg);
471
- const body = el('div', { class: 'c3pg-initbody' });
472
- for (const [id] of INITS) { S.initHosts[id] = el('div', { 'data-in-host': id, hidden: true }); body.append(S.initHosts[id]); }
473
- pane.append(add, bar, body);
474
- S.initNote = el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-c3pg': 'init-note' }, 'Reading the repository');
475
- body.prepend(S.initNote);
476
- drawInitSwitch();
477
- loadInitRows().then(() => { showInit(S.init); loadBadges(); });
478
- }
479
- /* The counts on the sub-view buttons: each view's own `badge(ctx)` (a number, or null when it cannot say). */
480
- function loadBadges() {
481
- const ws = S.workspaces.find((w) => w.id === S.ws) || {};
482
- const ctx = { repo: ws.root || null, ws: S.ws, rows: (S.initRows && S.initRows.rows) || [] };
483
- S.badges = S.badges || {};
484
- for (const [id, fn] of Object.entries(BADGES)) Promise.resolve().then(() => fn(ctx)).then((n) => { S.badges[id] = Number(n) || 0; drawInitSwitch(); }, () => { /* no count */ });
485
- }
486
- function drawInitSwitch() {
487
- S.initSeg.replaceChildren(...INITS.map(([id, label, mount, icon]) => {
488
- const n = S.badges && S.badges[id];
489
- return el('button', { type: 'button', 'aria-pressed': String(S.init === id), 'data-c3-init': id, onclick: () => showInit(id) }, c3ic(icon, 'sm'), label, n ? [' ', el('span', { class: 'bdg', 'data-c3-init-badge': id }, String(n))] : null);
490
- }));
491
- }
492
- /* The roadmap read answers { pending } while the repository is still being read. Ask again until it has rows, and say so meanwhile. */
493
- async function loadInitRows() {
494
- for (let n = 0; n < 40; n++) {
495
- const r = await pmGet('/api/initiatives?ws=' + encodeURIComponent(S.ws) + '&view=roadmap');
496
- if (r && r.horizons) {
497
- S.initRows = { rows: [...(r.horizons.now || []), ...(r.horizons.next || []), ...(r.horizons.later || []), ...(r.dormant || [])], sig: r.sig || null, strategy: r.strategy || null };
498
- S.initNote.hidden = true;
499
- return;
500
- }
501
- if (!(r && r.pending)) {
502
- S.initRows = { rows: [], sig: null };
503
- S.initNote.hidden = false;
504
- S.initNote.textContent = 'The initiatives could not be read' + (r && r.error ? ': ' + r.error : '.');
505
- return;
506
- }
507
- S.initNote.hidden = false; S.initNote.textContent = 'Reading the repository';
508
- await new Promise((res) => setTimeout(res, 4000));
509
- }
510
- S.initRows = { rows: [], sig: null };
511
- S.initNote.textContent = 'The repository is taking long to read. Reload in a moment for the initiatives.';
512
- }
513
- function showInit(id) {
514
- S.init = id; drawInitSwitch();
515
- const ws = S.workspaces.find((w) => w.id === S.ws) || {};
516
- for (const [k, , mount] of INITS) {
517
- if (!mount) continue;
518
- S.initHosts[k].hidden = k !== id;
519
- if (k === id && !S.mounted['init-' + k] && S.initRows) {
520
- S.mounted['init-' + k] = true;
521
- const ctx = { ...initiativesViewCtx({ repo: ws.root || null, ws: S.ws, rows: S.initRows.rows, sig: S.initRows.sig, openInitiative: () => location.assign('/classic.html'), setState: () => {} }), strategy: S.initRows.strategy, plan: planNode(sc(OWN.has(k) ? 'init-' + k : 'init')), links: crossLinks((t) => showTab(t, true)) };
522
- if (k === 'lifecycle' || OWN.has(k)) mount(S.initHosts[k], Object.freeze(ctx));
523
- else { const view = el('div'); S.initHosts[k].replaceChildren(ctx.plan, view); mount(view, Object.freeze({ ...ctx, plan: null })); }
524
- }
525
- }
526
- }
527
450
  /* Reports: the headline deck on top (the Plan deck of the Reports scope), then the three tabs (Dashboards, Reports, Charts; c3-reports.js).
528
451
  * The Reports deck always names its scope, so real mode gets the engine's report signals and never the Briefing's cards (console3-deck-flow-fixes). */
529
- function repTab(pane) { mountReports(pane, { ws: S.ws, pmGet, deck: (h) => h.append(planNode('rep')) }); }
452
+ function repTab(pane) { return import('./c3-reports.js').then((m) => m.mountReports(pane, { ws: S.ws, pmGet, deck: (h) => h.append(planNode('rep')) })); }
453
+ /* Reports opens on a view (Drafts, a dashboard). Before the tab has loaded, c3-reports.js keeps the view and opens on it. */
454
+ const reportsView = (v) => import('./c3-reports.js').then((m) => m.showReportsView(v));
530
455
 
531
- const BUILD = { brief: briefTab, cal: (p) => mountCalendarViews(p, { view: 'week', page: true, scope: sc('cal'), deck: planNode('cal', { bare: true }) }), work: (p) => { mountPmWork(p, { go: (t) => showTab(t, true), ws: S.ws }); }, init: (p) => mountInitiatives(p, { ws: S.ws, workspaces: S.workspaces, demo: DEMO, showTab }), rep: repTab };
456
+ /* Each tab's code loads the first time the tab is shown (c3-desktop-light-load): the page starts with the Briefing's modules only.
457
+ * The Work tab's module (pm-work-view.js) is already loaded for the console's shared helpers. */
458
+ const BUILD = { brief: briefTab,
459
+ cal: (p) => import('./calendar-views.js').then((m) => m.mountCalendarViews(p, { view: 'week', page: true, scope: sc('cal'), deck: planNode('cal', { bare: true }) })),
460
+ work: (p) => { mountPmWork(p, { go: (t) => showTab(t, true), ws: S.ws }); },
461
+ init: (p) => import('./c3-initiatives.js').then((m) => m.mountInitiatives(p, { ws: S.ws, workspaces: S.workspaces, demo: DEMO, showTab })),
462
+ rep: repTab };
463
+ /* A tab whose code could not load says so in its pane, and the next press tries again. */
464
+ function build(id) {
465
+ S.mounted[id] = Promise.resolve().then(() => BUILD[id](S.panes[id])).catch((e) => {
466
+ delete S.mounted[id];
467
+ S.panes[id].replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status', 'data-c3pg': 'tab-error' }, 'This page could not load: ' + String((e && e.message) || e) + '. Open the tab again to retry.'));
468
+ });
469
+ }
470
+ /* Settings loads on its first opening (c3-settings.js). `then(fn)` runs fn with the mounted Settings. */
471
+ function settingsReady() {
472
+ if (!S.settingsLoad) {
473
+ S.settingsCtx = { ws: S.ws, workspaces: S.workspaces, theme: S.theme, themePref: S.themePref, setTheme, goTab: (t, v) => { showTab(t, true); if (t === 'rep' && v) reportsView(v); }, section: q.get('section') || '', root: S.root };
474
+ S.settingsLoad = import('./c3-settings.js').then((m) => { S.settings = m.mountSettings(S.panes.settings, S.settingsCtx); return S.settings; });
475
+ S.settingsLoad.catch((e) => {
476
+ S.settingsLoad = null;
477
+ S.panes.settings.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status', 'data-c3pg': 'tab-error' }, 'Settings could not load: ' + String((e && e.message) || e) + '. Open Settings again to retry.'));
478
+ });
479
+ }
480
+ return S.settingsLoad;
481
+ }
532
482
 
533
483
  function showTab(id, push) {
534
484
  if (!BUILD[id] && id !== 'settings') id = 'brief';
@@ -543,9 +493,9 @@ function showTab(id, push) {
543
493
  if (S.gearBtn) S.gearBtn.setAttribute('aria-pressed', String(id === 'settings'));
544
494
  if (id === 'settings') {
545
495
  S.ttl.replaceChildren(gearIcon('lg'), 'Settings'); document.title = 'Settings · PMOS console';
546
- if (!S.settings) { S.settingsCtx = { ws: S.ws, workspaces: S.workspaces, theme: S.theme, themePref: S.themePref, setTheme, goTab: (t, v) => { showTab(t, true); if (t === 'rep' && v) showReportsView(v); }, section: q.get('section') || '', root: S.root }; S.settings = mountSettings(S.panes.settings, S.settingsCtx); }
496
+ if (!S.settings) settingsReady();
547
497
  else S.settings.refresh();
548
- } else if (!S.mounted[id]) { S.mounted[id] = true; BUILD[id](S.panes[id]); }
498
+ } else if (!S.mounted[id]) build(id);
549
499
  S.root.dataset.tab = id;
550
500
  S.dockOpen = S.wide && id === 'brief' && dockPref(); syncDock(); // the dock starts folded (c3-quiet-pages); Ctrl J or the More menu opens it, and the choice is kept
551
501
  S.pageView.classList.remove('view'); void S.pageView.offsetWidth; S.pageView.classList.add('view');
@@ -565,22 +515,27 @@ function briefCount() {
565
515
  onBriefing((x) => { b = x; upd(); }); onBriefPage((x) => { page = x; upd(); });
566
516
  loadBriefing(); if (S.tab !== 'brief') loadBriefPage();
567
517
  }
518
+ /* The Briefing's first read has answered (or 4 s have passed), and the browser has a free moment. */
519
+ function afterBriefing() {
520
+ const idle = () => new Promise((res) => (typeof requestIdleCallback === 'function' ? requestIdleCallback(() => res(), { timeout: 1500 }) : setTimeout(res, 200)));
521
+ return Promise.race([loadBriefing(), new Promise((res) => setTimeout(res, 4000))]).then(idle, idle);
522
+ }
568
523
  async function askCount() {
569
524
  if (S.demo) { setBdg(S.initBdg, 4); return; }
570
- const j = await pmGet('/api/needs?ws=' + encodeURIComponent(S.ws)).catch(() => null);
525
+ const j = await pmGet('/api/needs?ws=' + encodeURIComponent(S.ws) + '&kind=triage').catch(() => null); // only the triage rows: the badge counts them
571
526
  if (!j || !Array.isArray(j.needs)) return;
572
527
  setBdg(S.initBdg, j.needs.filter((r) => r && r.kind === 'triage').length);
573
528
  }
574
529
 
575
530
  async function workCount() {
576
- const j = await pmGet('/api/pm-items').catch(() => null);
577
- if (!j || !j.ok || !j.lists) return;
531
+ const j = await pmGet('/api/pm-items?counts=1').catch(() => null); // the counts alone; the Work page reads the lists when it opens
532
+ if (!j || !j.ok || !(j.lists || j.counts)) return;
578
533
  if (j.counts && typeof j.counts.inbox === 'number') { // the Work page's own counts: what is to process (inbox) plus what is on today
579
534
  const w = j.counts.inbox + (Number(j.counts.today) || 0);
580
535
  S.workBdg.textContent = String(w); S.workBdg.hidden = w === 0;
581
536
  return;
582
537
  }
583
- const n = (j.lists.triage || []).length + (S.demo ? 0 : (j.lists.today || []).length); // the mockup's Work badge counts what is to triage
538
+ const L = j.lists || {}; const n = (L.triage || []).length + (S.demo ? 0 : (L.today || []).length); // the mockup's Work badge counts what is to triage
584
539
  S.workBdg.textContent = String(n); S.workBdg.hidden = n === 0;
585
540
  }
586
541
 
@@ -628,7 +583,8 @@ async function boot() {
628
583
  if (q.get('studio') && /^[a-z][a-z0-9-]{0,39}$/.test(q.get('studio'))) { if (!start) start = 'rep'; import('./c3-design-studio.js').then((m) => m.openStudio({ ws: S.ws, id: q.get('studio'), name: '' })).catch(() => { /* the studio is not available */ }); }
629
584
  if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
630
585
  new ResizeObserver(moveInd).observe(S.tabNav);
631
- workCount(); briefCount(); askCount(); S.dock.start();
586
+ briefCount(); S.dock.start(); S.dockStarted = true;
587
+ afterBriefing().then(() => { workCount(); askCount(); }); // the Work and Initiatives counts wait until the Briefing has its own reads
632
588
  setInterval(() => { S.sub.textContent = dateLine(); }, 30000);
633
589
  }
634
590
 
@@ -173,6 +173,17 @@
173
173
  "B1 with nothing stored, status says absent",
174
174
  "B5 status never carries the key or its length"
175
175
  ]
176
+ },
177
+ {
178
+ "id": "STD-REFRESH",
179
+ "file": "workspace/proactive.test.js",
180
+ "run": "data-refresh-settings",
181
+ "ref": "planning/prd/data-refresh-settings-prd.md R3 (the PM said Build on 2026-10-14)",
182
+ "reason": "R3 moves the morning brief's schedule from schedules.json to Settings › Data refresh. The brief set no longer writes a schedule; it turns the brief source on, daily at 06:45. H8 now asserts that through GET /api/refresh, and I7 accepts the CLI's new 'already on in Settings, Data refresh' line.",
183
+ "names": [
184
+ "H8 one press creates the daily 06:45 morning-brief schedule once; a second press adds nothing",
185
+ "I7 `brief schedule` reports that the schedule already exists"
186
+ ]
176
187
  }
177
188
  ],
178
189
  "entries": [
@@ -79,6 +79,9 @@ const validateLib = require('./lib/validate.js');
79
79
  const pageAgent = require('./lib/page-agent.js'); // page-starts-agent — the fixed prompts a page press may start
80
80
  const scanRunLib = require('./lib/scan-run.js'); // horizon-scan — the watch list suggest run and the scheduled scans, in the 60-second tick
81
81
  const reportFinderLib = require('./lib/report-finder.js'); // report-workspace — the weekly look for the reports the PM already sends
82
+ const refreshLib = require('./lib/refresh.js'); // data-refresh-settings — the sources, their schedules and the refresh log
83
+ const refreshRunLib = require('./lib/refresh-run.js'); // data-refresh-settings — the engine bound to this server (created by the tick below)
84
+ let refreshRun = null;
82
85
  const driverRunLib = require('./lib/driver-run.js'); // initiative-driver — wakes, the unblock sweep and the runs, in the 60-second tick
83
86
  const headlessLib = require('./lib/headless.js'); // workspace-headless-jobs — which jobs run as `claude -p`, and with which tools
84
87
  const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
@@ -1945,7 +1948,7 @@ let kitBusy = null; // workspace-kit-update: the one kit or app update
1945
1948
  const autoSync = require('./lib/auto-sync').createAutoSync({
1946
1949
  roots: () => [...new Set([...workspaces.values()].map((w) => w.root))],
1947
1950
  busy: syncing,
1948
- onDone: (root) => boardCache.invalidate(root),
1951
+ onDone: (root, out) => { boardCache.invalidate(root); try { if (refreshRun) refreshRun.codeRun('sync', { trigger: 'change', result: out && out.ok ? 'done' : 'failed', got: out && out.ok ? (out.message || 'synced') : null, error: out && !out.ok ? out.message || 'the sync failed' : null }); } catch { /* the log is not the sync's business */ } },
1949
1952
  label: `${os.hostname()} (auto-sync)`,
1950
1953
  });
1951
1954
  // workspace-data-freshness (R2 to R4): the facts cache is kept in the runtime state folder, and ONE change detector per
@@ -1958,7 +1961,13 @@ const freshness = require('./lib/freshness.js').create({
1958
1961
  workspaces: () => [...workspaces.values()],
1959
1962
  schedules: () => readSchedules(),
1960
1963
  factsStatus: (root) => factsAsync.status(root),
1961
- onChange: (w) => { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root); },
1964
+ onChange: (w) => {
1965
+ // data-refresh-settings: the repository source is the rebuild on change; switched off in Data refresh, a change rebuilds nothing.
1966
+ if (refreshRun && !refreshRun.jobGate('repo').ok) return;
1967
+ boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root);
1968
+ if (refreshRun) refreshRun.codeRun('repo', { trigger: 'change', ws: w.id, got: 'a change in the repository' });
1969
+ },
1970
+ scheduleOf: (skill) => (refreshRun && refreshRun.managedSkill(skill) ? refreshRun.scheduleOfSkill(skill) : undefined),
1962
1971
  pollMs: Math.max(250, Number(process.env.PMOS_FRESH_POLL_MS) || 10000),
1963
1972
  stableMs: Math.max(0, Number(process.env.PMOS_FRESH_STABLE_MS) || 3000),
1964
1973
  });
@@ -2621,7 +2630,62 @@ const coreDocsTick = () => {
2621
2630
  proSettings, inQuiet: (now, quiet) => proactiveLib.inQuietHours(now, quiet), isLead: (w) => isPlanNeedsTree(w) }, Number.isFinite(t) ? { now: t } : {});
2622
2631
  } catch (e) { warnWriteFail('core documents tick', e); }
2623
2632
  };
2624
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } autoSync.tick().catch(() => {}); dashboardScheduleTick(); driverTick(); scanTick(); reportFinderTick(); coreDocsTick(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2633
+ // data-refresh-settings: the refresh engine. It starts the skill sources that are due (calendar, Jira, work assigned to you, tracker,
2634
+ // the morning brief), writes the refresh log, and gates the job ticks below: a job that is off, or whose input is still running, waits.
2635
+ const REFRESH_BOOT = Date.now();
2636
+ refreshRun = refreshRunLib.create({
2637
+ stateDir: STATE,
2638
+ now: () => { const t = process.env.PMOS_REFRESH_NOW ? Date.parse(process.env.PMOS_REFRESH_NOW) : NaN; return Number.isFinite(t) ? t + (Date.now() - REFRESH_BOOT) : Date.now(); },
2639
+ homeWs: () => { const h = homeWorkspace(); return h ? h.id : null; },
2640
+ proactive: () => proSettings(),
2641
+ schedules: { read: readSchedules, write: writeSchedules },
2642
+ startSkill: (wsId, skill, name, origin) => startSkillSession(wsId, skill, name, origin),
2643
+ sessionAlive: (id) => { const x = sessions.get(String(id || '')); return !!x && x.state !== 'exited'; },
2644
+ freshRows: (wsIdArg) => { const w = getWs(wsIdArg || ''); return w ? freshness.sources({ id: w.id, root: w.root, repoStateDir: (() => { try { return stateDirFor(w); } catch { return null; } })() }) : []; },
2645
+ copyPaths: (id, wsIdArg) => {
2646
+ const w = getWs(wsIdArg || '');
2647
+ if (id === 'mywork') return [path.join(STATE, 'my-work', 'read.json')];
2648
+ if (id === 'jira' && w) { let d = null; try { d = stateDirFor(w); } catch { d = null; } return d ? [path.join(d, 'tracker', 'jira')] : []; }
2649
+ return [];
2650
+ },
2651
+ tell: ({ source, label, error, ws }) => {
2652
+ const w = getWs(ws || '') || homeWorkspace(); const nf = w ? needsFileFor(w) : null; if (!nf) return;
2653
+ let needs = []; try { needs = JSON.parse(fs.readFileSync(nf, 'utf8')); } catch { needs = []; }
2654
+ needs.push({ run_id: `refresh-${source}-${Date.now()}`, kind: 'refresh', initiative_id: label, question: `${label} could not refresh: ${String(error).slice(0, 300)} See Settings, Data refresh.` });
2655
+ fs.writeFileSync(nf, JSON.stringify(needs)); invalidateNeedsAgg(w.id);
2656
+ },
2657
+ setDayCheckEvery: (every) => {
2658
+ let raw = {}; try { raw = JSON.parse(fs.readFileSync(PRO_SETTINGS(), 'utf8')); } catch { raw = {}; }
2659
+ const cur = proSettings();
2660
+ const n = proactiveLib.normalizeSettings({ ...raw, dayCheck: { ...cur.dayCheck, every: Number(every) } });
2661
+ if (!n.ok) throw new Error(n.error);
2662
+ fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n');
2663
+ },
2664
+ setRepoWatch: (on) => { for (const w of workspaces.values()) freshness.setEnabled(w.root, on); },
2665
+ jobs: {
2666
+ daycheck: () => {
2667
+ const date = briefLib.localDate(Date.now()); const run = readDayRun(date);
2668
+ if (!run) return { ok: false, error: 'There is no plan for today yet. Press Prepare my day first; the day check reads that plan.' };
2669
+ const c = startDayCheck(run, 'pressed', Date.now());
2670
+ if (!c.ok) return { ok: false, error: c.error };
2671
+ writeDayRun(c.run); return { ok: true, started: 'session', session: c.session };
2672
+ },
2673
+ scan: (wsIdArg) => { const w = getWs(wsIdArg || '') || homeWorkspace(); if (!w) return { ok: false, error: 'no workspace' }; const r = scanner.startNow(w, {}, { by: 'pm' }); return r.ok ? { ok: true } : { ok: false, status: r.status, error: r.error }; },
2674
+ coredocs: () => { let m; try { m = require('./lib/routes-core-docs.js'); } catch { return { ok: false, error: 'core documents are not installed' }; }
2675
+ m.tick({ getWs, stateDirFor, workspaceList: () => [...workspaces.values()], runtimeDir: STATE, selfRoot: EMBEDDED_SELF ? SELF_ROOT : null, initiativeIndex,
2676
+ spawnSession, stopSession: (id) => { const x = sessions.get(String(id || '')); if (x && x.child) stopHeadless(x); }, invalidateNeedsAgg,
2677
+ proSettings, inQuiet: () => false, isLead: (w) => isPlanNeedsTree(w) }, { force: true });
2678
+ return { ok: true }; },
2679
+ driver: () => { const r = driver.tick({ sweepNow: true }); return r && r.off ? { ok: false, error: 'The initiative driver is switched off on this computer.' } : { ok: true }; },
2680
+ finder: () => reportFinder.runNow(),
2681
+ signals: () => { const t0 = Date.now(); const r = proactiveScan({ manual: true }); refreshRun.codeRun('signals', { trigger: 'pressed', took_ms: Date.now() - t0, got: r && r.ok ? `${r.appended || 0} new signals` : null, result: r && r.ok ? 'done' : 'failed', error: r && r.ok ? null : (r && r.error) || 'the scan failed' }); return { ok: true, started: 'done' }; },
2682
+ repo: () => { for (const w of workspaces.values()) { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root, { force: true }); } refreshRun.codeRun('repo', { trigger: 'pressed', got: 'board and initiative facts rebuilt' }); return { ok: true, started: 'rebuild' }; },
2683
+ sync: () => { autoSync.tick().then((out) => { const ran = (out || []).filter(Boolean); refreshRun.codeRun('sync', { trigger: 'pressed', result: ran.every((x) => x.ok) ? 'done' : 'failed', got: ran.length ? `${ran.length} repositories synced` : 'nothing to sync', error: ran.filter((x) => !x.ok).map((x) => x.message).join('; ') || null }); }).catch(() => {}); return { ok: true, started: 'sync' }; },
2684
+ },
2685
+ warn: (what, e) => warnWriteFail(what, e),
2686
+ });
2687
+ const gated = (id, fn) => () => { let g = { ok: true }; try { g = refreshRun.jobGate(id); } catch { g = { ok: true }; } if (g.ok) fn(); };
2688
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } try { refreshRun.tick(); } catch (e) { warnWriteFail('refresh tick', e); } gated('sync', () => autoSync.tick().catch(() => {}))(); dashboardScheduleTick(); gated('driver', driverTick)(); gated('scan', scanTick)(); gated('finder', reportFinderTick)(); gated('coredocs', coreDocsTick)(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2625
2689
 
2626
2690
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2627
2691
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -3408,6 +3472,7 @@ function attachPty(s, { seed = false } = {}) {
3408
3472
  : `exited (code ${exitCode})`;
3409
3473
  appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
3410
3474
  persist(s); pushSessions();
3475
+ noteSessionEnd(s);
3411
3476
  });
3412
3477
  return p;
3413
3478
  }
@@ -3583,6 +3648,7 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
3583
3648
  persist(s); pushSessions();
3584
3649
  // page-starts-agent: a caller that started this run from the page reads its result here (a proposal, never a write).
3585
3650
  if (typeof onDone === 'function') { try { onDone({ result: r, code, signal, timedOut, stopped: !!s.stoppedByPm, note: s.note }); } catch (e) { warnWriteFail('headless result', e); } }
3651
+ noteSessionEnd(s);
3586
3652
  });
3587
3653
  child.stdin.on('error', () => {});
3588
3654
  child.stdin.end(prompt);
@@ -3603,7 +3669,15 @@ function killHeadless(child, sig) {
3603
3669
  try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} }
3604
3670
  }
3605
3671
 
3606
- function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone, scratch, extraEnv, writePaths }) {
3672
+ // data-refresh-settings: every session passes the refresh engine on its way out, so a background refresh writes its start line
3673
+ // (lib/refresh-run.js started: only an origin it knows is logged). The end is noted where a session exits (noteSessionEnd).
3674
+ function spawnSession(opts) {
3675
+ const s = spawnSessionInner(opts);
3676
+ try { if (s && refreshRun) refreshRun.started(s); } catch (e) { warnWriteFail('refresh log (start)', e); }
3677
+ return s;
3678
+ }
3679
+ function noteSessionEnd(s) { try { if (refreshRun) refreshRun.ended(s); } catch (e) { warnWriteFail('refresh log (end)', e); } }
3680
+ function spawnSessionInner({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone, scratch, extraEnv, writePaths }) {
3607
3681
  const id = nextSessionId();
3608
3682
  /* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
3609
3683
  * PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
@@ -3790,6 +3864,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3790
3864
  : `exited (code ${exitCode})`;
3791
3865
  appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
3792
3866
  persist(s); pushSessions();
3867
+ noteSessionEnd(s);
3793
3868
  });
3794
3869
  feed(s);
3795
3870
  persist(s); pushSessions(); pushWorkspaces();
@@ -4345,7 +4420,7 @@ function dayTick() {
4345
4420
  if (a.ok) run = a.run;
4346
4421
  run = startDayTasks(run, now);
4347
4422
  const due = dayRunLib.checkDue(run, { settings: proSettings(), now, events: brief ? brief.calendar || [] : [] });
4348
- if (due.due) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
4423
+ if (due.due && refreshRun.jobGate('daycheck').ok) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
4349
4424
  run.checks.next = due.next;
4350
4425
  if (JSON.stringify(run) !== before) writeDayRun(run);
4351
4426
  }
@@ -4364,13 +4439,15 @@ function myWorkRead(wsId) {
4364
4439
  return { ok: true, session: s.id };
4365
4440
  }
4366
4441
  /** Refresh now (workspace-data-freshness R7): start skill `skillId` in a session of repository `wsId`, as myWorkRead does. */
4367
- function startSkillSession(wsId, skillId, name) {
4442
+ function startSkillSession(wsId, skillId, name, origin) {
4368
4443
  const w = getWs(wsId);
4369
4444
  if (!w) return { ok: false, status: 400, error: 'no workspace' };
4370
4445
  const task = taskById(skillId, w.root);
4371
4446
  if (!task || task.kind !== 'agent' || !task.prompt) return { ok: false, status: 409, error: `the ${skillId} skill is not installed for this repository` };
4372
4447
  const prompt = skillStore.retargetFiling(task.prompt, filingContract(w));
4373
- const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: 'source-refresh', initialInput: prompt + '\r',
4448
+ // data-refresh-settings: a refresh of a source runs as `refresh:<source>`, so the refresh log records it whoever pressed it.
4449
+ const src = refreshLib.SKILL_OF.get(skillId);
4450
+ const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: origin || (src ? 'refresh:' + src : 'source-refresh'), initialInput: prompt + '\r',
4374
4451
  modelAsk: { scheduleTier: null, skillTier: task.tier || null, skill: task.skill } });
4375
4452
  if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the refresh session could not start' };
4376
4453
  return { ok: true, session: s.id };
@@ -4593,7 +4670,12 @@ function proactiveTick() {
4593
4670
  const now = Date.now();
4594
4671
  if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
4595
4672
  proLastScan = now;
4596
- try { proactiveScan({ manual: false }); } catch (e) { warnWriteFail('proactive scan', e); }
4673
+ if (refreshRun.jobGate('signals').ok) {
4674
+ try {
4675
+ const r = proactiveScan({ manual: false });
4676
+ if (r && !r.skipped) refreshRun.codeRun('signals', { trigger: 'schedule', took_ms: Date.now() - now, got: `${r.appended || 0} new signals` });
4677
+ } catch (e) { warnWriteFail('proactive scan', e); refreshRun.codeRun('signals', { trigger: 'schedule', result: 'failed', error: String((e && e.message) || e) }); }
4678
+ }
4597
4679
  try { learnWeekly({ manual: false }); } catch (e) { warnWriteFail('proactive weekly review', e); }
4598
4680
  }
4599
4681
 
@@ -6662,6 +6744,8 @@ async function handleRequest(req, res) {
6662
6744
  const list = readSchedules(); const made = []; const kept = [];
6663
6745
  for (const def of set) {
6664
6746
  if (!taskById(def.skill, w.root)) { kept.push({ skill: def.skill, why: 'the skill is not installed' }); continue; }
6747
+ // data-refresh-settings (R3): a refresh skill is scheduled in Settings › Data refresh. The set turns its source on there.
6748
+ if (refreshRun.managedSkill(def.skill)) { const turned = refreshRun.ensureOn(refreshRun.managedSkill(def.skill), w.id); kept.push({ skill: def.skill, why: turned ? 'turned on in Settings, Data refresh' : 'already on in Settings, Data refresh', refresh: refreshRun.managedSkill(def.skill) }); continue; }
6665
6749
  if (list.some((x) => x.ws === w.id && x.skill === def.skill)) { kept.push({ skill: def.skill, why: 'already scheduled' }); continue; }
6666
6750
  const norm = scheduleLogic.normalizeSchedule({ ...def, task: def.skill });
6667
6751
  if (!norm.ok) { kept.push({ skill: def.skill, why: norm.error }); continue; }
@@ -6971,8 +7055,12 @@ async function handleRequest(req, res) {
6971
7055
  ? { count: strict.empty ? 0 : strict.data.items.length, error: null }
6972
7056
  : { count: null, error: strict.error } });
6973
7057
  }
7058
+ // ?kind=a,b keeps only those kinds (the console's Initiatives badge counts triage; the Briefing's agent rows read three kinds):
7059
+ // the full queue is large and most readers want a few rows of it (c3-desktop-light-load).
7060
+ const kinds = String(url.searchParams.get('kind') || '').split(',').map((k) => k.trim()).filter(Boolean);
7061
+ const queue = strict.ok ? strict.all : readNeeds(w);
6974
7062
  return json(res, {
6975
- needs: strict.ok ? strict.all : readNeeds(w),
7063
+ needs: kinds.length && Array.isArray(queue) ? queue.filter((n) => n && kinds.includes(n.kind)) : queue,
6976
7064
  state: strict.ok ? { ok: true } : { ok: false, error: strict.error },
6977
7065
  by_ws: needsByWorkspace(),
6978
7066
  });
@@ -8380,9 +8468,11 @@ async function handleRequest(req, res) {
8380
8468
  if (!taskById('morning-brief', w.root)) return json(res, { ok: false, error: 'the morning-brief skill is not installed for this repository' }, 409);
8381
8469
  const norm = scheduleLogic.normalizeSchedule({ ...SEED_SETS.brief[0], task: 'morning-brief' });
8382
8470
  if (!norm.ok) return json(res, { ok: false, error: norm.error }, 500);
8383
- sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null };
8384
- sched.nextRun = scheduleLogic.nextRun(sched);
8471
+ // data-refresh-settings (R3): the brief's schedule lives in Settings › Data refresh. The entry kept here only carries this run.
8472
+ sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null, enabled: false, endedReason: 'Moved to Settings › Data refresh' };
8473
+ sched.nextRun = null;
8385
8474
  const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
8475
+ try { refreshRun.ensureOn('brief', w.id); } catch (e) { warnWriteFail('refresh settings (brief)', e); }
8386
8476
  }
8387
8477
  const result = fireSchedule(sched);
8388
8478
  // A session that could not start is recorded as already ended, so the page says why instead of waiting on a read.
@@ -8630,6 +8720,8 @@ async function handleRequest(req, res) {
8630
8720
  // A brief for a day the PM pressed "Prepare my day": its agent priorities become tasks and start (G13), and a merge
8631
8721
  // closes the check that sent it (G22).
8632
8722
  try { dayAfterBrief(out.date, merged); } catch (e) { warnWriteFail('day run after the brief', e); }
8723
+ // data-refresh-settings: a full brief ends the morning brief's refresh run (its session stays open after it submits).
8724
+ if (body.merge !== true) { try { refreshRun.briefDone(`${(out.items || []).length} items`); } catch (e) { warnWriteFail('refresh log (brief)', e); } }
8633
8725
  return json(res, { ok: true, brief: out, ...(merged ? { merged } : {}), ...(remembered ? { remembered } : {}), ...(memoryError ? { memory_error: memoryError } : {}) });
8634
8726
  }
8635
8727
  if (req.method === 'GET') {
@@ -9078,7 +9170,9 @@ async function handleRequest(req, res) {
9078
9170
  // c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
9079
9171
  selfRoot: EMBEDDED_SELF ? SELF_ROOT : null,
9080
9172
  // horizon-scan-portfolio: Scan now, Stop and an approved research proposal start and end runs in the scanner; scanClock fixes its clock in the suites
9081
- scanner, scanClock })) return;
9173
+ scanner, scanClock,
9174
+ // data-refresh-settings: Settings › Data refresh (lib/routes-refresh.js)
9175
+ refreshRun })) return;
9082
9176
  res.writeHead(404); res.end('not found');
9083
9177
  }
9084
9178
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Initiative c3-desktop-light-load: the desktop console loads the Briefing's code and reads only, each tab's code on its first opening, the folded dock reads only the runs, the badges read counts, the pulse reads the gantt rows alone, no read twice on open.",
3
+ "modules": ["light-load.test.js"],
4
+ "panels": ["c3-light-load-test.js"],
5
+ "seconds": { "light-load.test.js": 15, "c3-light-load-test.js": 60 }
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "initiative data-refresh-settings: the refresh engine (lib/refresh.js) and its routes on a real server (lib/routes-refresh.js, lib/refresh-run.js).",
3
+ "modules": ["refresh.test.js", "refresh-routes.test.js"],
4
+ "seconds": { "refresh.test.js": 2, "refresh-routes.test.js": 25 }
5
+ }