create-pmos 0.193.0 → 0.194.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 (42) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/okf/core/concepts/frontmatter-format.md +3 -2
  4. package/payload/planning/evals/eval-calibration.md +1 -1
  5. package/payload/planning/okrs/health-budgets.md +1 -1
  6. package/payload/state/discoveries.md +1 -1
  7. package/workspace/c3-core-docs-test.js +152 -0
  8. package/workspace/guide/core-documents.md +49 -0
  9. package/workspace/guide/index.md +2 -1
  10. package/workspace/guide/initiative-boards.md +14 -0
  11. package/workspace/lib/core-docs-needs.js +86 -0
  12. package/workspace/lib/core-docs.js +675 -0
  13. package/workspace/lib/coredoc-cli.js +90 -0
  14. package/workspace/lib/doc-category.js +1 -1
  15. package/workspace/lib/driver-cli.js +16 -2
  16. package/workspace/lib/driver-decide.js +8 -1
  17. package/workspace/lib/driver-facts.js +8 -2
  18. package/workspace/lib/driver-run.js +6 -0
  19. package/workspace/lib/driver.js +32 -4
  20. package/workspace/lib/headless.js +13 -3
  21. package/workspace/lib/needs-providers.js +1 -0
  22. package/workspace/lib/pmos-verbs.js +3 -0
  23. package/workspace/lib/route-modules.js +1 -0
  24. package/workspace/lib/routes-core-docs.js +171 -0
  25. package/workspace/lib/routes-driver.js +1 -0
  26. package/workspace/lib/vendor/scripts/migrate-layout.js +1 -1
  27. package/workspace/lib/vendor/workspace/lib/doc-category.js +1 -1
  28. package/workspace/package-lock.json +2 -2
  29. package/workspace/package.json +1 -1
  30. package/workspace/public/pm/c3-brief-needs.js +3 -0
  31. package/workspace/public/pm/c3-core-docs.css +18 -0
  32. package/workspace/public/pm/c3-core-docs.js +234 -0
  33. package/workspace/public/pm/c3-demo-coredocs.js +211 -0
  34. package/workspace/public/pm/c3-demo.js +2 -1
  35. package/workspace/public/pm/c3-driver.js +6 -4
  36. package/workspace/public/pm/c3-ib-docs.js +24 -14
  37. package/workspace/public/pm/c3-ii-tabs.js +15 -8
  38. package/workspace/public/pm/c3-initiatives.js +29 -2
  39. package/workspace/server.js +15 -2
  40. package/workspace/skills/initiative-drive.skill +6 -2
  41. package/workspace/test-lists/c3-core-docs.json +9 -0
  42. package/workspace/test-lists/core-documents-backend.json +5 -0
@@ -1,6 +1,8 @@
1
1
  /* pm/c3-ib-docs.js — the Docs tab of a board (mockup v5, 34-initiatives.js tabDocs / docList / docRow), and the doc helpers the
2
2
  * initiative's own Docs tab (c3-ii-tabs.js) shares.
3
3
  * registerBoardTab('docs', (host, ctx) => ...)
4
+ * CORE First, the board's core documents (c3-core-docs.js, GET /api/core-docs level=board): the set, its state, Draft it, Review the draft.
5
+ * Then "Other documents", the list below.
4
6
  * DOCS The documents of the board's initiatives from the Documents data: GET /api/docs/library (files { path, title, category, date,
5
7
  * initiative }, categories). A file belongs to the board when its `initiative` is one of the board's initiatives (from
6
8
  * loadItems, c3-ini-work.js). Graph nodes (files with `node`) are left out. A search box filters by title, path, type and
@@ -10,8 +12,9 @@
10
12
  * Nothing is invented: a board with no linked documents says so. */
11
13
  import { el } from './dom.js';
12
14
  import { registerBoardTab } from './c3-initiatives.js';
13
- import { icon, pbtn, lk, chip, tag, st, note, sect, list, empty, nrow, plural, day, ago, parseDay, put } from './c3-ini-ui.js';
15
+ import { icon, pbtn, lk, chip, tag, st, note, sect, list, empty, nrow, plural, day, ago, parseDay, put, cols } from './c3-ini-ui.js';
14
16
  import { loadItems } from './c3-ini-work.js';
