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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/okf/core/concepts/frontmatter-format.md +3 -2
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-core-docs-test.js +152 -0
- package/workspace/guide/core-documents.md +49 -0
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/initiative-boards.md +14 -0
- package/workspace/lib/core-docs-needs.js +86 -0
- package/workspace/lib/core-docs.js +675 -0
- package/workspace/lib/coredoc-cli.js +90 -0
- package/workspace/lib/doc-category.js +1 -1
- package/workspace/lib/driver-cli.js +16 -2
- package/workspace/lib/driver-decide.js +8 -1
- package/workspace/lib/driver-facts.js +8 -2
- package/workspace/lib/driver-run.js +6 -0
- package/workspace/lib/driver.js +32 -4
- package/workspace/lib/headless.js +13 -3
- package/workspace/lib/needs-providers.js +1 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-core-docs.js +171 -0
- package/workspace/lib/routes-driver.js +1 -0
- package/workspace/lib/vendor/scripts/migrate-layout.js +1 -1
- package/workspace/lib/vendor/workspace/lib/doc-category.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-needs.js +3 -0
- package/workspace/public/pm/c3-core-docs.css +18 -0
- package/workspace/public/pm/c3-core-docs.js +234 -0
- package/workspace/public/pm/c3-demo-coredocs.js +211 -0
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-driver.js +6 -4
- package/workspace/public/pm/c3-ib-docs.js +24 -14
- package/workspace/public/pm/c3-ii-tabs.js +15 -8
- package/workspace/public/pm/c3-initiatives.js +29 -2
- package/workspace/server.js +15 -2
- package/workspace/skills/initiative-drive.skill +6 -2
- package/workspace/test-lists/c3-core-docs.json +9 -0
- 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
|
-
|
|
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: '
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
77
|
-
return
|
|
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(
|
|
81
|
-
return
|
|
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
|
|
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
|
-
|
|
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 (
|
|
191
|
-
if (!lib.ok)
|
|
192
|
-
if (!lib.docs.length)
|
|
193
|
-
const box = el('div');
|
|
194
|
-
|
|
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 (
|
|
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.
|
package/workspace/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|