create-pmos 0.193.0 → 0.195.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/notes-final.js +36 -9
- package/workspace/lib/notes-live-guard.js +30 -1
- package/workspace/lib/notes-whisper.js +11 -1
- 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
- package/workspace/test-lists/notes-silence-loops.json +5 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* pm/c3-demo-coredocs.js — DEMO ONLY (/c3.html?demo=1): the core documents of each level (initiative core-documents; the approved mockup
|
|
2
|
+
* 43-doc-levels.js). It answers GET /api/core-docs with the shape of the route (the three sets, a state for each entry, drafts), the three
|
|
3
|
+
* POSTs (draft, issue, drop) and the documents the rows open (GET /api/doc and the sheet's revisions, proposals and log for
|
|
4
|
+
* planning/core-docs/...). A write changes the in-memory state only. A draft that was asked for shows "Agent drafting" for a few seconds, then
|
|
5
|
+
* becomes a draft to review; an issue turns a draft into an up-to-date document.
|
|
6
|
+
*
|
|
7
|
+
* The mockup's boards and initiatives are mapped to the ones this demo holds (c3-demo-boards.js): Microsite (Build and deliver off) is
|
|
8
|
+
* "Office move"; IDPF-15 (in Build) is IDPF-12; IDPF-21 (research only, Discover) is IDPF-21; IDPF-09 (in Evaluate, with an outcome review
|
|
9
|
+
* draft) is IDPF-62, an Evaluate initiative of the same board. Dates come from the demo's today.
|
|
10
|
+
* Wiring: c3-demo.js calls installCoreDocs() after installBoards(), so this wrapper is outermost. Real mode never loads this file. */
|
|
11
|
+
import { TODAY } from './c3-demo-data.js';
|
|
12
|
+
|
|
13
|
+
const DAY = 86400000;
|
|
14
|
+
const ymd = (n) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(TODAY); const t = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]) + n * DAY); return t.toISOString().slice(0, 10); };
|
|
15
|
+
|
|
16
|
+
const SCOPE_Q = { portfolio: 'Are we betting on the right things?', board: 'Is this area moving, and what comes next?', initiative: 'What are we building, why, and did it work?' };
|
|
17
|
+
const READER = { portfolio: 'leadership', board: 'the board owner, the team and the engineering lead', initiative: 'you, the builders and the reviewers' };
|
|
18
|
+
const RHYTHM = { portfolio: 'each quarter, with a monthly pulse', board: 'every sprint or two weeks', initiative: 'at each stage' };
|
|
19
|
+
|
|
20
|
+
/* the core sets: key, title, ic, for, reader, cadence, by, kind (doc | live | report), cat, isNew, state, when, due, flag, home */
|
|
21
|
+
const PF = [
|
|
22
|
+
{ key: 'strategy', title: 'Product strategy', ic: 'target', for: 'Vision, where we play and what we will not do. Every board and key result traces to it.', reader: 'Leadership, every PM', cadence: 'Yearly, reviewed each quarter', by: 'you', st: 'ok', w: -9, flag: '2 key results do not trace to it', isNew: true },
|
|
23
|
+
{ key: 'okrs', title: 'OKRs and health budgets', ic: 'flag', for: 'The key results initiatives anchor to, and the budgets maintenance work runs on.', reader: 'Leadership, PMs', cadence: 'Quarterly', by: 'proposes', st: 'ok', w: -12 },
|
|
24
|
+
{ key: 'roadmap', title: 'Portfolio roadmap', ic: 'cal', for: 'Bets by quarter (Now, Next, Later) and the key result each serves. A sendable copy of the Roadmap.', reader: 'Leadership, stakeholders', cadence: 'Monthly', by: 'drafts', st: 'draft', w: 0, due: 4 },
|
|
25
|
+
{ key: 'invest', title: 'Investment mix', ic: 'cols', for: 'Where the effort goes: by board, by key result, and feature work against upkeep.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'draft', w: 0, due: 4, isNew: true },
|
|
26
|
+
{ key: 'scan', title: 'Horizon scan reports', ic: 'search', for: 'Benchmark, competitive, insight and productivity reports, each with proposals.', reader: 'PMs, leadership', cadence: 'Weekly scan', by: 'drafts', st: 'ok', w: -1, kind: 'report' },
|
|
27
|
+
{ key: 'status', title: 'Portfolio status report', ic: 'report', for: 'One page: progress on key results, what changed, what needs a decision.', reader: 'Leadership', cadence: 'Every two weeks', by: 'drafts', st: 'due', w: -10, due: 4, kind: 'report' },
|
|
28
|
+
{ key: 'decisions', title: 'Decision log', ic: 'scale', for: 'Decisions that cross boards, and why they were made.', reader: 'Everyone', cadence: 'As decided', by: 'files', st: 'ok', w: -2 },
|
|
29
|
+
{ key: 'kill', title: 'Kill conditions', ic: 'x', for: 'The result that would make us stop a bet.', reader: 'Leadership', cadence: 'Quarterly', by: 'watches', st: 'ok', w: -30, flag: '1 tripped' },
|
|
30
|
+
{ key: 'prisks', title: 'Cross-board risks', ic: 'alert', for: 'The few risks that span boards, rolled up from the risks each initiative records.', reader: 'Leadership, board owners', cadence: 'Monthly', by: 'drafts', st: 'ok', w: -3, isNew: true },
|
|
31
|
+
{ key: 'quarter', title: 'Quarterly review', ic: 'up', for: 'What landed, what moved the key results, and what we learned. Built from outcome reviews.', reader: 'Leadership', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
|
|
32
|
+
{ key: 'context', title: 'Market and user context', ic: 'notes', for: 'Jobs to be done, the market map and the research behind them.', reader: 'PMs', cadence: 'When research lands', by: 'keeps', st: 'ok', w: -20 },
|
|
33
|
+
];
|
|
34
|
+
const BD = [
|
|
35
|
+
{ key: 'charter', title: 'Board charter', ic: 'flag', for: 'Why this board exists: purpose, owner, the key results it serves, scope in and out, how work flows.', reader: 'Owner, team, new joiners', cadence: 'When the board changes', by: 'drafts', st: 'ok', w: -15, isNew: true },
|
|
36
|
+
{ key: 'backlog', title: 'Roadmap and backlog', ic: 'todo', for: 'Initiatives by stage, horizon and owner.', reader: 'Team, stakeholders', cadence: 'Live', by: 'live', st: 'ok', kind: 'live', tab: 'initiatives' },
|
|
37
|
+
{ key: 'intake', title: 'Intake log', ic: 'inbox', for: 'Asks received, accepted or declined, and why.', reader: 'Owner, requesters', cadence: 'Live', by: 'live', st: 'ok', kind: 'live', tab: 'triage' },
|
|
38
|
+
{ key: 'bstatus', title: 'Board status report', ic: 'report', for: 'Progress, slips, asks and what needs a decision, for the owner\'s manager.', reader: 'Stakeholders', cadence: 'Every two weeks', by: 'drafts', st: 'draft', w: 0, due: 2, kind: 'report' },
|
|
39
|
+
{ key: 'capacity', title: 'Capacity plan', ic: 'user', for: 'Who works on the board, their points a week, and whether the plan fits.', reader: 'Owner, eng lead', cadence: 'Each sprint', by: 'drafts', st: 'ok', w: -6, flag: 'Ana is at 112%', isNew: true },
|
|
40
|
+
{ key: 'metrics', title: 'Board metrics', ic: 'up', for: 'The few numbers this board moves, with one definition each.', reader: 'Team', cadence: 'Quarterly', by: 'keeps', st: 'ok', w: -40 },
|
|
41
|
+
{ key: 'brisks', title: 'Board risks', ic: 'alert', for: 'Risks on this board\'s initiatives and the waits on other boards. Written on each initiative, collected here.', reader: 'Owner', cadence: 'Every two weeks', by: 'drafts', st: 'ok', w: -1, isNew: true },
|
|
42
|
+
{ key: 'sprint', title: 'Sprint review', ic: 'clock', for: 'What the sprint committed and finished, what slipped and why.', reader: 'Team, stakeholders', cadence: 'Each sprint', by: 'drafts', st: 'draft', w: 0, build: true, isNew: true },
|
|
43
|
+
{ key: 'release', title: 'Release notes', ic: 'out', for: 'What each release holds, every line cited.', reader: 'Team, support, customers', cadence: 'Each release', by: 'drafts', st: 'due', due: 3, build: true, kind: 'live', tab: 'releases' },
|
|
44
|
+
{ key: 'retro', title: 'Board retro', ic: 'undo', for: 'Patterns across this board\'s initiatives, from their learnings and outcome reviews.', reader: 'Team', cadence: 'Quarterly', by: 'drafts', st: 'ok', w: -6, isNew: true },
|
|
45
|
+
];
|
|
46
|
+
const IN = [
|
|
47
|
+
{ key: 'record', title: 'Initiative record', ic: 'init', cat: 'Discover', for: 'Identity, anchor, lane and stage, with links to everything else. The gate checks it.', by: 'drafts' },
|
|
48
|
+
{ key: 'problem', title: 'Problem statement', ic: 'notes', cat: 'Discover', for: 'The problem, who has it and the evidence.', by: 'drafts', tool: 'problem' },
|
|
49
|
+
{ key: 'research', title: 'Research notes', ic: 'search', cat: 'Discover', for: 'Interviews, data pulls and anything else the problem rests on.', by: 'drafts' },
|
|
50
|
+
{ key: 'rice', title: 'Priority (RICE)', ic: 'scale', cat: 'Plan', for: 'Why now, compared with the rest of the board.', by: 'proposes', tool: 'rice' },
|
|
51
|
+
{ key: 'prd', title: 'PRD', ic: 'doc', cat: 'Plan', for: 'What and why, requirements and done criteria.', by: 'drafts' },
|
|
52
|
+
{ key: 'rubric', title: 'Eval rubric', ic: 'check', cat: 'Plan', for: 'How the result will be judged, agreed before the build.', by: 'drafts' },
|
|
53
|
+
{ key: 'design', title: 'Design and mockup', ic: 'eye', cat: 'Plan', for: 'The approved look and flows.', by: 'drafts' },
|
|
54
|
+
{ key: 'stories', title: 'User stories and ADRs', ic: 'tree', cat: 'Plan', for: 'Slices the builders pick up, and technical decisions with their reasons.', by: 'drafts' },
|
|
55
|
+
{ key: 'risks', title: 'Risks', ic: 'alert', cat: 'Do', for: 'What could sink it. Rolls up to the board and the portfolio.', by: 'drafts', tool: 'risks' },
|
|
56
|
+
{ key: 'launch', title: 'Launch checklist and comms', ic: 'out', cat: 'Do', for: 'What must be true to ship, and who hears about it.', by: 'drafts', tool: 'release' },
|
|
57
|
+
{ key: 'review', title: 'Review verdict and acceptance', ic: 'shield', cat: 'Do', for: 'The review gate result and your sign-off.', by: 'reviews' },
|
|
58
|
+
{ key: 'update', title: 'Status update', ic: 'report', cat: 'Do', for: 'A short update for stakeholders while it is active.', by: 'drafts', kind: 'report' },
|
|
59
|
+
{ key: 'explainer', title: 'Explainer', ic: 'info', cat: 'Learn', for: 'What changed, in plain words, for support and new readers.', by: 'drafts' },
|
|
60
|
+
{ key: 'outcome', title: 'Outcome review', ic: 'up', cat: 'Learn', for: 'Did it move the key result, and what we learned. Feeds the quarterly review.', by: 'drafts', isNew: true },
|
|
61
|
+
];
|
|
62
|
+
const CAT_ORDER = ['Discover', 'Plan', 'Do', 'Learn'];
|
|
63
|
+
/* states the mockup gives its demo initiatives; anything not listed comes from the stage */
|
|
64
|
+
const INI_STATE = {
|
|
65
|
+
'IDPF-12': { record: 'ok', problem: 'ok', research: 'ok', rice: 'ok', prd: 'ok', rubric: 'ok', design: 'ok', stories: 'ok', risks: 'ok', launch: 'due', review: 'later', update: 'draft' },
|
|
66
|
+
'IDPF-21': { record: 'ok', problem: 'draft', research: 'due' },
|
|
67
|
+
'IDPF-62': { record: 'ok', problem: 'ok', research: 'ok', rice: 'ok', prd: 'ok', rubric: 'ok', design: 'ok', stories: 'ok', risks: 'ok', launch: 'ok', review: 'ok', update: 'ok', explainer: 'ok', outcome: 'draft' },
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/* the documents the rows open: the mockup's, as markdown; any other entry gets a short generic body */
|
|
71
|
+
const BODY = {
|
|
72
|
+
invest: '## Headline\nIDPF takes 46% of the effort and Payments 28%. Upkeep is 22% against a 20% budget. Two key results get no effort this quarter.\n\n## By board\n- IDPF: 46%\n- Payments: 28%\n- Growth: 14%\n- Office move: 6%\n- Hiring Q4: 4%\n\n## Feature work against upkeep\n- Feature (anchored to a key result): 78%\n- Upkeep (health budgets): 22% of a 20% budget\n\n## Worth a look\n- KR 2.1 Activation has three initiatives on two boards. One shared definition of "activated" would save rework.\n- KR 3.3 and KR 5.1 have no initiative at all.\n',
|
|
73
|
+
sprint: '## Headline\nThe sprint committed 44 points and finished 36 (82%). Two stories slipped, both waiting on QA.\n\n## Finished\n- IDPF-1175 Setup step 2: first API key\n- IDPF-1210 Spike: token bucket per partner\n\n## Carried over\n- IDPF-1182 Workspace provisioning API, 5 points: waiting for QA\n- IDPF-1190 QA test plan for beta, 3 points: blocked, no QA this sprint\n\n## For the next sprint\n- Add QA capacity before committing the beta.\n',
|
|
74
|
+
charter: '## Purpose\nGive developers a platform they can set up and build on alone: identity, keys, limits and the first API call.\n\n## Serves\n- KR 2.1 Activation to 45%\n- KR 1.3 Partner uptime 99.9%\n\n## Scope\n- In: sign-in, workspaces, keys, limits, SDKs\n- Out: billing (Payments board)\n\n## How work flows\nBuild and deliver is on. Two-week sprints. New asks arrive in Triage.\n',
|
|
75
|
+
outcome: '## The bet\nIf developers can create and test a key on one page, more of them make a first API call within one day.\n\n## Did it move the key result?\nYes. First API call within one day rose from 52% to 61% in the five weeks after release (target for the initiative: 60%).\n\n## Kill conditions\nNone tripped.\n\n## What we learned\n- A test call on the same page mattered more than the new layout.\n- We should have measured rotation before; we have no baseline.\n\n## What next\n- Close the initiative as Hit.\n',
|
|
76
|
+
};
|
|
77
|
+
const PRETTY = { portfolio: 'portfolio', board: 'board', initiative: 'initiative' };
|
|
78
|
+
|
|
79
|
+
const slugOf = (scope) => scope.replace(/[^A-Za-z0-9]+/g, '-').toLowerCase();
|
|
80
|
+
const pathOf = (scope, key, slot) => 'planning/core-docs/' + slugOf(scope) + '/' + key + '/' + slot + '.md';
|
|
81
|
+
const parseScope = (scope) => { const m = /^(portfolio|board|initiative):?(.*)$/.exec(String(scope || '')); return m ? { level: m[1], id: m[2] } : null; };
|
|
82
|
+
|
|
83
|
+
/* ── state: what a press changed ── */
|
|
84
|
+
const OV = new Map(); // scope|key -> { st, when, draft, running, issued }
|
|
85
|
+
const ovKey = (scope, key) => scope + '|' + key;
|
|
86
|
+
|
|
87
|
+
function boardInfo(id) {
|
|
88
|
+
const S = typeof window !== 'undefined' ? window.__c3boards : null;
|
|
89
|
+
const b = S && S.boards ? S.boards.find((x) => x.id === id) : null;
|
|
90
|
+
return b ? { id, name: b.name, build: !!b.build, jira: b.jira || '', owner: b.owner } : { id, name: id, build: id === 'idpf', jira: '', owner: 'You' };
|
|
91
|
+
}
|
|
92
|
+
function iniInfo(id) {
|
|
93
|
+
const S = typeof window !== 'undefined' ? window.__c3boards : null;
|
|
94
|
+
const r = S && S.rows ? S.rows.find((x) => x.id === id) : null;
|
|
95
|
+
if (!r) return null;
|
|
96
|
+
const b = S.boards.find((x) => x.id === r.board);
|
|
97
|
+
const wf = r.workflow || (b && b.workflow) || [];
|
|
98
|
+
const stage = (wf.find((s) => s.name === r.stage) || {});
|
|
99
|
+
return { id, board: r.board, stage: r.stage, cat: stage.cat || 'Plan', wfTpl: r.template || (b && b.template) || 'product' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function entryOf(scope, e, base) {
|
|
103
|
+
const o = OV.get(ovKey(scope, e.key)) || {};
|
|
104
|
+
const stt = o.st || base.st;
|
|
105
|
+
const when = o.when !== undefined ? o.when : base.when;
|
|
106
|
+
const draft = stt === 'draft' ? (o.draft || { slot: ymd(0), path: pathOf(scope, e.key, ymd(0)) }) : null;
|
|
107
|
+
let open = null;
|
|
108
|
+
if (e.tab) open = { tab: e.tab }; else if (e.tool) open = { tool: e.tool }; else if (e.kind === 'report') open = { reports: true };
|
|
109
|
+
else if (stt === 'draft') open = { path: draft.path };
|
|
110
|
+
else if (stt === 'ok' || stt === 'due' || stt === 'stale') open = when ? { path: pathOf(scope, e.key, when) } : null;
|
|
111
|
+
return { key: e.key, title: e.title, ic: e.ic, for: e.for, reader: e.reader || null, cadence: e.cadence || null, by: e.by, kind: e.kind || 'doc', cat: e.cat || null, isNew: !!e.isNew,
|
|
112
|
+
st: stt, when: when || null, due: base.due != null ? (typeof base.due === 'number' ? ymd(base.due) : base.due) : null, flag: o.st ? null : base.flag || null, staleWhy: null, open, draft, running: !!o.running };
|
|
113
|
+
}
|
|
114
|
+
const summaryOf = (docs) => { const n = (s) => docs.filter((d) => d.st === s).length; return { draft: n('draft'), due: n('due'), missing: n('missing'), stale: n('stale') }; };
|
|
115
|
+
|
|
116
|
+
function setOf(scope) {
|
|
117
|
+
const sc = parseScope(scope);
|
|
118
|
+
if (!sc) return null;
|
|
119
|
+
const head = { ok: true, level: sc.level, scope, question: SCOPE_Q[sc.level], reader: READER[sc.level], rhythm: RHYTHM[sc.level], build: null, wfTpl: null, stage: null, cat: null };
|
|
120
|
+
let docs;
|
|
121
|
+
if (sc.level == 'portfolio') docs = PF.map((e) => entryOf(scope, e, { ...e, when: e.w != null ? ymd(e.w) : null }));
|
|
122
|
+
else if (sc.level === 'board') {
|
|
123
|
+
const b = boardInfo(sc.id); head.build = b.build;
|
|
124
|
+
docs = BD.filter((e) => !e.build || b.build).map((e) => {
|
|
125
|
+
const idpf = sc.id === 'idpf';
|
|
126
|
+
const st0 = e.key === 'charter' && sc.id === 'office' ? 'missing' : idpf ? e.st : (e.st === 'draft' ? 'ok' : e.st);
|
|
127
|
+
return entryOf(scope, e, { ...e, st: st0, when: st0 === 'missing' ? null : (e.w != null ? ymd(e.w) : null), flag: idpf ? e.flag : null });
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
const i = iniInfo(sc.id); if (!i) return { ok: false, error: 'No initiative ' + sc.id };
|
|
131
|
+
const b = boardInfo(i.board); head.build = b.build; head.stage = i.stage; head.cat = i.cat; head.wfTpl = i.wfTpl;
|
|
132
|
+
const cur = CAT_ORDER.indexOf(i.cat); const given = INI_STATE[sc.id] || {}; const research = i.wfTpl === 'research';
|
|
133
|
+
docs = IN.filter((e) => (b.build || !['rubric', 'stories', 'review'].includes(e.key) || i.board === 'idpf') && (!research || ['record', 'problem', 'research', 'rice', 'risks', 'update', 'outcome'].includes(e.key))).map((e) => {
|
|
134
|
+
const at = CAT_ORDER.indexOf(e.cat);
|
|
135
|
+
const st0 = given[e.key] || (at > cur ? 'later' : at === cur ? 'due' : 'ok');
|
|
136
|
+
return entryOf(scope, e, { ...e, st: st0, when: st0 === 'ok' ? ymd(-(at + 2) * 6) : st0 === 'draft' ? ymd(0) : null });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return { ...head, summary: summaryOf(docs), docs };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function bodyText(scope, key, slot) {
|
|
143
|
+
const sc = parseScope(scope) || {};
|
|
144
|
+
const all = [...PF, ...BD, ...IN]; const e = all.find((x) => x.key === key) || { title: key, for: '' };
|
|
145
|
+
const title = e.title + (sc.level === 'board' ? ' · ' + boardInfo(sc.id).name : sc.level === 'initiative' ? ' · ' + sc.id : '');
|
|
146
|
+
let body = BODY[key];
|
|
147
|
+
if (key === 'charter' && sc.id === 'office') body = '## Purpose\nMove the Jakarta office in 2027 with one contractor, and keep the team working through it.\n\n## Serves\n- Project milestone: lease signed\n\n## How work flows\nProject workflow. Build and deliver is off: the work is PMOS tasks, no Jira.\n';
|
|
148
|
+
if (!body) body = '## Summary\n' + (e.for || 'The document.') + '\n\n## Sources\n- Records PMOS reads for this ' + (PRETTY[sc.level] || 'level') + '\n';
|
|
149
|
+
const draftedBy = '---\ntitle: ' + title + '\nsource: core-docs:' + key + '\nslot: ' + slot + '\n---\n';
|
|
150
|
+
return draftedBy + '# ' + title + '\n\n' + body;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const json = (b, status) => new Response(JSON.stringify(b), { status: status || 200, headers: { 'content-type': 'application/json' } });
|
|
154
|
+
const KEYED = (p) => /^planning\/core-docs\/([^/]+)\/([^/]+)\/(\d{4}-\d{2}-\d{2})\.md$/.exec(String(p || ''));
|
|
155
|
+
|
|
156
|
+
function answer(method, path, q, body) {
|
|
157
|
+
if (path === '/api/core-docs' && method === 'GET') {
|
|
158
|
+
const level = q.get('level');
|
|
159
|
+
const scope = level == 'portfolio' ? 'portfolio' : level === 'board' ? 'board:' + (q.get('board') || '') : 'initiative:' + (q.get('id') || '');
|
|
160
|
+
const out = setOf(scope);
|
|
161
|
+
return out && out.ok ? json(out) : json(out || { ok: false, error: 'No such level.' }, out ? 404 : 400);
|
|
162
|
+
}
|
|
163
|
+
if (path.startsWith('/api/core-docs/') && method !== 'GET') {
|
|
164
|
+
const k = ovKey(body.scope, body.key);
|
|
165
|
+
if (path === '/api/core-docs/draft') {
|
|
166
|
+
const cur = OV.get(k) || {};
|
|
167
|
+
if (cur.running) return json({ ok: false, error: 'A draft is already running for this document.' }, 409);
|
|
168
|
+
const prev = (setOf(body.scope) || { docs: [] }).docs.find((d) => d.key === body.key) || {};
|
|
169
|
+
OV.set(k, { ...cur, running: true, st: prev.st === 'draft' ? 'draft' : cur.st || prev.st });
|
|
170
|
+
setTimeout(() => { OV.set(k, { st: 'draft', when: ymd(0), draft: { slot: ymd(0), path: pathOf(body.scope, body.key, ymd(0)) }, running: false }); }, 5000);
|
|
171
|
+
return json({ ok: true, run: 'demo-core-doc-' + body.key });
|
|
172
|
+
}
|
|
173
|
+
if (path === '/api/core-docs/issue') { OV.set(k, { st: 'ok', when: ymd(0), running: false }); return json({ ok: true }); }
|
|
174
|
+
if (path === '/api/core-docs/drop') { OV.set(k, { st: 'missing', when: null, running: false }); return json({ ok: true }); }
|
|
175
|
+
}
|
|
176
|
+
/* the documents the rows open */
|
|
177
|
+
const m = KEYED(q.get('path') || (body && body.path));
|
|
178
|
+
if (m) {
|
|
179
|
+
const slug = m[1]; const key = m[2]; const slot = m[3];
|
|
180
|
+
const scope = slug == 'portfolio' ? 'portfolio' : slug.startsWith('board-') ? 'board:' + slug.slice(6) : 'initiative:' + (/^initiative-(.+)$/.exec(slug) || [])[1];
|
|
181
|
+
const sc = scope.startsWith('initiative:') ? 'initiative:' + String(scope.slice(11)).toUpperCase() : scope;
|
|
182
|
+
if (path === '/api/doc') return new Response(bodyText(sc, key, slot), { status: 200, headers: { 'content-type': 'text/markdown; charset=utf-8' } });
|
|
183
|
+
const p = q.get('path') || body.path;
|
|
184
|
+
if (path === '/api/doc-sheet/revisions') return json({ ok: true, path: p, revisions: [{ rev: 1, by: 'claude-code/session', at: slot + 'T08:10:00.000Z', why: 'Drafted from the sources', before_hash: '', after_hash: '' }] });
|
|
185
|
+
if (path === '/api/doc-sheet/proposals') return json({ ok: true, path: p, proposals: [] });
|
|
186
|
+
if (path === '/api/doc-sheet/log') return json({ ok: true, path: p, revisions: [{ rev: 1, by: 'claude-code/session', at: slot + 'T08:10:00.000Z', why: 'Drafted from the sources', via: 'sheet', text_by: 'agent', before_hash: '', after_hash: '' }], events: [] });
|
|
187
|
+
if (path === '/api/doc-health/check') return json({ ok: true, path: p, health: { passed: 3, total: 3, checks: [{ id: 'fresh', pass: true, reason: 'drafted today' }, { id: 'complete', pass: true, reason: 'Required sections present' }, { id: 'traced', pass: true, reason: 'Sources listed' }] } });
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let installed = false;
|
|
193
|
+
export function installCoreDocs() {
|
|
194
|
+
if (installed || typeof window === 'undefined') return;
|
|
195
|
+
installed = true;
|
|
196
|
+
const prev = window.fetch;
|
|
197
|
+
window.fetch = (input, init) => {
|
|
198
|
+
try {
|
|
199
|
+
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
200
|
+
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
201
|
+
const u = new URL(raw, location.origin);
|
|
202
|
+
if (u.origin === location.origin && (u.pathname.startsWith('/api/core-docs') || u.pathname.startsWith('/api/doc'))) {
|
|
203
|
+
let b = {};
|
|
204
|
+
if (method !== 'GET') { try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; } }
|
|
205
|
+
const out = answer(method, u.pathname, u.searchParams, b);
|
|
206
|
+
if (out) return Promise.resolve(out);
|
|
207
|
+
}
|
|
208
|
+
} catch { /* an unreadable request goes on as it is */ }
|
|
209
|
+
return prev(input, init);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -13,6 +13,7 @@ import * as D from './c3-demo-data.js';
|
|
|
13
13
|
import { installInit } from './c3-demo-init.js';
|
|
14
14
|
import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
|
|
15
15
|
import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
|
|
16
|
+
import { installCoreDocs } from './c3-demo-coredocs.js'; // Core documents of each level: GET /api/core-docs and its POSTs (installed outermost, after installBoards)
|
|
16
17
|
import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
|
|
17
18
|
import './c3-demo-cal3.js'; // Calendar Quarter and Year: answers GET /api/c3-strategy (chained under this file's fetch wrapper)
|
|
18
19
|
import './c3-demo-rep-ix.js'; // Reports fixtures: sets window.__c3repix (report-families-view.js reads it in demo only)
|
|
@@ -123,4 +124,4 @@ export function demoPill() {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export const DEMO_WS = D.WS_ID;
|
|
126
|
-
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); installBoards(); const repReady = import('./c3-demo-rep2.js').then((m) => { m.installRep2(); return import('./c3-demo-rs.js'); }).then((m) => m.installRs()); window.__c3demo = true; window.__c3demoReady = Promise.all([repReady, import('./c3-demo-brief-ix.js')]).then(([, m]) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
127
|
+
export function installDemo() { freezeClock(); install(); installRep3(); installInit(); installBoards(); installCoreDocs(); const repReady = import('./c3-demo-rep2.js').then((m) => { m.installRep2(); return import('./c3-demo-rs.js'); }).then((m) => m.installRs()); window.__c3demo = true; window.__c3demoReady = Promise.all([repReady, import('./c3-demo-brief-ix.js')]).then(([, m]) => m.installBriefIx()).then(() => import('./c3-demo-misc2.js')).then((m) => m.installMisc2()).then(() => import('./c3-demo-brief-page.js')).then((m) => m.installBriefPage()); } // Briefing case files, people and a simulated agent (c3-shell.js waits for __c3demoReady)
|
|
@@ -276,20 +276,22 @@ export function agentSetting(state, { onPick } = {}) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/* ── Needs you: the agent's rows (Briefing) ── */
|
|
279
|
-
/** The
|
|
279
|
+
/** The agent's rows of GET /api/needs: "Agent has 3 drafts on <title>", "<title> is waiting on you", and "2 portfolio drafts to review" (core
|
|
280
|
+
* documents, kind core-doc-drafts: it opens that scope's Docs view). [] when the read fails. */
|
|
280
281
|
export async function loadDriverNeeds() {
|
|
281
282
|
const r = await pmGet('/api/needs');
|
|
282
|
-
return r && Array.isArray(r.needs) ? r.needs.filter((n) => n && (n.kind === 'driver-drafts' || n.kind === 'driver-blocked')) : [];
|
|
283
|
+
return r && Array.isArray(r.needs) ? r.needs.filter((n) => n && (n.kind === 'driver-drafts' || n.kind === 'driver-blocked' || n.kind === 'core-doc-drafts')) : [];
|
|
283
284
|
}
|
|
284
285
|
/** One li.nrow for a driver row (the mockup's two rows). `go(row)` opens the initiative or the Work page. */
|
|
285
286
|
export function needsRow(n, go) {
|
|
286
287
|
ensureDriverCss();
|
|
287
288
|
const blocked = n.kind === 'driver-blocked';
|
|
289
|
+
const docs = n.kind === 'core-doc-drafts';
|
|
288
290
|
const work = !n.initiative_id;
|
|
289
|
-
const chips = blocked ? [el('span', { class: 'tag warn' }, 'waiting on you'), chip(icon(work ? 'check' : 'init', 'sm'), work ? 'Work' : n.initiative_id)]
|
|
291
|
+
const chips = docs ? [tag('agent'), chip(icon('doc', 'sm'), 'Core documents'), el('span', { class: 'dim' }, 'Review each draft, then issue it')] : blocked ? [el('span', { class: 'tag warn' }, 'waiting on you'), chip(icon(work ? 'check' : 'init', 'sm'), work ? 'Work' : n.initiative_id)]
|
|
290
292
|
: [tag('agent'), chip(icon(work ? 'check' : 'init', 'sm'), work ? 'Work' : n.initiative_id)];
|
|
291
293
|
chips[0].className = blocked ? 'tag warn' : 'tag acc';
|
|
292
|
-
chips.push(el('span', { class: 'dim' }, blocked ? 'The agent will not go on until you decide' : 'Keep, send back or drop each one'));
|
|
294
|
+
if (!docs) chips.push(el('span', { class: 'dim' }, blocked ? 'The agent will not go on until you decide' : 'Keep, send back or drop each one'));
|
|
293
295
|
const li = nrow({ ic: blocked ? 'next' : 'spark', hot: true, title: n.question, onOpen: () => go(n), openLabel: n.question, chips,
|
|
294
296
|
attrs: { 'data-needs-row': n.run_id, 'data-kind': n.kind, 'data-drv-need': n.kind, 'data-ref-row': n.initiative_id || n.run_id } });
|
|
295
297
|
return li;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "notes-silence-loops: Whisper looping on quiet audio. The loop guard (live and in the final pass) and the final pass dropping segments with no speech under them (notes-loops.test.js).",
|
|
3
|
+
"modules": ["notes-loops.test.js"],
|
|
4
|
+
"seconds": { "notes-loops.test.js": 3 }
|
|
5
|
+
}
|