17
+ import { coreDocs } from './c3-core-docs.js';
15
18
 
16
19
  const STALE_DAYS = 30;
17
20
  /** The documents of the library that belong to the ids: { ok, error, docs: [{ path, name, cat, ty, date, ini, st }] }. */
@@ -36,8 +39,9 @@ async function openDoc(ctx, d, titles) {
36
39
  } catch { ctx.toast('The document sheet could not open. The file is ' + d.path + '.'); }
37
40
  }
38
41
 
39
- /** The search box and the list of docs. `grouped` false draws one flat list (an initiative's own page). */
40
- export function renderDocs(host, ctx, docs, order, titles, grouped) {
42
+ /** The search box and the list of docs. `grouped` false draws one flat list (an initiative's own page). `title` names the list ("Other documents"
43
+ * beside the core documents; "Documents" by default). */
44
+ export function renderDocs(host, ctx, docs, order, titles, grouped, title) {
41
45
  const results = el('div', { 'data-inb': 'docs-list' });
42
46
  const input = el('input', { type: 'text', placeholder: 'Search documents', 'aria-label': 'Search documents', 'data-inb': 'docs-search', autocomplete: 'off' });
43
47
  const count = el('span', { class: 'dim mono ini-meta', 'data-inb': 'docs-count', 'aria-live': 'polite' });
@@ -51,32 +55,38 @@ export function renderDocs(host, ctx, docs, order, titles, grouped) {
51
55
  const shown = docs.filter((d) => !q || (d.name + ' ' + d.path + ' ' + d.ty + ' ' + d.ini + ' ' + (titles.get(d.ini) || '')).toLowerCase().includes(q));
52
56
  count.textContent = q ? plural(shown.length, 'document') + ' match' + (shown.length === 1 ? 'es' : '') : plural(docs.length, 'document');
53
57
  if (!shown.length) { put(results, empty(q ? 'No document matches "' + input.value.trim() + '".' : 'No documents.', { 'data-inb': 'docs-none' })); return; }
54
- if (!grouped) { put(results, sect({ ic: 'doc', title: 'Documents', count: shown.length }, list(shown.map(row)))); return; }
58
+ if (!grouped) { put(results, sect({ ic: 'doc', title: title || 'Documents', count: shown.length }, list(shown.map(row)))); return; }
55
59
  put(results, ...order.filter((id) => shown.some((d) => d.ini === id)).map((id) => {
56
60
  const g = shown.filter((d) => d.ini === id);
57
61
  return sect({ ic: 'init', title: id + ' ' + (titles.get(id) || ''), count: g.length, attrs: { 'data-inb-docgrp': id }, right: lk({ onclick: () => ctx.openInitiative(id, 'docs'), 'aria-label': 'Open ' + id }, 'Open initiative') }, list(g.map(row)));
58
62
  }));
59
63
  };
60
64
  input.addEventListener('input', draw);
61
- put(host, el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, input), count), results);
65
+ put(host, grouped && title ? el('div', { class: 'sh2 cd-oh', 'data-inb': 'docs-title' }, el('h2', null, icon('doc', 'sm'), title, ' ', el('span', { class: 'bdg g' }, String(docs.length)))) : null,
66
+ el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, input), count), results);
62
67
  draw();
63
68
  }
64
69
 
