create-pmos 0.132.0 → 0.134.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/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/bin/pmos.js +18 -2
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/notes.md +19 -1
- package/workspace/guide/welcome.md +38 -0
- package/workspace/lib/import-overlay.js +32 -0
- package/workspace/lib/notes-audio.js +28 -9
- package/workspace/lib/notes-capture.js +128 -0
- package/workspace/lib/notes-live-guard.js +117 -0
- package/workspace/lib/notes-whisper.js +132 -16
- package/workspace/lib/notes.js +4 -2
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-notes.js +60 -8
- package/workspace/lib/routes-onboarding.js +215 -0
- package/workspace/lib/vendor/scripts/anchor-resolve.js +351 -0
- package/workspace/lib/vendor/scripts/delivery-index.js +356 -0
- package/workspace/lib/vendor/scripts/intake.js +78 -0
- package/workspace/lib/vendor/scripts/lane-defs.js +226 -0
- package/workspace/lib/vendor/scripts/migrate-layout.js +775 -0
- package/workspace/lib/vendor/scripts/nine-stages.js +81 -0
- package/workspace/lib/vendor/scripts/onboarding/apply.js +110 -0
- package/workspace/lib/vendor/scripts/onboarding/brief.js +154 -0
- package/workspace/lib/vendor/scripts/onboarding/first-briefing.js +41 -0
- package/workspace/lib/vendor/scripts/onboarding/first-look.js +84 -0
- package/workspace/lib/vendor/scripts/onboarding/infer-brief.js +70 -0
- package/workspace/lib/vendor/scripts/onboarding/proposals.js +98 -0
- package/workspace/lib/vendor/scripts/onboarding/scan-worker.js +27 -0
- package/workspace/lib/vendor/scripts/onboarding/scan.js +859 -0
- package/workspace/lib/vendor/scripts/onboarding/structure.js +217 -0
- package/workspace/lib/vendor/scripts/plan-items-stage.js +271 -0
- package/workspace/lib/vendor/scripts/read-jsonl.js +53 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +43 -0
- package/workspace/lib/vendor/scripts/status-facts.js +214 -0
- package/workspace/lib/vendor/scripts/trust/import-guard.js +36 -0
- package/workspace/lib/vendor/scripts/trust/ledger.js +53 -0
- package/workspace/lib/vendor/scripts/workflow-state.js +1888 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +91 -0
- package/workspace/lib/vendor/workspace/lib/board-cache.js +156 -0
- package/workspace/lib/vendor/workspace/lib/console-state.js +295 -0
- package/workspace/lib/vendor/workspace/lib/doc-category.js +227 -0
- package/workspace/lib/vendor/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/vendor/workspace/lib/git-info.js +306 -0
- package/workspace/lib/vendor/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +419 -0
- package/workspace/lib/vendor/workspace/lib/review-gate.js +39 -0
- package/workspace/lib/vendor/workspace/lib/sink.js +321 -0
- package/workspace/lib/vendor/workspace/lib/state-root.js +587 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/calendar-views.js +25 -12
- package/workspace/public/pm/components.css +280 -0
- package/workspace/public/pm/console3.css +19 -0
- package/workspace/public/pm/shell-recording.js +122 -12
- package/workspace/public/pm/view-notes.js +7 -2
- package/workspace/public/pm/welcome.js +283 -0
- package/workspace/public/welcome.html +20 -0
- package/workspace/server.js +2 -2
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/meeting-notes.json +5 -3
- package/workspace/test-lists/onboarding-firstrun.json +15 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/* pm/welcome.js — the first-run page (/welcome). One primary button per screen.
|
|
2
|
+
*
|
|
3
|
+
* Welcome → First look → Your portfolio → Questions (only when the scan left any) → First Briefing → More sources
|
|
4
|
+
*
|
|
5
|
+
* Routes (lib/routes-onboarding.js): GET /api/onboarding/status|structure|briefing, POST /api/onboarding/scan|apply|undo.
|
|
6
|
+
* More sources uses POST /api/sources/refresh (lib/routes-sources.js): it starts the source's read-only skill as a session in
|
|
7
|
+
* the workspace with the PM's own connection. Nothing is read until the PM presses Bring in; the server calls nothing outside.
|
|
8
|
+
* Presses carry the launch nonce through pm/http.js. The page writes nothing itself; text taken from the scanned folder is
|
|
9
|
+
* untrusted, so every value goes through esc() and no quoted text from a file is shown.
|
|
10
|
+
* data-testid attributes are the browser suite's handles.
|
|
11
|
+
*/
|
|
12
|
+
import { pmGet, pmPost } from './http.js';
|
|
13
|
+
|
|
14
|
+
const root = document.getElementById('wl-root');
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
const S = {
|
|
17
|
+
ws: params.get('ws') || '', folder: null, added: [], step: 'welcome', busy: '', error: '', note: '',
|
|
18
|
+
look: null, structure: null, checked: {}, titles: {}, boardOf: {}, open: {}, answers: {}, skipped: {}, importId: '', briefing: [], sources: {},
|
|
19
|
+
};
|
|
20
|
+
const STEPS = [['welcome', 'Welcome'], ['look', 'First look'], ['portfolio', 'Your portfolio'], ['questions', 'Questions'], ['briefing', 'First Briefing'], ['sources', 'More sources']];
|
|
21
|
+
/* The sources a first run can bring in. `source` is a lib/freshness.js SOURCES id with a read-only skill. Inbox and Slack
|
|
22
|
+
* have no import skill yet, so the page says so instead of offering a button that cannot work. */
|
|
23
|
+
const MORE_SOURCES = [
|
|
24
|
+
{ source: 'calendar', name: 'Calendar', adds: 'Your meetings today and for the next 14 days, so the Briefing can get you ready for them.' },
|
|
25
|
+
{ source: 'my-work', name: 'Jira', adds: 'The Jira issues assigned to you, so My work starts with them.' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
29
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
30
|
+
const ref = (e) => (e ? (e.line ? `${e.file}:${e.line}` : e.file) : '');
|
|
31
|
+
|
|
32
|
+
/* ── model ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
33
|
+
const rowsOf = () => (S.structure ? S.structure.boards.flatMap((b) => b.initiatives.map((r) => ({ ...r, board: b.key }))) : []);
|
|
34
|
+
const boardOfRow = (r) => S.boardOf[r.proposal_id] || r.board;
|
|
35
|
+
function counts() {
|
|
36
|
+
const on = rowsOf().filter((r) => S.checked[r.proposal_id]);
|
|
37
|
+
return { n: on.length, m: new Set(on.map(boardOfRow)).size };
|
|
38
|
+
}
|
|
39
|
+
function adopt(structure) {
|
|
40
|
+
S.structure = structure; S.checked = {}; S.titles = {}; S.boardOf = {}; S.open = {}; S.answers = {}; S.skipped = {};
|
|
41
|
+
for (const r of rowsOf()) { S.checked[r.proposal_id] = !!r.checked; S.titles[r.proposal_id] = r.title; }
|
|
42
|
+
}
|
|
43
|
+
function revisions() {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const r of rowsOf()) {
|
|
46
|
+
const rev = {};
|
|
47
|
+
if (S.titles[r.proposal_id] !== r.title && String(S.titles[r.proposal_id]).trim()) rev.title = String(S.titles[r.proposal_id]).trim();
|
|
48
|
+
if (S.boardOf[r.proposal_id] && S.boardOf[r.proposal_id] !== r.board) rev.board = S.boardOf[r.proposal_id];
|
|
49
|
+
if (Object.keys(rev).length) out[r.proposal_id] = rev;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── actions ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
55
|
+
const fail = (r, what) => { S.error = (r && r.error) || `${what} did not work.`; };
|
|
56
|
+
async function run(label, fn) {
|
|
57
|
+
S.busy = label; S.error = ''; render();
|
|
58
|
+
try { await fn(); } catch (e) { S.error = String((e && e.message) || e); }
|
|
59
|
+
S.busy = ''; render();
|
|
60
|
+
}
|
|
61
|
+
async function loadStructure() {
|
|
62
|
+
const r = await pmGet(`/api/onboarding/structure?ws=${encodeURIComponent(S.ws)}`);
|
|
63
|
+
if (!r || !r.ok) { fail(r, 'Reading the portfolio'); return false; }
|
|
64
|
+
adopt(r); return true;
|
|
65
|
+
}
|
|
66
|
+
async function loadBriefing() {
|
|
67
|
+
const r = await pmGet(`/api/onboarding/briefing?ws=${encodeURIComponent(S.ws)}`);
|
|
68
|
+
if (!r || !r.ok) { fail(r, 'Reading the Briefing'); return false; }
|
|
69
|
+
S.briefing = r.items || []; return true;
|
|
70
|
+
}
|
|
71
|
+
const addFolder = () => run('add', async () => {
|
|
72
|
+
const p = await pmPost('/api/pick-folder', {});
|
|
73
|
+
if (!p || !p.ok) { if (p && p.cancelled) return; S.note = (p && p.unsupported) ? 'This system has no folder chooser. Start pmos in the folder instead.' : ''; if (!S.note) fail(p, 'Choosing a folder'); return; }
|
|
74
|
+
const w = await pmPost('/api/workspaces', { root: p.root, name: p.root.split(/[\\/]/).filter(Boolean).pop(), focus: false });
|
|
75
|
+
if (!w || !w.workspace) { fail(w, 'Adding the folder'); return; }
|
|
76
|
+
if (!S.added.some((a) => a.id === w.workspace.id) && w.workspace.id !== S.ws) S.added.push({ id: w.workspace.id, name: w.workspace.name, root: w.workspace.root });
|
|
77
|
+
});
|
|
78
|
+
const scan = () => run('scan', async () => {
|
|
79
|
+
const r = await pmPost('/api/onboarding/scan', { ws: S.ws, extra_roots: S.added.map((a) => a.root) });
|
|
80
|
+
if (!r || !r.ok) { fail(r, 'The scan'); return; }
|
|
81
|
+
S.look = r.first_look; S.step = 'look';
|
|
82
|
+
});
|
|
83
|
+
const toPortfolio = () => run('structure', async () => { if (await loadStructure()) S.step = 'portfolio'; });
|
|
84
|
+
async function applyNow() {
|
|
85
|
+
const include = rowsOf().filter((r) => S.checked[r.proposal_id]).map((r) => r.proposal_id);
|
|
86
|
+
const answers = {};
|
|
87
|
+
// an untouched question sends its default (the PM saw it and pressed Import); a skipped one sends nothing
|
|
88
|
+
for (const q of (S.structure.questions || [])) {
|
|
89
|
+
if (S.skipped[q.id]) continue;
|
|
90
|
+
const v = S.answers[q.id] != null ? S.answers[q.id] : q.default;
|
|
91
|
+
if (q.id === 'template') answers.template = Object.assign(answers.template || {}, { [q.board]: v });
|
|
92
|
+
else answers[q.id] = v;
|
|
93
|
+
}
|
|
94
|
+
const r = await pmPost('/api/onboarding/apply', { ws: S.ws, include, revisions: revisions(), answers });
|
|
95
|
+
if (!r || !r.ok) { fail(r, 'The import'); return; }
|
|
96
|
+
S.importId = r.import_id;
|
|
97
|
+
if (await loadBriefing()) S.step = 'briefing';
|
|
98
|
+
}
|
|
99
|
+
const importNow = () => {
|
|
100
|
+
if (!counts().n) return;
|
|
101
|
+
if ((S.structure.questions || []).length && S.step !== 'questions') { S.step = 'questions'; render(); return; }
|
|
102
|
+
run('apply', applyNow);
|
|
103
|
+
};
|
|
104
|
+
// "Not now": Welcome stops opening on its own for this folder (`pmos welcome` still opens it), then the console opens
|
|
105
|
+
const notNow = () => run('dismiss', async () => {
|
|
106
|
+
const r = await pmPost('/api/onboarding/dismiss', { ws: S.ws });
|
|
107
|
+
if (!r || !r.ok) { fail(r, 'Not now'); return; }
|
|
108
|
+
location.href = '/c3.html?ws=' + encodeURIComponent(S.ws);
|
|
109
|
+
});
|
|
110
|
+
const undo = () => run('undo', async () => {
|
|
111
|
+
const r = await pmPost('/api/onboarding/undo', { ws: S.ws, import_id: S.importId });
|
|
112
|
+
if (!r || !r.ok) { fail(r, 'Undo'); return; }
|
|
113
|
+
S.importId = '';
|
|
114
|
+
if (await loadStructure()) S.step = 'portfolio';
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/* ── views ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
118
|
+
const spin = (txt) => `<span class="wl-spin" data-testid="loading" aria-hidden="true"></span>${esc(txt)}`;
|
|
119
|
+
|
|
120
|
+
function viewWelcome() {
|
|
121
|
+
const f = S.folder || { name: '', root: '' };
|
|
122
|
+
const added = S.added.length ? `<ul class="wl-added" data-testid="added-folders">${S.added.map((a) => `<li data-testid="added-folder">${esc(a.name)} <span class="wl-path">${esc(a.root)}</span></li>`).join('')}</ul>` : '';
|
|
123
|
+
return `<h1 class="wl-h1">Welcome to PMOS</h1>
|
|
124
|
+
<p class="wl-p wl-muted">PMOS reads your folder and shows you what it found: your initiatives, your documents and what needs you first.</p>
|
|
125
|
+
<section class="wl-card"><h2 class="wl-h2" data-testid="folder-name">${esc(f.name || 'This folder')}</h2><div class="wl-path" data-testid="folder-path">${esc(f.root)}</div>${added}
|
|
126
|
+
<div class="wl-actions"><button type="button" class="wl-btn" data-testid="add-folder" ${S.busy ? 'disabled' : ''}>${S.busy === 'add' ? spin('Waiting for the chooser') : 'Add another folder'}</button></div>
|
|
127
|
+
${S.note ? `<p class="wl-p wl-muted" data-testid="note">${esc(S.note)}</p>` : ''}</section>
|
|
128
|
+
<p class="wl-promise" data-testid="promise">Reads only. Writes nothing in your folder. Nothing leaves this computer.</p>
|
|
129
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="scan-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'scan' ? spin('Scanning your folder') : 'Scan'}</button>
|
|
130
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="not-now" ${S.busy ? 'disabled' : ''}>Not now</button></div>
|
|
131
|
+
${S.busy === 'scan' ? '<p class="wl-p wl-muted" data-testid="scan-wait">A large folder can take a minute or two. PMOS keeps working while it reads.</p>' : ''}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function viewLook() {
|
|
135
|
+
const L = S.look || {};
|
|
136
|
+
const items = (L.findings || []).slice(0, 10).map((f) => `<li data-testid="finding"><strong>${esc(f.title)}</strong>${f.why ? `<div class="wl-muted">${esc(f.why)}</div>` : ''}<div class="wl-ref" data-testid="finding-ref">${esc(ref((f.evidence || [])[0]))}</div></li>`).join('');
|
|
137
|
+
return `<h1 class="wl-h1">First look</h1>
|
|
138
|
+
<p class="wl-p" data-testid="first-look-sentence">${esc(L.sentence)}</p>
|
|
139
|
+
<section class="wl-card"><h2 class="wl-h2">What matters most</h2>${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-findings">Nothing in this folder needs you urgently.</p>'}</section>
|
|
140
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="continue-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'structure' ? spin('Building your portfolio') : 'Continue'}</button></div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rowView(r) {
|
|
144
|
+
const id = r.proposal_id; const docs = r.docs || []; const open = !!S.open[id];
|
|
145
|
+
const opts = S.structure.boards.map((b) => `<option value="${esc(b.key)}" ${b.key === boardOfRow(r) ? 'selected' : ''}>${esc(b.name)}</option>`).join('');
|
|
146
|
+
return `<div class="wl-row" data-testid="row" data-id="${esc(id)}">
|
|
147
|
+
<input type="checkbox" class="wl-check" data-testid="row-check" data-id="${esc(id)}" aria-label="Import ${esc(S.titles[id])}" ${S.checked[id] ? 'checked' : ''} />
|
|
148
|
+
<div><input type="text" class="wl-input" data-testid="row-title" data-id="${esc(id)}" value="${esc(S.titles[id])}" aria-label="Title" />
|
|
149
|
+
<div class="wl-meta"><span class="wl-chip" data-testid="row-lane">${esc(r.lane)}</span><span class="wl-chip" data-testid="row-anchor">${esc(r.anchor || 'no anchor')}</span><span class="wl-chip" data-testid="row-stage">${esc(r.stage)}</span>
|
|
150
|
+
<button type="button" class="wl-btn wl-small" data-testid="row-docs-toggle" data-id="${esc(id)}" aria-expanded="${open}">${plural(docs.length, 'document', 'documents')}</button>
|
|
151
|
+
<select class="wl-input" data-testid="row-board" data-id="${esc(id)}" aria-label="Board">${opts}</select></div>
|
|
152
|
+
${open ? `<ul class="wl-docs" data-testid="row-docs">${docs.map((d) => `<li>${esc(d.title && d.title !== d.file ? d.title + ' · ' : '')}${esc(d.file)}</li>`).join('')}</ul>` : ''}</div></div>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function viewPortfolio() {
|
|
156
|
+
const all = rowsOf(); const c = counts();
|
|
157
|
+
const boards = S.structure.boards.map((b) => {
|
|
158
|
+
const mine = all.filter((r) => boardOfRow(r) === b.key);
|
|
159
|
+
return `<section class="wl-card wl-board" data-testid="board" data-board="${esc(b.key)}"><header><h2 class="wl-h2" data-testid="board-name">${esc(b.name)}</h2>
|
|
160
|
+
<span class="wl-muted" data-testid="unsorted-count">${plural((b.unsorted || []).length, 'document', 'documents')} unsorted</span></header>
|
|
161
|
+
${mine.length ? mine.map(rowView).join('') : '<p class="wl-p wl-muted">No initiatives here.</p>'}</section>`;
|
|
162
|
+
}).join('');
|
|
163
|
+
return `<h1 class="wl-h1">Your portfolio</h1>
|
|
164
|
+
<p class="wl-p wl-muted">This is how PMOS would set up your work. Untick what you do not want, rename it, or move it to another board.</p>
|
|
165
|
+
${boards}
|
|
166
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="import-button" ${S.busy || !c.n ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function viewQuestions() {
|
|
170
|
+
const qs = (S.structure.questions || []).map((q) => {
|
|
171
|
+
const skipped = !!S.skipped[q.id];
|
|
172
|
+
const cur = S.answers[q.id] != null ? S.answers[q.id] : q.default;
|
|
173
|
+
const input = (q.options && q.options.length)
|
|
174
|
+
? (q.options.length > 4 && !q.free
|
|
175
|
+
? `<select class="wl-input" data-testid="question-select" data-id="${esc(q.id)}" ${skipped ? 'disabled' : ''}>${q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<option value="${esc(v)}" ${String(cur) === String(v) ? 'selected' : ''}>${esc(t)}</option>`; }).join('')}</select>`
|
|
176
|
+
: q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<label class="wl-label"><input type="radio" name="q-${esc(q.id)}" data-testid="question-option" data-id="${esc(q.id)}" value="${esc(v)}" ${String(cur) === String(v) ? 'checked' : ''} ${skipped ? 'disabled' : ''} /> ${esc(t)}</label>`; }).join('')
|
|
177
|
+
+ (q.free ? `<label class="wl-label wl-free">${esc(q.free)} <input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" placeholder="KR-A1 or health:upkeep" value="${q.options.some((o) => String(typeof o === 'object' ? o.value : o) === String(cur)) ? '' : esc(cur)}" ${skipped ? 'disabled' : ''} /></label>` : ''))
|
|
178
|
+
: `<input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" value="${esc(cur)}" ${skipped ? 'disabled' : ''} />`;
|
|
179
|
+
return `<section class="wl-card wl-q" data-testid="question" data-id="${esc(q.id)}"><p class="wl-p"><strong>${esc(q.text)}</strong></p>${input}
|
|
180
|
+
${q.options && q.options.length ? '' : `<p class="wl-p wl-muted">Default: ${esc(q.default)}</p>`}<button type="button" class="wl-btn wl-small" data-testid="question-skip" data-id="${esc(q.id)}" aria-pressed="${skipped}">${skipped ? 'Skipped. Use the default' : 'Skip'}</button></section>`;
|
|
181
|
+
}).join('');
|
|
182
|
+
const c = counts();
|
|
183
|
+
return `<h1 class="wl-h1">A few questions</h1><p class="wl-p wl-muted">The scan could not decide these. Each has a default, and you can skip any of them.</p>${qs}
|
|
184
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="questions-continue" ${S.busy ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function viewBriefing() {
|
|
188
|
+
const items = S.briefing.map((b) => `<li data-testid="briefing-item"><strong>${esc(b.title)}</strong>${b.why ? `<div class="wl-muted">${esc(b.why)}</div>` : ''}${b.initiative ? `<div class="wl-muted" data-testid="briefing-initiative">${esc(b.initiative.title)} · board ${esc(b.initiative.board)}</div>` : ''}<div class="wl-ref">${esc(ref((b.evidence || [])[0]))}</div></li>`).join('');
|
|
189
|
+
return `<h1 class="wl-h1">Your first Briefing</h1>
|
|
190
|
+
<p class="wl-p wl-muted">${S.briefing.length ? 'These need you first.' : 'Nothing needs you right now.'}</p>
|
|
191
|
+
<section class="wl-card">${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-briefing">Your portfolio is in. Open PMOS to see it.</p>'}</section>
|
|
192
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="sources-continue">Continue</button>
|
|
193
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="undo-link" ${S.busy ? 'disabled' : ''}>${S.busy === 'undo' ? spin('Undoing') : 'Undo this import'}</button></div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sourceRow(x) {
|
|
197
|
+
const st = S.sources[x.source] || {};
|
|
198
|
+
const busy = S.busy === 'source:' + x.source;
|
|
199
|
+
const state = st.started ? '<span class="wl-muted" data-testid="source-started">Started in an agent session. Follow it in PMOS.</span>'
|
|
200
|
+
: st.skipped ? '<span class="wl-muted" data-testid="source-skipped">Skipped. You can bring it in later from Settings.</span>'
|
|
201
|
+
: `<button type="button" class="wl-btn" data-testid="source-bring" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>${busy ? spin('Starting') : 'Bring in'}</button>
|
|
202
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="source-skip" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>Skip</button>`;
|
|
203
|
+
return `<li class="wl-source" data-testid="source-row" data-id="${esc(x.source)}"><div class="wl-source-text"><strong>${esc(x.name)}</strong><div class="wl-muted">${esc(x.adds)}</div>${st.error ? `<div class="wl-error" role="alert">${esc(st.error)}</div>` : ''}</div><div class="wl-source-actions">${state}</div></li>`;
|
|
204
|
+
}
|
|
205
|
+
function viewSources() {
|
|
206
|
+
return `<h1 class="wl-h1">Bring in more sources</h1>
|
|
207
|
+
<p class="wl-p wl-muted">Your folder is in. These sources are optional. Each one runs in an agent session with your own connection, reads only, and writes nothing to the tool. Nothing is read until you press Bring in.</p>
|
|
208
|
+
<section class="wl-card"><ul class="wl-sources">${MORE_SOURCES.map(sourceRow).join('')}</ul>
|
|
209
|
+
<p class="wl-p wl-muted" data-testid="sources-not-yet">Inbox and Slack have no import yet.</p></section>
|
|
210
|
+
<div class="wl-actions"><a class="wl-btn wl-primary" data-testid="open-pmos" href="/c3.html?ws=${encodeURIComponent(S.ws)}">Open PMOS</a></div>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const VIEWS = { welcome: viewWelcome, look: viewLook, portfolio: viewPortfolio, questions: viewQuestions, briefing: viewBriefing, sources: viewSources };
|
|
214
|
+
|
|
215
|
+
function render() {
|
|
216
|
+
const keepScroll = window.scrollY;
|
|
217
|
+
const stepsBar = `<ol class="wl-steps" data-testid="steps">${STEPS.filter(([k]) => k !== 'questions' || S.step === 'questions' || (S.structure && (S.structure.questions || []).length))
|
|
218
|
+
.map(([k, t]) => `<li ${k === S.step ? 'aria-current="step"' : ''} data-step="${k}">${t}</li>`).join('')}</ol>`;
|
|
219
|
+
const a = document.activeElement;
|
|
220
|
+
const was = a && root.contains(a) ? [a.getAttribute('data-testid'), a.getAttribute('data-id')] : null;
|
|
221
|
+
root.setAttribute('data-step', S.step);
|
|
222
|
+
root.innerHTML = stepsBar + (S.error ? `<div class="wl-error" role="alert" data-testid="error">${esc(S.error)}</div>` : '') + VIEWS[S.step]();
|
|
223
|
+
window.scrollTo(0, keepScroll);
|
|
224
|
+
if (was && was[0]) { // keep the keyboard where it was after a re-render
|
|
225
|
+
const again = [...root.querySelectorAll('[data-testid="' + was[0] + '"]')].find((el) => (el.getAttribute('data-id') || null) === (was[1] || null));
|
|
226
|
+
if (again && !again.disabled) again.focus();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ── events (one delegate; the markup is rebuilt on each render) ─────────────────────────────────────────────────── */
|
|
231
|
+
const idOf = (el) => el.getAttribute('data-id');
|
|
232
|
+
const bringIn = (source) => run('source:' + source, async () => {
|
|
233
|
+
const r = await pmPost('/api/sources/refresh', { ws: S.ws, source });
|
|
234
|
+
S.sources[source] = r && r.ok ? { started: true } : { error: (r && r.error) || 'Starting the import did not work.' };
|
|
235
|
+
});
|
|
236
|
+
document.addEventListener('click', (e) => {
|
|
237
|
+
if (!root.contains(e.target)) return;
|
|
238
|
+
const t = e.target.closest('[data-testid]'); if (!t) return;
|
|
239
|
+
const k = t.getAttribute('data-testid');
|
|
240
|
+
if (k === 'add-folder') addFolder();
|
|
241
|
+
else if (k === 'scan-button') scan();
|
|
242
|
+
else if (k === 'continue-button') toPortfolio();
|
|
243
|
+
else if (k === 'import-button' || k === 'questions-continue') { if (k === 'questions-continue') run('apply', applyNow); else importNow(); }
|
|
244
|
+
else if (k === 'undo-link') undo();
|
|
245
|
+
else if (k === 'sources-continue') { S.step = 'sources'; S.error = ''; render(); }
|
|
246
|
+
else if (k === 'source-bring') bringIn(idOf(t));
|
|
247
|
+
else if (k === 'source-skip') { S.sources[idOf(t)] = { skipped: true }; render(); }
|
|
248
|
+
else if (k === 'not-now') notNow();
|
|
249
|
+
else if (k === 'row-docs-toggle') { S.open[idOf(t)] = !S.open[idOf(t)]; render(); }
|
|
250
|
+
else if (k === 'question-skip') { const id = idOf(t); S.skipped[id] = !S.skipped[id]; render(); }
|
|
251
|
+
});
|
|
252
|
+
root.addEventListener('change', (e) => {
|
|
253
|
+
const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
|
|
254
|
+
if (k === 'row-check') { S.checked[idOf(t)] = t.checked; render(); }
|
|
255
|
+
else if (k === 'row-board') { S.boardOf[idOf(t)] = t.value; render(); }
|
|
256
|
+
else if (k === 'question-option' || k === 'question-select') S.answers[idOf(t)] = t.value;
|
|
257
|
+
});
|
|
258
|
+
root.addEventListener('input', (e) => {
|
|
259
|
+
const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
|
|
260
|
+
if (k === 'row-title') S.titles[idOf(t)] = t.value;
|
|
261
|
+
else if (k === 'question-input') {
|
|
262
|
+
const id = idOf(t); const q = ((S.structure && S.structure.questions) || []).find((x) => x.id === id);
|
|
263
|
+
S.answers[id] = t.value.trim() ? t.value : (q ? q.default : t.value); // an emptied free answer goes back to the default
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/* ── start ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
268
|
+
async function start() {
|
|
269
|
+
render();
|
|
270
|
+
const list = await pmGet('/api/workspaces');
|
|
271
|
+
const all = (list && list.workspaces) || [];
|
|
272
|
+
const mine = all.find((w) => w.id === S.ws) || (!S.ws ? all[0] : null);
|
|
273
|
+
if (!mine) { S.error = 'This folder is not open in PMOS. Start pmos in the folder, then reload.'; render(); return; }
|
|
274
|
+
S.ws = mine.id; S.folder = { name: mine.name, root: mine.root };
|
|
275
|
+
const st = await pmGet(`/api/onboarding/status?ws=${encodeURIComponent(S.ws)}`);
|
|
276
|
+
if (st && st.ok && st.state === 'imported') {
|
|
277
|
+
const live = (st.imports || []).filter((i) => !i.retracted).pop();
|
|
278
|
+
S.importId = live ? live.import_id : '';
|
|
279
|
+
if (S.importId && await loadBriefing() && await loadStructure()) S.step = 'briefing';
|
|
280
|
+
}
|
|
281
|
+
render();
|
|
282
|
+
}
|
|
283
|
+
start();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="wl-page" data-pm-page-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="pmos-nonce" content="" />
|
|
7
|
+
<title>Welcome to PMOS</title>
|
|
8
|
+
<!-- The first-run page. The server fills the nonce meta the way it does for /c3.html, so a press (Scan, Import, Undo)
|
|
9
|
+
carries it. Tokens and fonts are the console's, so light and dark both resolve. The page does not load the console. -->
|
|
10
|
+
<link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
11
|
+
<link rel="stylesheet" href="/pm/tokens.css" />
|
|
12
|
+
<link rel="stylesheet" href="/pm/components.css" />
|
|
13
|
+
|
|
14
|
+
</head>
|
|
15
|
+
<body class="wl-body">
|
|
16
|
+
<main id="wl-root" class="wl-wrap" data-testid="welcome-root"></main>
|
|
17
|
+
<noscript><p class="wl-wrap">This page needs JavaScript.</p></noscript>
|
|
18
|
+
<script type="module" src="/pm/welcome.js"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
package/workspace/server.js
CHANGED
|
@@ -5547,12 +5547,12 @@ async function handleRequest(req, res) {
|
|
|
5547
5547
|
const built = BUILD_RE.exec(url.pathname);
|
|
5548
5548
|
const p = built ? built[1] : url.pathname;
|
|
5549
5549
|
|
|
5550
|
-
if (p === '/' || p === '/index.html' || p === '/c3.html') {
|
|
5550
|
+
if (p === '/' || p === '/index.html' || p === '/c3.html' || p === '/welcome' || p === '/welcome.html') {
|
|
5551
5551
|
// pm-os-absorb Run 2 (R25, X11): the per-launch nonce goes into the base meta line, nowhere else. no-store, so a
|
|
5552
5552
|
// restarted server never answers a press from a page that holds the previous launch's value. The page code is
|
|
5553
5553
|
// referenced under this launch's build prefix.
|
|
5554
5554
|
const n = pmGuard.launchNonce();
|
|
5555
|
-
return fs.readFile(path.join(PUB, p === '/c3.html' ? 'c3.html' : 'index.html'), 'utf8', (err, html) => {
|
|
5555
|
+
return fs.readFile(path.join(PUB, p === '/c3.html' ? 'c3.html' : (p === '/welcome' || p === '/welcome.html') ? 'welcome.html' : 'index.html'), 'utf8', (err, html) => {
|
|
5556
5556
|
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
5557
5557
|
let out = withBuild(html);
|
|
5558
5558
|
if (n) out = out.replace('<meta name="' + pmGuard.NONCE_META + '" content="" />', '<meta name="' + pmGuard.NONCE_META + '" content="' + n + '" />');
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
"pm-live-notes-test.js",
|
|
120
120
|
"pm-notes-menu-test.js",
|
|
121
121
|
"pm-record-control-test.js",
|
|
122
|
+
"pm-c3-record-test.js",
|
|
122
123
|
"pm-r30-test.js",
|
|
123
124
|
"pm-breakdown-test.js",
|
|
124
125
|
"pm-breakdown-phone-test.js",
|
|
@@ -234,6 +235,7 @@
|
|
|
234
235
|
"pm-live-notes-test.js": 24,
|
|
235
236
|
"pm-notes-menu-test.js": 10,
|
|
236
237
|
"pm-record-control-test.js": 12,
|
|
238
|
+
"pm-c3-record-test.js": 12,
|
|
237
239
|
"pm-r30-test.js": 4,
|
|
238
240
|
"ui-test.js": 30,
|
|
239
241
|
"plan-items.test.js": 1,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js).",
|
|
2
|
+
"_comment": "meeting-notes Runs 1 to 10: the Notes engine and routes (notes.test.js), local Whisper, line labels and repository checks (notes-listen.test.js), the final pass after Stop and the Wispr Flow fetch (notes-final.test.js), Qwen3-ASR kept loaded (notes-qwen.test.js), and the Notes view in a browser (pm-notes-test.js), the echo canceller for the replay (notes-echo.test.js, notes-audio-quality Run 2) and the optional cleaned copy of the replay (notes-polish.test.js, Run 3) and the 24 kHz replay (notes-hires.test.js, Run 4), plus the note pop-up patched in place while it listens (pm-note-dock-test.js). notes-recording-reliability adds the duplication fixture (notes-duplication.test.js): duplicated audio and transcript across retry, restart, two tabs and a late chunk, and the live guards.",
|
|
3
3
|
"modules": [
|
|
4
4
|
"notes.test.js",
|
|
5
5
|
"notes-listen.test.js",
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"notes-qwen.test.js",
|
|
8
8
|
"notes-echo.test.js",
|
|
9
9
|
"notes-polish.test.js",
|
|
10
|
-
"notes-hires.test.js"
|
|
10
|
+
"notes-hires.test.js",
|
|
11
|
+
"notes-duplication.test.js"
|
|
11
12
|
],
|
|
12
13
|
"panels": [
|
|
13
14
|
"pm-notes-test.js",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"pm-note-dock-test.js": 20,
|
|
23
24
|
"notes-echo.test.js": 25,
|
|
24
25
|
"notes-polish.test.js": 3,
|
|
25
|
-
"notes-hires.test.js": 10
|
|
26
|
+
"notes-hires.test.js": 10,
|
|
27
|
+
"notes-duplication.test.js": 25
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative onboarding-scan-and-map, slice 2: the first-run routes (in-process) and the /welcome page (browser).",
|
|
3
|
+
"modules": [
|
|
4
|
+
"routes-onboarding.test.js",
|
|
5
|
+
"onboarding-scripts.test.js"
|
|
6
|
+
],
|
|
7
|
+
"panels": [
|
|
8
|
+
"pm-welcome-test.js"
|
|
9
|
+
],
|
|
10
|
+
"seconds": {
|
|
11
|
+
"routes-onboarding.test.js": 6,
|
|
12
|
+
"pm-welcome-test.js": 25,
|
|
13
|
+
"onboarding-scripts.test.js": 8
|
|
14
|
+
}
|
|
15
|
+
}
|