65
70
  registerBoardTab('docs', async (host, ctx) => {
66
71
  let dead = false;
67
- put(host, note('Reading the documents'));
72
+ const other = el('div', { class: 'cd-host', 'data-cd': 'other' }, note('Reading the documents'));
73
+ const cd = coreDocs({ api: ctx.api, level: 'board', board: ctx.board.id, ws: ctx.ws, today: ctx.today, toast: ctx.toast, scopeLabel: ctx.board.name, boardRow: ctx.board,
74
+ nav: { tab: (t) => ctx.openBoard(ctx.board.id, t), tool: () => {}, reports: () => ctx.showTab('rep') } });
75
+ put(host, cols([cd.left, other], cd.right));
76
+ const stop = () => { dead = true; cd.dispose(); };
68
77
  const items = await loadItems(ctx);
69
- if (dead) return () => {};
70
- if (!items.ok) { put(host, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The initiatives of this board could not be read: ' + items.error + '.')); return () => {}; }
78
+ if (dead) return stop;
79
+ if (!items.ok) { put(other, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The initiatives of this board could not be read: ' + items.error + '.')); return stop; }
71
80
  const titles = new Map(items.initiatives.map((r) => [r.id, r.title]));
72
81
  const lib = await loadDocs(ctx, new Set(titles.keys()));
73
- if (dead) return () => {};
74
- if (!lib.ok) { put(host, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The documents could not be read: ' + lib.error + '.')); return () => {}; }
82
+ if (dead) return stop;
83
+ if (!lib.ok) { put(other, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The documents could not be read: ' + lib.error + '.')); return stop; }
84
+ cd.setIniDocs({ docs: lib.docs.length, initiatives: new Set(lib.docs.map((d) => d.ini)).size });
75
85
  if (!lib.docs.length) {
76
- put(host, empty(items.initiatives.length ? 'No documents are linked to the initiatives of ' + ctx.board.name + ' yet.' : ctx.board.name + ' has no initiatives, so there are no documents.', { 'data-inb': 'docs-empty' }));
77
- return () => { dead = true; };
86
+ put(other, empty(items.initiatives.length ? 'No other documents are linked to the initiatives of ' + ctx.board.name + ' yet.' : ctx.board.name + ' has no initiatives, so there are no other documents.', { 'data-inb': 'docs-empty' }));
87
+ return stop;
78
88
  }
79
89
  const order = [...new Set(lib.docs.map((d) => d.ini))];
80
- renderDocs(host, ctx, lib.docs, order, titles, true);
81
- return () => { dead = true; };
90
+ renderDocs(other, ctx, lib.docs, order, titles, true, 'Other documents');
91
+ return stop;
82
92
  });
@@ -7,7 +7,9 @@
7
7
  * first and scrolled to. Right: Details (portfolio, board, workflow with Edit, stage, owner, anchor, horizon, lane), Progress,
8
8
  * and links to the board's Backlog, Board and Timeline filtered to this initiative; then its releases.
9
9
  * Breakdown its epics and items, Jira and PMOS mixed (loadItems), and "Add a PMOS task" (POST /api/boards/tasks/add, epic optional).
10
- * Docs the documents linked to it (GET /api/docs/library), c3-ib-docs.js renderDocs.
10
+ * Docs Left: its core documents by category (Discover, Plan, Do, Learn; c3-core-docs.js, GET /api/core-docs level=initiative): the current one
11
+ * lists its rows, a finished or not-yet-reached one folds to one line. Right: what this level is for, the order rules, then "Other documents"
12
+ * (the documents linked to it, GET /api/docs/library, c3-ib-docs.js renderDocs).
11
13
  * Tickets its Jira items: the counts by status and the rows. Read-only: status and dates are Jira's. (Build and deliver only.)
12
14
  * Timeline epic > item on the HTML timeline, its own bar on top; PMOS bars drag to reschedule, Jira bars are locked.
13
15
  * History ctx.history: moves, workflow changes, assignments and the accept, with who, when, why and the order warnings.
@@ -18,6 +20,7 @@ import { icon, pbtn, lk, chip, tag, st, note, sect, card, list, empty, cols, hbs
18
20
  import { wfOf, catOf, kindOf, tplName, toolLabel, toolIcon, loadItems, dropItems, workRow, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
19
21
  import { recordCards, recordToolsFor, RECORD_META } from './c3-ini-records.js';
20
22
  import { loadDocs, renderDocs } from './c3-ib-docs.js';
23
+ import { coreDocs } from './c3-core-docs.js';
21
24
  import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
22
25
  import { driverStrip } from './c3-driver.js';
23
26
 
@@ -185,14 +188,18 @@ tab('breakdown', async (ctx, isDead, host) => {
185
188
  });
186
189
 
187
190
  /* ── Docs ── */
188
- tab('docs', async (ctx, isDead) => {
191
+ registerInitiativeTab('docs', async (host, ctx) => {
192
+ let dead = false;
193
+ const other = el('div', { class: 'cd-host', 'data-cd': 'other' });
194
+ const cd = coreDocs({ api: ctx.api, level: 'initiative', board: ctx.board.id, id: ctx.row.id, ws: ctx.ws, today: ctx.today, toast: ctx.toast, scopeLabel: ctx.row.id,
195
+ nav: { tab: (t) => ctx.openBoard(ctx.board.id, t), tool: (t) => ctx.go({ view: 'ini', board: ctx.board.id, id: ctx.row.id, tab: 'overview', tool: t }), reports: () => ctx.showTab('rep') } });
196
+ put(host, cols(cd.left, [cd.right, other]));
189
197
  const lib = await loadDocs(ctx, new Set([ctx.row.id]));
190
- if (isDead()) return null;
191
- if (!lib.ok) return errLine('The documents could not be read: ' + lib.error + '.');
192
- if (!lib.docs.length) return empty('No documents are linked to ' + ctx.row.id + ' yet.', { 'data-inb': 'docs-empty' });
193
- const box = el('div');
194
- renderDocs(box, ctx, lib.docs, [ctx.row.id], new Map([[ctx.row.id, ctx.row.title]]), false);
195
- return box;
198
+ if (dead) return () => {};
199
+ if (!lib.ok) put(other, errLine('The documents could not be read: ' + lib.error + '.'));
200
+ else if (!lib.docs.length) put(other, empty('No other documents are linked to ' + ctx.row.id + ' yet.', { 'data-inb': 'docs-empty' }));
201
+ else { const box = el('div'); renderDocs(box, ctx, lib.docs, [ctx.row.id], new Map([[ctx.row.id, ctx.row.title]]), false, 'Other documents'); put(other, box); }
202
+ return () => { dead = true; cd.dispose(); };
196
203
  });
197
204
 
198
205
  /* ── Jira items ── */
@@ -21,6 +21,7 @@
21
21
  *
22
22
  * URL STATE `init=` with history.replaceState, read back on load:
23
23
  * (none) Portfolio
24
+ * docs Portfolio, Documents view (the core documents of the Portfolio)
24
25
  * board:<board>:<tab>[:<filter>] a board tab; <filter> is a query string (cat=Do&view=risk), each part URI-encoded
25
26
  * ini:<board>:<id>:<tab>[:<tool>] an initiative tab; <tool> is the tool the Overview shows first
26
27
  *
@@ -51,6 +52,7 @@ import { RECORD_META, recordToolsFor } from './c3-ini-records.js';
51
52
  import { agentSetting, loadAgentMode, MODES as AGENT_MODES } from './c3-driver.js';
52
53
  import { scanPortfolioSection, scanPointer } from './c3-scan-setup.js';
53
54
  import { lifeBanner, pageMenu, ensureLifeCss } from './c3-ini-life.js';
55
+ import { coreDocs, segBadge } from './c3-core-docs.js';
54
56
 
55
57
  export { btn, geo };
56
58
  export { TEMPLATES, CATS };
@@ -88,12 +90,14 @@ export function parseLoc(s) {
88
90
  const p = String(s || '').split(':').map(dec);
89
91
  if (p[0] === 'board' && p[1]) return { view: 'board', board: p[1], tab: BOARD_IDS.includes(p[2]) ? p[2] : 'summary', filter: p[3] ? Object.fromEntries(new URLSearchParams(p[3])) : {} };
90
92
  if (p[0] === 'ini' && p[1] && p[2]) return { view: 'ini', board: p[1], id: p[2], tab: INI_IDS.includes(p[3]) ? p[3] : 'overview', tool: p[4] || '' };
93
+ if (p[0] === 'docs') return { view: 'portfolio', tab: 'docs' };
91
94
  return { view: 'portfolio' };
92
95
  }
93
96
  /** The inverse of parseLoc; '' for the Portfolio. */
94
97
  export function formatLoc(loc) {
95
98
  const e = encodeURIComponent;
96
- if (!loc || loc.view === 'portfolio' || !loc.board) return '';
99
+ if (loc && loc.view === 'portfolio') return loc.tab === 'docs' ? 'docs' : '';
100
+ if (!loc || !loc.board) return '';
97
101
  if (loc.view === 'board') {
98
102
  const f = Object.entries(loc.filter || {}).filter(([, v]) => v !== '' && v != null);
99
103
  return ['board', e(loc.board), e(loc.tab || 'summary')].join(':') + (f.length ? ':' + e(new URLSearchParams(f).toString()) : '');
@@ -154,6 +158,7 @@ export function mountInitiatives(pane, opts) {
154
158
  }
155
159
  function go(loc) {
156
160
  const l = { ...(loc || { view: 'portfolio' }) };
161
+ if (l.view === 'portfolio') l.tab = l.tab === 'docs' ? 'docs' : 'overview';
157
162
  if (l.view === 'board') { l.tab = BOARD_IDS.includes(l.tab) ? l.tab : 'summary'; l.filter = l.filter || {}; }
158
163
  if (l.view === 'ini') { l.tab = INI_IDS.includes(l.tab) ? l.tab : 'overview'; l.tool = l.tool || ''; }
159
164
  P.loc = l; writeUrl(); draw();
@@ -161,7 +166,7 @@ export function mountInitiatives(pane, opts) {
161
166
  const openBoard = (id, tab, filter) => go({ view: 'board', board: id, tab: tab || 'summary', filter: filter || {} });
162
167
  const refresh = () => { P.boards = null; P.ini = null; dropItems(); draw(); };
163
168
  // The Briefing's Horizon scan card opens a board's Triage or settings (c3-scan.js goBoard). A page that is not built yet reads the address instead.
164
- if (typeof window !== 'undefined') { if (window.__iniGo) window.removeEventListener('pmos:ini-go', window.__iniGo); window.__iniGo = (e) => { const d = (e && e.detail) || {}; if (d.board && root.isConnected) openBoard(d.board, d.tab || 'summary'); }; window.addEventListener('pmos:ini-go', window.__iniGo); }
169
+ if (typeof window !== 'undefined') { if (window.__iniGo) window.removeEventListener('pmos:ini-go', window.__iniGo); window.__iniGo = (e) => { const d = (e && e.detail) || {}; if (d.loc != null && root.isConnected) go(parseLoc(d.loc)); else if (d.board && root.isConnected) openBoard(d.board, d.tab || 'summary'); }; window.addEventListener('pmos:ini-go', window.__iniGo); }
165
170
  const redraw = () => draw();
166
171
 
167
172
  async function loadBoards() {
@@ -232,7 +237,26 @@ export function mountInitiatives(pane, opts) {
232
237
  }
233
238
 
234
239
  /* ════════ PORTFOLIO ════════ */
240
+ /** Overview | Documents, under the breadcrumb of the Portfolio. */
241
+ function pfTabs(cur, badge) {
242
+ return segTabs([['overview', 'Overview', 'cols'], ['docs', 'Documents', 'doc', badge]], cur, (k) => go(k === 'docs' ? { view: 'portfolio', tab: 'docs' } : { view: 'portfolio' }), 'Portfolio views', { 'data-ini': 'pf-tabs' });
243
+ }
244
+ const setBadge = (badge, r) => { const n = r && r.summary ? (Number(r.summary.draft) || 0) + (Number(r.summary.due) || 0) : 0; badge.textContent = String(n); badge.hidden = !n; };
245
+ /** The Portfolio's Documents view: its core set, the question it answers and the drafts waiting (c3-core-docs.js). */
246
+ function drawPortfolioDocs(boards, token) {
247
+ const asks = boards.reduce((n, b) => n + (Number(b.asks) || 0), 0);
248
+ const total = boards.reduce((n, b) => n + ((b.counts || {}).total || 0), 0);
249
+ const badge = segBadge(0);
250
+ const lhHost = el('div', { class: 'cd-host' });
251
+ const cd = coreDocs({ api: pageApi, level: 'portfolio', ws: opts.ws || (boards[0] && boards[0].ws) || '', today: P.today, toast, nav: { reports: () => (opts.showTab || (() => {}))('rep'), tab: () => {}, tool: () => {} },
252
+ onData: (r) => { setBadge(badge, r); put(lhHost, lh('doc', 'Portfolio documents', PORTFOLIO + ': the documents leadership reads to judge the bets. ' + (r.docs.filter((d) => d.st === 'draft').length ? plural(r.docs.filter((d) => d.st === 'draft').length, 'draft') + ' to review, ' : '') + r.docs.filter((d) => d.st === 'due').length + ' due.')); } });
253
+ P.dispose = () => cd.dispose();
254
+ put(root, pathCrumbs(null, null, el('div', { class: 'chips2' }, mono(plural(boards.length, 'board') + ' · ' + plural(total, 'initiative') + ' · ' + plural(asks, 'new ask')), pbtn('primary', 'New board', 'plus', { 'data-ini': 'new-board', onclick: () => openNewBoard() }))),
255
+ pfTabs('docs', badge),
256
+ el('div', { class: 'calbody', 'data-ini': 'pf-docs' }, lhHost, cols(cd.left, cd.right)));
257
+ }
235
258
  async function drawPortfolio(boards, token) {
259
+ if (P.loc.tab === 'docs') return drawPortfolioDocs(boards, token);
236
260
  const asks = boards.reduce((n, b) => n + (Number(b.asks) || 0), 0);
237
261
  const total = boards.reduce((n, b) => n + ((b.counts && b.counts.total) || 0), 0);
238
262
  const bad = boards.filter((b) => b.health === 'bad'); const warn = boards.filter((b) => b.health === 'warn');
@@ -251,9 +275,12 @@ export function mountInitiatives(pane, opts) {
251
275
  askBoards.length ? chgl(askBoards.map((b) => el('li', null, icon('inbox', 'sm'), lk({ onclick: () => openBoard(b.id, 'triage'), 'data-ini-asks': b.id }, b.name), el('span', { class: 'tag' }, plural(Number(b.asks), 'new ask')))))
252
276
  : note('No new asks on any board.')),
253
277
  ];
278
+ const badge = segBadge(0);
279
+ pageApi.get('/api/core-docs', { level: 'portfolio' }).then((r) => { if (token === P.seq) setBadge(badge, r); }, () => {});
254
280
  put(root,
255
281
  pathCrumbs(null, null, el('div', { class: 'chips2' }, mono(plural(boards.length, 'board') + ' · ' + plural(total, 'initiative') + ' · ' + plural(asks, 'new ask')),
256
282
  pbtn('primary', 'New board', 'plus', { 'data-ini': 'new-board', onclick: () => openNewBoard() }))),
283
+ pfTabs('overview', badge),
257
284
  el('div', { class: 'calbody', 'data-ini': 'portfolio' },
258
285
  lh('cols', sentenceOf(bad, warn), PORTFOLIO + ': boards hold initiatives, initiatives hold epics, epics hold stories, tasks and bugs. Stages roll up into Discover, Plan, Do and Learn so boards compare.'),
259
286
  // The Horizon scan runs for the whole portfolio (c3-scan-setup.js): its section is here, with Scan now always visible. Not in demo mode.
@@ -2598,7 +2598,19 @@ const reportFinderTick = () => {
2598
2598
  const t = process.env.PMOS_REPORT_FIND_NOW ? Date.parse(process.env.PMOS_REPORT_FIND_NOW) : NaN;
2599
2599
  try { reportFinder.tick(Number.isFinite(t) ? { now: t } : {}); } catch (e) { warnWriteFail('report finder tick', e); }
2600
2600
  };
2601
- 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(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2601
+ // core-documents: every 30 minutes inside the working hours, ONE headless run per due core document of the portfolio and of each board
2602
+ // (at most 3 at once, once per document per day, never one the PM writes). The run writes a draft with `pmos coredoc write`; nothing is sent.
2603
+ // PMOS_CORE_DOCS_NOW (a local date-time) fixes its clock for the suites.
2604
+ const coreDocsTick = () => {
2605
+ let m; try { m = require('./lib/routes-core-docs.js'); } catch { return; }
2606
+ const t = process.env.PMOS_CORE_DOCS_NOW ? Date.parse(process.env.PMOS_CORE_DOCS_NOW) : NaN;
2607
+ try {
2608
+ m.tick({ getWs, stateDirFor, workspaceList: () => [...workspaces.values()], runtimeDir: STATE, selfRoot: EMBEDDED_SELF ? SELF_ROOT : null, initiativeIndex,
2609
+ spawnSession, stopSession: (id) => { const x = sessions.get(String(id || '')); if (x && x.child) stopHeadless(x); }, invalidateNeedsAgg,
2610
+ proSettings, inQuiet: (now, quiet) => proactiveLib.inQuietHours(now, quiet), isLead: (w) => isPlanNeedsTree(w) }, Number.isFinite(t) ? { now: t } : {});
2611
+ } catch (e) { warnWriteFail('core documents tick', e); }
2612
+ };
2613
+ 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(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2602
2614
 
2603
2615
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2604
2616
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -3637,7 +3649,8 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
3637
3649
  Object.assign(penv, itemEnv);
3638
3650
  // horizon-scan: the scan runs set PMOS_SCAN_FILE, PMOS_SCAN_ORIGIN and PMOS_SCAN_RUN for `pmos scan file`; the report-setup and report-find runs
3639
3651
  // (report-workspace) set PMOS_REPORT_FILE and PMOS_REPORT_RUN for `pmos report propose`; nothing else is added.
3640
- if (extraEnv && typeof extraEnv === 'object') for (const k of ['PMOS_SCAN_FILE', 'PMOS_SCAN_ORIGIN', 'PMOS_SCAN_RUN', 'PMOS_REPORT_FILE', 'PMOS_REPORT_RUN']) if (typeof extraEnv[k] === 'string') penv[k] = extraEnv[k];
3652
+ // core-documents: a core-doc run sets PMOS_CORE_DOC_SCOPE and PMOS_CORE_DOC_KEY, the one entry `pmos coredoc write` may write.
3653
+ if (extraEnv && typeof extraEnv === 'object') for (const k of ['PMOS_SCAN_FILE', 'PMOS_SCAN_ORIGIN', 'PMOS_SCAN_RUN', 'PMOS_REPORT_FILE', 'PMOS_REPORT_RUN', 'PMOS_CORE_DOC_SCOPE', 'PMOS_CORE_DOC_KEY']) if (typeof extraEnv[k] === 'string') penv[k] = extraEnv[k];
3641
3654
  const base = { id, ws: w ? w.id : wsId(SELF_ROOT), root, name: (name && String(name).trim()) || agent, agent, origin: tag,
3642
3655
  profile: pres.id, profileRefusal: pres.ok ? null : pres.error };
3643
3656
  // Hand to agent: the breakdown item this session was started for, or null. Set only by /api/plan-items/handoff/go.
@@ -15,7 +15,7 @@ output:
15
15
  The prompt that starts this run names the target, the reason the run started, the stage category and what the PM already kept. Work on that target only.
16
16
 
17
17
  Rules that always hold:
18
- 1. THE ONLY WRITE is `pmos driver propose`. You have no Write and no Edit tool. Do not change a repository file, do not commit, do not push, and do not move the initiative to another stage. Keeping a proposal is the PM's act, not yours.
18
+ 1. THE ONLY WRITES are `pmos driver propose` and `pmos coredoc write` (a draft of a core document of this initiative, nothing else). You have no Write and no Edit tool. Do not change a repository file, do not commit, do not push, and do not move the initiative to another stage. Keeping a proposal is the PM's act, not yours.
19
19
  2. SEND NOTHING. Never send, reply, forward, post, comment, react, create, transition or label anything in mail, Slack, Jira, Confluence, GitHub, Drive, Figma or any other source. Read a connector only where it is already allowed. If a tool is refused, say so in your answer and go on without it.
20
20
  3. EVERY CLAIM CARRIES A POINTER. A pointer is a Jira key, a Slack link, a meeting note path, a requirement note path or a document path. Give it with `--source label=ref`. A claim you cannot point at is not a claim: leave it out. A proposal of the kind evidence, sizing, assumption or stakeholders is refused without a pointer.
21
21
  4. NOTHING INVENTED. Write only what you read. Say "not known" and name who could tell you. A run that finds nothing new is a correct run: file nothing.
@@ -26,7 +26,7 @@ Rules that always hold:
26
26
  Procedure:
27
27
  1. Orient. Run `pmos driver status initiative <id>`. Read the initiative record (`planning/initiatives/<id>.md`, or `pmos/state/initiatives/<id>.md`) and every file it names: PRD, brief, requirement notes. Run `pmos initiative list` to see the other open initiatives.
28
28
  2. Check first for a prior rejection and a duplicate. Search `planning/out-of-scope/` for the idea (Grep on its key words). Compare the title and the intent with the other open initiatives. If you find a match, file a `note` proposal first that names the file or the initiative, quotes what it says in one line, and says why this one is the same or different. Then stop unless the match is weak.
29
- 3. Find the stage category in the prompt. For Plan, Do and Learn, go to step 5. For Discover, do step 4.
29
+ 3. Find the stage category in the prompt. When the prompt names core documents of this stage that are due or stale, do step 8 first. For Plan, Do and Learn, go to step 5. For Discover, do step 4.
30
30
  4. Discover, in this order. Skip any step already kept.
31
31
  a. Problem statement. One paragraph: who has the problem, what it is, what it costs them. File `--what problem` with `--data "<the paragraph>"`. Give at least one source when you have one.
32
32
  b. Evidence. Every source that supports or contradicts the problem: a Jira key, a Slack thread, a meeting note, a requirement note, a document. For each one, a pointer and one line on what it says. File `--what evidence` with a JSON file: `{"items": [{"text": "<one line>", "ref": "<pointer>", "stance": "supports|contradicts"}]}` and one `--source label=ref` for each pointer. Look for contradicting evidence as hard as for supporting evidence.
@@ -43,6 +43,10 @@ Procedure:
43
43
  `pmos driver propose initiative <id> --what evidence --file /tmp/evidence.json --why "<one line: why the PM needs this>" --source "Jira DISC-12=https://…" --source "Meeting note=notes/2026-10-08.md"`
44
44
  Write the JSON or text file in the scratch folder of this session. If `pmos driver propose` refuses, read the reason, fix the proposal and file it once more. Do not work around a refusal.
45
45
  7. Finish with the output contract. Do not repeat the proposals in full.
46
+ 8. Core documents of this stage (due or stale, named in the prompt). For each one: read its sources (the record, the files it names, the Jira snapshot, the notes that name the initiative), write the document once with
47
+ `pmos coredoc write --scope initiative:<id> --key <key> --file - <<'PMOS_DOC'` … `PMOS_DOC` (the document on standard input; every fact with its source in brackets; "not known" where no source says), then file it:
48
+ `pmos driver propose initiative <id> --what doc --data '{"key":"<key>","slot":"<the slot it printed>"}' --why "<one line>"`.
49
+ The PM's Keep issues the draft. While a document of this stage is due, do not propose a stage move: `pmos driver propose` refuses it.
46
50
 
47
51
  Output contract — produce EXACTLY these sections in your final message:
48
52
  - Filed (the proposal ids and their kinds, or "nothing new")
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "c3-core-docs: the core documents of each level (Portfolio Documents, a board's Docs tab, an initiative's Docs tab), Draft it, Review the draft, Issue it, the Briefing row and 400 px, in demo mode (browser).",
3
+ "panels": [
4
+ "c3-core-docs-test.js"
5
+ ],
6
+ "seconds": {
7
+ "c3-core-docs-test.js": 40
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "_comment": "core-documents backend: the sets, state, store, CLI, run, tick and needs rows in process, and the core-docs routes on a real server with a stub claude.",
3
+ "modules": ["core-docs.test.js", "core-docs-server.test.js"],
4
+ "seconds": { "core-docs.test.js": 4, "core-docs-server.test.js": 12 }
5
+ }